@memtensor/memos-local-openclaw-plugin 1.0.4-beta.2 → 1.0.4-beta.20
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 +7 -0
- package/README.md +111 -44
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +36 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +6 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +160 -26
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +22 -0
- package/dist/client/hub.js.map +1 -1
- package/dist/client/skill-sync.d.ts +7 -0
- package/dist/client/skill-sync.d.ts.map +1 -1
- package/dist/client/skill-sync.js +10 -0
- package/dist/client/skill-sync.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -3
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +9 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +500 -112
- package/dist/hub/server.js.map +1 -1
- package/dist/hub/user-manager.d.ts +11 -0
- package/dist/hub/user-manager.d.ts.map +1 -1
- package/dist/hub/user-manager.js +31 -3
- package/dist/hub/user-manager.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest/chunker.d.ts +2 -1
- package/dist/ingest/chunker.d.ts.map +1 -1
- package/dist/ingest/chunker.js +14 -10
- package/dist/ingest/chunker.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +96 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +84 -9
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.d.ts +1 -1
- package/dist/sharing/types.d.ts.map +1 -1
- package/dist/skill/evolver.d.ts +4 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +59 -5
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +2 -0
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +45 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/installer.d.ts +26 -0
- package/dist/skill/installer.d.ts.map +1 -1
- package/dist/skill/installer.js +80 -4
- package/dist/skill/installer.js.map +1 -1
- package/dist/skill/upgrader.d.ts +2 -0
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +139 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +3 -0
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +75 -0
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts +12 -0
- package/dist/storage/ensure-binding.d.ts.map +1 -0
- package/dist/storage/ensure-binding.js +53 -0
- package/dist/storage/ensure-binding.js.map +1 -0
- package/dist/storage/sqlite.d.ts +115 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +458 -110
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +156 -40
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +3 -1
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +2952 -910
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +39 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1198 -227
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +774 -74
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -2
- package/scripts/postinstall.cjs +1 -1
- package/skill/memos-memory-guide/SKILL.md +64 -26
- package/src/capture/index.ts +40 -1
- package/src/client/connector.ts +161 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +2 -3
- package/src/hub/server.ts +481 -107
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/chunker.ts +19 -13
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +89 -1
- package/src/shared/llm-call.ts +99 -10
- package/src/sharing/types.ts +1 -1
- package/src/skill/evolver.ts +63 -6
- package/src/skill/generator.ts +44 -5
- package/src/skill/installer.ts +107 -4
- package/src/skill/upgrader.ts +139 -1
- package/src/skill/validator.ts +79 -0
- package/src/storage/ensure-binding.ts +52 -0
- package/src/storage/sqlite.ts +498 -137
- package/src/telemetry.ts +172 -41
- package/src/tools/memory-search.ts +2 -1
- package/src/types.ts +12 -2
- package/src/viewer/html.ts +2952 -910
- package/src/viewer/server.ts +1109 -212
package/index.ts
CHANGED
|
@@ -9,19 +9,22 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
12
13
|
import { buildContext } from "./src/config";
|
|
13
14
|
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
15
|
+
import { ensureSqliteBinding } from "./src/storage/ensure-binding";
|
|
14
16
|
import { SqliteStore } from "./src/storage/sqlite";
|
|
15
17
|
import { Embedder } from "./src/embedding";
|
|
16
18
|
import { IngestWorker } from "./src/ingest/worker";
|
|
17
19
|
import { RecallEngine } from "./src/recall/engine";
|
|
18
20
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
19
21
|
import { DEFAULTS } from "./src/types";
|
|
22
|
+
import type { SearchHit } from "./src/types";
|
|
20
23
|
import { ViewerServer } from "./src/viewer/server";
|
|
21
24
|
import { HubServer } from "./src/hub/server";
|
|
22
25
|
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
23
26
|
import { getHubStatus, connectToHub } from "./src/client/connector";
|
|
24
|
-
import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub } from "./src/client/skill-sync";
|
|
27
|
+
import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
|
|
25
28
|
import { SkillEvolver } from "./src/skill/evolver";
|
|
26
29
|
import { SkillInstaller } from "./src/skill/installer";
|
|
27
30
|
import { Summarizer } from "./src/ingest/providers";
|
|
@@ -81,13 +84,20 @@ const memosLocalPlugin = {
|
|
|
81
84
|
|
|
82
85
|
register(api: OpenClawPluginApi) {
|
|
83
86
|
// ─── Ensure better-sqlite3 native module is available ───
|
|
84
|
-
const pluginDir = path.dirname(
|
|
87
|
+
const pluginDir = path.dirname(fileURLToPath(import.meta.url));
|
|
88
|
+
|
|
89
|
+
function normalizeFsPath(p: string): string {
|
|
90
|
+
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
let sqliteReady = false;
|
|
86
94
|
|
|
87
95
|
function trySqliteLoad(): boolean {
|
|
88
96
|
try {
|
|
89
97
|
const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
|
|
90
|
-
|
|
98
|
+
const resolvedNorm = normalizeFsPath(resolved);
|
|
99
|
+
const pluginNorm = normalizeFsPath(pluginDir);
|
|
100
|
+
if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
|
|
91
101
|
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
|
|
92
102
|
return false;
|
|
93
103
|
}
|
|
@@ -170,7 +180,7 @@ const memosLocalPlugin = {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
173
|
-
const stateDir = api.resolvePath("~/.openclaw");
|
|
183
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || api.resolvePath("~/.openclaw");
|
|
174
184
|
|
|
175
185
|
// Fallback: read config from file if not provided by OpenClaw
|
|
176
186
|
const configPath = path.join(stateDir, "state", "memos-local", "config.json");
|
|
@@ -196,6 +206,8 @@ const memosLocalPlugin = {
|
|
|
196
206
|
error: (msg: string) => api.logger.warn(`[error] ${msg}`),
|
|
197
207
|
}, hostModels);
|
|
198
208
|
|
|
209
|
+
ensureSqliteBinding(ctx.log);
|
|
210
|
+
|
|
199
211
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
200
212
|
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
201
213
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
@@ -205,6 +217,7 @@ const memosLocalPlugin = {
|
|
|
205
217
|
const workspaceDir = api.resolvePath("~/.openclaw/workspace");
|
|
206
218
|
const skillCtx = { ...ctx, workspaceDir };
|
|
207
219
|
const skillEvolver = new SkillEvolver(store, engine, skillCtx);
|
|
220
|
+
skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
|
|
208
221
|
const skillInstaller = new SkillInstaller(store, skillCtx);
|
|
209
222
|
|
|
210
223
|
let pluginVersion = "0.0.0";
|
|
@@ -269,6 +282,18 @@ const memosLocalPlugin = {
|
|
|
269
282
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
270
283
|
let currentAgentId = "main";
|
|
271
284
|
|
|
285
|
+
// ─── Check allowPromptInjection policy ───
|
|
286
|
+
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
287
|
+
// will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
|
|
288
|
+
const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
|
|
289
|
+
const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
|
|
290
|
+
if (!allowPromptInjection) {
|
|
291
|
+
api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
295
|
+
}
|
|
296
|
+
|
|
272
297
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
273
298
|
async (...args: any[]) => {
|
|
274
299
|
const t0 = performance.now();
|
|
@@ -280,6 +305,7 @@ const memosLocalPlugin = {
|
|
|
280
305
|
return result;
|
|
281
306
|
} catch (e) {
|
|
282
307
|
ok = false;
|
|
308
|
+
telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
|
|
283
309
|
throw e;
|
|
284
310
|
} finally {
|
|
285
311
|
const dur = performance.now() - t0;
|
|
@@ -293,6 +319,21 @@ const memosLocalPlugin = {
|
|
|
293
319
|
candidates: det.candidates,
|
|
294
320
|
filtered: det.hits ?? det.filtered ?? [],
|
|
295
321
|
});
|
|
322
|
+
} else if (det && det.local && det.hub) {
|
|
323
|
+
const localHits = det.local?.hits ?? [];
|
|
324
|
+
const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
|
|
325
|
+
score: h.score ?? 0,
|
|
326
|
+
role: h.source?.role ?? h.role ?? "assistant",
|
|
327
|
+
summary: h.summary ?? "",
|
|
328
|
+
original_excerpt: h.excerpt ?? h.summary ?? "",
|
|
329
|
+
origin: "hub-remote",
|
|
330
|
+
ownerName: h.ownerName ?? "",
|
|
331
|
+
groupName: h.groupName ?? "",
|
|
332
|
+
}));
|
|
333
|
+
outputText = JSON.stringify({
|
|
334
|
+
candidates: [...localHits, ...hubHits],
|
|
335
|
+
filtered: [...localHits, ...hubHits],
|
|
336
|
+
});
|
|
296
337
|
} else {
|
|
297
338
|
outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
|
|
298
339
|
}
|
|
@@ -301,6 +342,93 @@ const memosLocalPlugin = {
|
|
|
301
342
|
}
|
|
302
343
|
};
|
|
303
344
|
|
|
345
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
346
|
+
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
347
|
+
scope === "group" || scope === "all" ? scope : "local";
|
|
348
|
+
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
349
|
+
target === "hub" || target === "both" ? target : "agents";
|
|
350
|
+
const resolveMemoryUnshareTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
351
|
+
target === "agents" || target === "hub" ? target : "all";
|
|
352
|
+
const resolveSkillPublishTarget = (target?: string, scope?: string): "agents" | "hub" => {
|
|
353
|
+
if (target === "hub") return "hub";
|
|
354
|
+
if (target === "agents") return "agents";
|
|
355
|
+
return scope === "public" || scope === "group" ? "hub" : "agents";
|
|
356
|
+
};
|
|
357
|
+
const resolveSkillHubVisibility = (visibility?: string, scope?: string): "public" | "group" =>
|
|
358
|
+
visibility === "group" || scope === "group" ? "group" : "public";
|
|
359
|
+
const resolveSkillUnpublishTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
360
|
+
target === "hub" || target === "all" ? target : "agents";
|
|
361
|
+
|
|
362
|
+
const shareMemoryToHub = async (
|
|
363
|
+
chunkId: string,
|
|
364
|
+
input?: { visibility?: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
|
|
365
|
+
): Promise<{ memoryId: string; visibility: "public" | "group"; groupId: string | null }> => {
|
|
366
|
+
const chunk = store.getChunk(chunkId);
|
|
367
|
+
if (!chunk) {
|
|
368
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const visibility = input?.visibility === "group" ? "group" : "public";
|
|
372
|
+
const groupId = visibility === "group" ? (input?.groupId ?? null) : null;
|
|
373
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
374
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
memory: {
|
|
378
|
+
sourceChunkId: chunk.id,
|
|
379
|
+
role: chunk.role,
|
|
380
|
+
content: chunk.content,
|
|
381
|
+
summary: chunk.summary,
|
|
382
|
+
kind: chunk.kind,
|
|
383
|
+
groupId,
|
|
384
|
+
visibility,
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
}) as { memoryId?: string; visibility?: "public" | "group" };
|
|
388
|
+
|
|
389
|
+
const memoryId = response?.memoryId ?? `${chunk.id}-hub`;
|
|
390
|
+
|
|
391
|
+
// Hub role: full hub_memories row for local recall/embeddings. Client: metadata only (team_shared_chunks) for UI.
|
|
392
|
+
if (ctx.config.sharing?.role === "hub") {
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
395
|
+
store.upsertHubMemory({
|
|
396
|
+
id: memoryId,
|
|
397
|
+
sourceChunkId: chunk.id,
|
|
398
|
+
sourceUserId: hubClient.userId,
|
|
399
|
+
role: chunk.role,
|
|
400
|
+
content: chunk.content,
|
|
401
|
+
summary: chunk.summary ?? "",
|
|
402
|
+
kind: chunk.kind,
|
|
403
|
+
groupId,
|
|
404
|
+
visibility,
|
|
405
|
+
createdAt: existing?.createdAt ?? now,
|
|
406
|
+
updatedAt: now,
|
|
407
|
+
});
|
|
408
|
+
} else if (ctx.config.sharing?.enabled && hubClient.userId) {
|
|
409
|
+
store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: memoryId, visibility, groupId });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { memoryId, visibility, groupId };
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const unshareMemoryFromHub = async (
|
|
416
|
+
chunkId: string,
|
|
417
|
+
input?: { hubAddress?: string; userToken?: string },
|
|
418
|
+
): Promise<void> => {
|
|
419
|
+
const chunk = store.getChunk(chunkId);
|
|
420
|
+
if (!chunk) {
|
|
421
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
422
|
+
}
|
|
423
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
424
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
425
|
+
method: "POST",
|
|
426
|
+
body: JSON.stringify({ sourceChunkId: chunk.id }),
|
|
427
|
+
});
|
|
428
|
+
store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
|
|
429
|
+
store.deleteTeamSharedChunk(chunk.id);
|
|
430
|
+
};
|
|
431
|
+
|
|
304
432
|
// ─── Tool: memory_search ───
|
|
305
433
|
|
|
306
434
|
api.registerTool(
|
|
@@ -309,24 +437,46 @@ const memosLocalPlugin = {
|
|
|
309
437
|
label: "Memory Search",
|
|
310
438
|
description:
|
|
311
439
|
"Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"Pass only a short natural-language query (2-5 key words).",
|
|
440
|
+
"Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
|
|
441
|
+
"Supports optional maxResults, minScore, and role filtering when you need tighter control.",
|
|
315
442
|
parameters: Type.Object({
|
|
316
443
|
query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
|
|
444
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
|
|
445
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
|
|
446
|
+
minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
|
|
447
|
+
role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
|
|
448
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
449
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
317
450
|
}),
|
|
318
451
|
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
319
|
-
const {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
452
|
+
const {
|
|
453
|
+
query,
|
|
454
|
+
scope: rawScope,
|
|
455
|
+
maxResults,
|
|
456
|
+
minScore: rawMinScore,
|
|
457
|
+
role: rawRole,
|
|
458
|
+
hubAddress,
|
|
459
|
+
userToken,
|
|
460
|
+
} = params as {
|
|
461
|
+
query: string;
|
|
462
|
+
scope?: string;
|
|
463
|
+
maxResults?: number;
|
|
464
|
+
minScore?: number;
|
|
465
|
+
role?: string;
|
|
466
|
+
hubAddress?: string;
|
|
467
|
+
userToken?: string;
|
|
468
|
+
};
|
|
469
|
+
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
470
|
+
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
471
|
+
let searchScope = resolveMemorySearchScope(rawScope);
|
|
472
|
+
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
473
|
+
searchScope = "all";
|
|
474
|
+
}
|
|
475
|
+
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
326
476
|
|
|
327
477
|
const agentId = currentAgentId;
|
|
328
|
-
const ownerFilter = [
|
|
329
|
-
const effectiveMaxResults =
|
|
478
|
+
const ownerFilter = [getCurrentOwner(), "public"];
|
|
479
|
+
const effectiveMaxResults = searchLimit;
|
|
330
480
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
331
481
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
332
482
|
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
@@ -337,9 +487,10 @@ const memosLocalPlugin = {
|
|
|
337
487
|
score: h.score,
|
|
338
488
|
summary: h.summary,
|
|
339
489
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
490
|
+
origin: h.origin || "local",
|
|
340
491
|
}));
|
|
341
492
|
|
|
342
|
-
if (result.hits.length === 0) {
|
|
493
|
+
if (result.hits.length === 0 && searchScope === "local") {
|
|
343
494
|
return {
|
|
344
495
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
345
496
|
details: { candidates: [], meta: result.meta },
|
|
@@ -363,11 +514,13 @@ const memosLocalPlugin = {
|
|
|
363
514
|
const indexSet = new Set(filterResult.relevant);
|
|
364
515
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
365
516
|
ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
|
|
366
|
-
} else {
|
|
517
|
+
} else if (searchScope === "local") {
|
|
367
518
|
return {
|
|
368
519
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
369
520
|
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
370
521
|
};
|
|
522
|
+
} else {
|
|
523
|
+
filteredHits = [];
|
|
371
524
|
}
|
|
372
525
|
}
|
|
373
526
|
|
|
@@ -389,29 +542,79 @@ const memosLocalPlugin = {
|
|
|
389
542
|
role: h.source.role,
|
|
390
543
|
score: h.score,
|
|
391
544
|
summary: h.summary,
|
|
545
|
+
origin: h.origin || "local",
|
|
392
546
|
};
|
|
393
547
|
});
|
|
394
548
|
|
|
395
549
|
if (searchScope !== "local") {
|
|
396
550
|
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
551
|
+
|
|
552
|
+
let filteredHubHits = hub.hits;
|
|
553
|
+
if (hub.hits.length > 0) {
|
|
554
|
+
const hubCandidates = hub.hits.map((h, i) => ({
|
|
555
|
+
index: filteredHits.length + i + 1,
|
|
556
|
+
role: (h.source?.role || "assistant") as string,
|
|
557
|
+
content: (h.summary || h.excerpt || "").slice(0, 300),
|
|
558
|
+
time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
559
|
+
}));
|
|
560
|
+
const localCandidatesForMerge = filteredHits.map((h, i) => ({
|
|
561
|
+
index: i + 1,
|
|
562
|
+
role: h.source.role,
|
|
563
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
564
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
565
|
+
}));
|
|
566
|
+
const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
|
|
567
|
+
const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
|
|
568
|
+
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
569
|
+
const relevantSet = new Set(mergedFilter.relevant);
|
|
570
|
+
const hubStartIdx = filteredHits.length + 1;
|
|
571
|
+
filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
|
|
572
|
+
filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
|
|
573
|
+
ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const originLabel = (h: SearchHit) => {
|
|
578
|
+
if (h.origin === "hub-memory") return " [团队缓存]";
|
|
579
|
+
if (h.origin === "local-shared") return " [本机共享]";
|
|
580
|
+
return "";
|
|
581
|
+
};
|
|
397
582
|
const localText = filteredHits.length > 0
|
|
398
583
|
? filteredHits.map((h, i) => {
|
|
399
584
|
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
400
|
-
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
585
|
+
return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
|
|
401
586
|
}).join("\n")
|
|
402
587
|
: "(none)";
|
|
403
|
-
const hubText =
|
|
404
|
-
?
|
|
588
|
+
const hubText = filteredHubHits.length > 0
|
|
589
|
+
? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
405
590
|
: "(none)";
|
|
406
591
|
|
|
592
|
+
const localDetailsFiltered = filteredHits.map((h) => {
|
|
593
|
+
let effectiveTaskId = h.taskId;
|
|
594
|
+
if (effectiveTaskId) {
|
|
595
|
+
const t = store.getTask(effectiveTaskId);
|
|
596
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
ref: h.ref,
|
|
600
|
+
chunkId: h.ref.chunkId,
|
|
601
|
+
taskId: effectiveTaskId,
|
|
602
|
+
skillId: h.skillId,
|
|
603
|
+
role: h.source.role,
|
|
604
|
+
score: h.score,
|
|
605
|
+
summary: h.summary,
|
|
606
|
+
origin: h.origin,
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
|
|
407
610
|
return {
|
|
408
611
|
content: [{
|
|
409
612
|
type: "text",
|
|
410
613
|
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
411
614
|
}],
|
|
412
615
|
details: {
|
|
413
|
-
local: { hits:
|
|
414
|
-
hub,
|
|
616
|
+
local: { hits: localDetailsFiltered, meta: result.meta },
|
|
617
|
+
hub: { ...hub, hits: filteredHubHits },
|
|
415
618
|
},
|
|
416
619
|
};
|
|
417
620
|
}
|
|
@@ -423,9 +626,15 @@ const memosLocalPlugin = {
|
|
|
423
626
|
};
|
|
424
627
|
}
|
|
425
628
|
|
|
629
|
+
const originTag = (o?: string) => {
|
|
630
|
+
if (o === "local-shared") return " [本机共享]";
|
|
631
|
+
if (o === "hub-memory") return " [团队缓存]";
|
|
632
|
+
if (o === "hub-remote") return " [团队]";
|
|
633
|
+
return "";
|
|
634
|
+
};
|
|
426
635
|
const lines = filteredHits.map((h, i) => {
|
|
427
636
|
const excerpt = h.original_excerpt;
|
|
428
|
-
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
637
|
+
const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
|
|
429
638
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
430
639
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
431
640
|
if (h.taskId) {
|
|
@@ -480,6 +689,7 @@ const memosLocalPlugin = {
|
|
|
480
689
|
score: h.score,
|
|
481
690
|
summary: h.summary,
|
|
482
691
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
692
|
+
origin: h.origin || "local",
|
|
483
693
|
};
|
|
484
694
|
}),
|
|
485
695
|
meta: result.meta,
|
|
@@ -843,7 +1053,9 @@ ${detail.content}`,
|
|
|
843
1053
|
{
|
|
844
1054
|
name: "network_team_info",
|
|
845
1055
|
label: "Network Team Info",
|
|
846
|
-
description:
|
|
1056
|
+
description:
|
|
1057
|
+
"Show current Hub connection status, signed-in user, role, and group memberships. " +
|
|
1058
|
+
"Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
|
|
847
1059
|
parameters: Type.Object({}),
|
|
848
1060
|
execute: trackTool("network_team_info", async () => {
|
|
849
1061
|
const status = await getHubStatus(store, ctx.config);
|
|
@@ -927,10 +1139,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
927
1139
|
};
|
|
928
1140
|
}
|
|
929
1141
|
|
|
1142
|
+
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1143
|
+
let footer = "\n\n---\n";
|
|
1144
|
+
|
|
1145
|
+
if (manifest && manifest.hasCompanionFiles) {
|
|
1146
|
+
const fileSummary = manifest.files
|
|
1147
|
+
.filter(f => f.type !== "eval")
|
|
1148
|
+
.map(f => `\`${f.relativePath}\``)
|
|
1149
|
+
.join(", ");
|
|
1150
|
+
footer += `**Companion files available:** ${fileSummary}\n`;
|
|
1151
|
+
footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
|
|
1152
|
+
footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
|
|
1153
|
+
if (manifest.installMode === "install_recommended") {
|
|
1154
|
+
footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
|
|
1155
|
+
}
|
|
1156
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1157
|
+
footer += `> Already installed at: ${manifest.installedPath}/\n`;
|
|
1158
|
+
}
|
|
1159
|
+
} else {
|
|
1160
|
+
footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
930
1163
|
return {
|
|
931
1164
|
content: [{
|
|
932
1165
|
type: "text",
|
|
933
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}
|
|
1166
|
+
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
934
1167
|
}],
|
|
935
1168
|
details: {
|
|
936
1169
|
skillId: skill.id,
|
|
@@ -938,6 +1171,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
938
1171
|
version: skill.version,
|
|
939
1172
|
status: skill.status,
|
|
940
1173
|
installed: skill.installed,
|
|
1174
|
+
companionFiles: manifest?.hasCompanionFiles ?? false,
|
|
1175
|
+
installMode: manifest?.installMode ?? "inline",
|
|
941
1176
|
},
|
|
942
1177
|
};
|
|
943
1178
|
}),
|
|
@@ -973,9 +1208,116 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
973
1208
|
{ name: "skill_install" },
|
|
974
1209
|
);
|
|
975
1210
|
|
|
1211
|
+
// ─── Tool: skill_files ───
|
|
1212
|
+
|
|
1213
|
+
api.registerTool(
|
|
1214
|
+
{
|
|
1215
|
+
name: "skill_files",
|
|
1216
|
+
label: "List Skill Companion Files",
|
|
1217
|
+
description:
|
|
1218
|
+
"List companion files (scripts, references, evals) for a skill. " +
|
|
1219
|
+
"Use this after skill_get to see what additional files are available. " +
|
|
1220
|
+
"Returns file names, sizes, and whether the skill recommends installation.",
|
|
1221
|
+
parameters: Type.Object({
|
|
1222
|
+
skillId: Type.String({ description: "The skill_id to inspect" }),
|
|
1223
|
+
}),
|
|
1224
|
+
execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
|
|
1225
|
+
const { skillId } = params as { skillId: string };
|
|
1226
|
+
ctx.log.debug(`skill_files called for skill=${skillId}`);
|
|
1227
|
+
|
|
1228
|
+
const manifest = skillInstaller.getCompanionManifest(skillId);
|
|
1229
|
+
if (!manifest) {
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: "text", text: `Skill not found: ${skillId}` }],
|
|
1232
|
+
details: { error: "not_found" },
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (!manifest.hasCompanionFiles) {
|
|
1237
|
+
return {
|
|
1238
|
+
content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
|
|
1239
|
+
details: manifest,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
|
|
1244
|
+
if (manifest.scriptsCount > 0) {
|
|
1245
|
+
lines.push(`### Scripts (${manifest.scriptsCount})`);
|
|
1246
|
+
for (const f of manifest.files.filter(f => f.type === "script")) {
|
|
1247
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (manifest.referencesCount > 0) {
|
|
1251
|
+
lines.push(`\n### References (${manifest.referencesCount})`);
|
|
1252
|
+
for (const f of manifest.files.filter(f => f.type === "reference")) {
|
|
1253
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (manifest.evalsCount > 0) {
|
|
1257
|
+
lines.push(`\n### Evals (${manifest.evalsCount})`);
|
|
1258
|
+
for (const f of manifest.files.filter(f => f.type === "eval")) {
|
|
1259
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (manifest.installMode === "install_recommended") {
|
|
1264
|
+
lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
|
|
1265
|
+
}
|
|
1266
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1267
|
+
lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1272
|
+
details: manifest,
|
|
1273
|
+
};
|
|
1274
|
+
}),
|
|
1275
|
+
},
|
|
1276
|
+
{ name: "skill_files" },
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
// ─── Tool: skill_file_get ───
|
|
1280
|
+
|
|
1281
|
+
api.registerTool(
|
|
1282
|
+
{
|
|
1283
|
+
name: "skill_file_get",
|
|
1284
|
+
label: "Get Skill Companion File",
|
|
1285
|
+
description:
|
|
1286
|
+
"Read the content of a specific companion file (script, reference) from a skill. " +
|
|
1287
|
+
"Use after skill_files to retrieve a script or reference document. " +
|
|
1288
|
+
"Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
|
|
1289
|
+
parameters: Type.Object({
|
|
1290
|
+
skillId: Type.String({ description: "The skill_id" }),
|
|
1291
|
+
path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
|
|
1292
|
+
}),
|
|
1293
|
+
execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
|
|
1294
|
+
const { skillId, path: filePath } = params as { skillId: string; path: string };
|
|
1295
|
+
ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
|
|
1296
|
+
|
|
1297
|
+
const result = skillInstaller.readCompanionFile(skillId, filePath);
|
|
1298
|
+
if ("error" in result) {
|
|
1299
|
+
return {
|
|
1300
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
1301
|
+
details: result,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const ext = filePath.split(".").pop() || "";
|
|
1306
|
+
const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
|
|
1310
|
+
details: { path: filePath, size: result.size },
|
|
1311
|
+
};
|
|
1312
|
+
}),
|
|
1313
|
+
},
|
|
1314
|
+
{ name: "skill_file_get" },
|
|
1315
|
+
);
|
|
1316
|
+
|
|
976
1317
|
// ─── Tool: memory_viewer ───
|
|
977
1318
|
|
|
978
|
-
const
|
|
1319
|
+
const gatewayPort = (api.config as any)?.gateway?.port ?? 18789;
|
|
1320
|
+
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
979
1321
|
|
|
980
1322
|
api.registerTool(
|
|
981
1323
|
{
|
|
@@ -988,6 +1330,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
988
1330
|
parameters: Type.Object({}),
|
|
989
1331
|
execute: trackTool("memory_viewer", async () => {
|
|
990
1332
|
ctx.log.debug(`memory_viewer called`);
|
|
1333
|
+
telemetry.trackViewerOpened();
|
|
991
1334
|
const url = `http://127.0.0.1:${viewerPort}`;
|
|
992
1335
|
return {
|
|
993
1336
|
content: [
|
|
@@ -1018,12 +1361,13 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1018
1361
|
api.registerTool(
|
|
1019
1362
|
{
|
|
1020
1363
|
name: "memory_write_public",
|
|
1021
|
-
label: "Write
|
|
1364
|
+
label: "Write Local Shared Memory",
|
|
1022
1365
|
description:
|
|
1023
|
-
"Write a piece of information to
|
|
1024
|
-
"Use this
|
|
1366
|
+
"Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
|
|
1367
|
+
"Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
|
|
1368
|
+
"If you already have a memory chunk and want to expose it, use memory_share instead.",
|
|
1025
1369
|
parameters: Type.Object({
|
|
1026
|
-
content: Type.String({ description: "The content to write to
|
|
1370
|
+
content: Type.String({ description: "The content to write to local shared memory" }),
|
|
1027
1371
|
summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
|
|
1028
1372
|
}),
|
|
1029
1373
|
execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
|
|
@@ -1068,7 +1412,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1068
1412
|
}
|
|
1069
1413
|
|
|
1070
1414
|
return {
|
|
1071
|
-
content: [{ type: "text", text: `
|
|
1415
|
+
content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
|
|
1072
1416
|
details: { chunkId, owner: "public" },
|
|
1073
1417
|
};
|
|
1074
1418
|
}),
|
|
@@ -1076,6 +1420,164 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1076
1420
|
{ name: "memory_write_public" },
|
|
1077
1421
|
);
|
|
1078
1422
|
|
|
1423
|
+
api.registerTool(
|
|
1424
|
+
{
|
|
1425
|
+
name: "memory_share",
|
|
1426
|
+
label: "Share Memory",
|
|
1427
|
+
description:
|
|
1428
|
+
"Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
|
|
1429
|
+
"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. " +
|
|
1430
|
+
"If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
|
|
1431
|
+
parameters: Type.Object({
|
|
1432
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1433
|
+
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
1434
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
|
|
1435
|
+
groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
|
|
1436
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1437
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1438
|
+
}),
|
|
1439
|
+
execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
|
|
1440
|
+
const {
|
|
1441
|
+
chunkId,
|
|
1442
|
+
target: rawTarget,
|
|
1443
|
+
visibility: rawVisibility,
|
|
1444
|
+
groupId,
|
|
1445
|
+
hubAddress,
|
|
1446
|
+
userToken,
|
|
1447
|
+
} = params as {
|
|
1448
|
+
chunkId: string;
|
|
1449
|
+
target?: string;
|
|
1450
|
+
visibility?: string;
|
|
1451
|
+
groupId?: string;
|
|
1452
|
+
hubAddress?: string;
|
|
1453
|
+
userToken?: string;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
const chunk = store.getChunk(chunkId);
|
|
1457
|
+
if (!chunk) {
|
|
1458
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const target = resolveMemoryShareTarget(rawTarget);
|
|
1462
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
1463
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1464
|
+
const messages: string[] = [];
|
|
1465
|
+
|
|
1466
|
+
if (target === "agents" || target === "both") {
|
|
1467
|
+
const local = store.markMemorySharedLocally(chunkId);
|
|
1468
|
+
if (!local.ok) {
|
|
1469
|
+
return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
|
|
1470
|
+
}
|
|
1471
|
+
details.local = {
|
|
1472
|
+
shared: true,
|
|
1473
|
+
owner: local.owner,
|
|
1474
|
+
originalOwner: local.originalOwner ?? null,
|
|
1475
|
+
};
|
|
1476
|
+
messages.push("shared to local agents");
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (target === "hub" || target === "both") {
|
|
1480
|
+
const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
|
|
1481
|
+
details.hub = {
|
|
1482
|
+
shared: true,
|
|
1483
|
+
memoryId: hub.memoryId,
|
|
1484
|
+
visibility: hub.visibility,
|
|
1485
|
+
groupId: hub.groupId,
|
|
1486
|
+
};
|
|
1487
|
+
messages.push(`shared to Hub (${hub.visibility})`);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
return {
|
|
1491
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1492
|
+
details,
|
|
1493
|
+
};
|
|
1494
|
+
}),
|
|
1495
|
+
},
|
|
1496
|
+
{ name: "memory_share" },
|
|
1497
|
+
);
|
|
1498
|
+
|
|
1499
|
+
api.registerTool(
|
|
1500
|
+
{
|
|
1501
|
+
name: "memory_unshare",
|
|
1502
|
+
label: "Unshare Memory",
|
|
1503
|
+
description:
|
|
1504
|
+
"Remove sharing from an existing memory. Use target='agents' to stop local multi-agent sharing, target='hub' to remove it from Hub, or target='all' (default) to remove both. " +
|
|
1505
|
+
"privateOwner is only needed for older public memories that were never tracked with an original owner.",
|
|
1506
|
+
parameters: Type.Object({
|
|
1507
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
|
|
1508
|
+
target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
|
|
1509
|
+
privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
|
|
1510
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1511
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1512
|
+
}),
|
|
1513
|
+
execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
|
|
1514
|
+
const {
|
|
1515
|
+
chunkId,
|
|
1516
|
+
target: rawTarget,
|
|
1517
|
+
privateOwner,
|
|
1518
|
+
hubAddress,
|
|
1519
|
+
userToken,
|
|
1520
|
+
} = params as {
|
|
1521
|
+
chunkId: string;
|
|
1522
|
+
target?: string;
|
|
1523
|
+
privateOwner?: string;
|
|
1524
|
+
hubAddress?: string;
|
|
1525
|
+
userToken?: string;
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const chunk = store.getChunk(chunkId);
|
|
1529
|
+
if (!chunk) {
|
|
1530
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const target = resolveMemoryUnshareTarget(rawTarget);
|
|
1534
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1535
|
+
const messages: string[] = [];
|
|
1536
|
+
|
|
1537
|
+
if (target === "agents" || target === "all") {
|
|
1538
|
+
const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1539
|
+
if (!local.ok) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: [{
|
|
1542
|
+
type: "text",
|
|
1543
|
+
text: local.reason === "original_owner_missing"
|
|
1544
|
+
? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
|
|
1545
|
+
: `Failed to stop local sharing for memory ${chunkId}.`,
|
|
1546
|
+
}],
|
|
1547
|
+
details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
details.local = {
|
|
1551
|
+
shared: false,
|
|
1552
|
+
owner: local.owner,
|
|
1553
|
+
};
|
|
1554
|
+
messages.push("removed from local agent sharing");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (target === "hub" || target === "all") {
|
|
1558
|
+
try {
|
|
1559
|
+
await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
|
|
1560
|
+
details.hub = { shared: false };
|
|
1561
|
+
messages.push("removed from Hub");
|
|
1562
|
+
} catch (err) {
|
|
1563
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1564
|
+
if (target === "all" && msg.includes("hub client connection is not configured")) {
|
|
1565
|
+
details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
|
|
1566
|
+
} else {
|
|
1567
|
+
throw err;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
return {
|
|
1573
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1574
|
+
details,
|
|
1575
|
+
};
|
|
1576
|
+
}),
|
|
1577
|
+
},
|
|
1578
|
+
{ name: "memory_unshare" },
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1079
1581
|
// ─── Tool: skill_search ───
|
|
1080
1582
|
|
|
1081
1583
|
api.registerTool(
|
|
@@ -1083,16 +1585,16 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1083
1585
|
name: "skill_search",
|
|
1084
1586
|
label: "Skill Search",
|
|
1085
1587
|
description:
|
|
1086
|
-
"Search available skills by natural language.
|
|
1087
|
-
"Use when you need a capability or guide and don't have a matching skill at hand.",
|
|
1588
|
+
"Search available skills by natural language. Use scope='mix' (default) for this agent plus local shared skills, 'self' for this agent only, 'public' for local shared skills only, or 'group'/'all' to include Hub skills as well. " +
|
|
1589
|
+
"Use this when you need a capability or guide and don't have a matching skill at hand.",
|
|
1088
1590
|
parameters: Type.Object({
|
|
1089
1591
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1090
|
-
scope: Type.Optional(Type.String({ description: "Search scope: 'mix'
|
|
1592
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1091
1593
|
}),
|
|
1092
1594
|
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
1093
1595
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1094
1596
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1095
|
-
const currentOwner =
|
|
1597
|
+
const currentOwner = getCurrentOwner();
|
|
1096
1598
|
|
|
1097
1599
|
if (rawScope === "group" || rawScope === "all") {
|
|
1098
1600
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1108,7 +1610,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1108
1610
|
}
|
|
1109
1611
|
|
|
1110
1612
|
const localText = localHits.length > 0
|
|
1111
|
-
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (
|
|
1613
|
+
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
|
|
1112
1614
|
: "(none)";
|
|
1113
1615
|
const hubText = hub.hits.length > 0
|
|
1114
1616
|
? hub.hits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
|
|
@@ -1130,7 +1632,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1130
1632
|
}
|
|
1131
1633
|
|
|
1132
1634
|
const text = hits.map((h, i) =>
|
|
1133
|
-
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (
|
|
1635
|
+
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
|
|
1134
1636
|
).join("\n");
|
|
1135
1637
|
|
|
1136
1638
|
return {
|
|
@@ -1148,31 +1650,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1148
1650
|
{
|
|
1149
1651
|
name: "skill_publish",
|
|
1150
1652
|
label: "Publish Skill",
|
|
1151
|
-
description:
|
|
1653
|
+
description:
|
|
1654
|
+
"Share a skill with local agents or publish it to the Hub. " +
|
|
1655
|
+
"Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
|
|
1656
|
+
"The old scope parameter is still accepted for backward compatibility.",
|
|
1152
1657
|
parameters: Type.Object({
|
|
1153
1658
|
skillId: Type.String({ description: "The skill ID to publish" }),
|
|
1154
|
-
|
|
1659
|
+
target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
|
|
1660
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
|
|
1661
|
+
scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
|
|
1155
1662
|
groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
|
|
1156
1663
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1157
1664
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1158
1665
|
}),
|
|
1159
1666
|
execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
|
|
1160
|
-
const {
|
|
1667
|
+
const {
|
|
1668
|
+
skillId: pubSkillId,
|
|
1669
|
+
target: rawTarget,
|
|
1670
|
+
visibility: rawVisibility,
|
|
1671
|
+
scope,
|
|
1672
|
+
groupId,
|
|
1673
|
+
hubAddress,
|
|
1674
|
+
userToken,
|
|
1675
|
+
} = params as {
|
|
1676
|
+
skillId: string;
|
|
1677
|
+
target?: string;
|
|
1678
|
+
visibility?: string;
|
|
1679
|
+
scope?: string;
|
|
1680
|
+
groupId?: string;
|
|
1681
|
+
hubAddress?: string;
|
|
1682
|
+
userToken?: string;
|
|
1683
|
+
};
|
|
1161
1684
|
const skill = store.getSkill(pubSkillId);
|
|
1162
1685
|
if (!skill) {
|
|
1163
1686
|
return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
|
|
1164
1687
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1688
|
+
const target = resolveSkillPublishTarget(rawTarget, scope);
|
|
1689
|
+
const visibility = resolveSkillHubVisibility(rawVisibility, scope);
|
|
1690
|
+
if (target === "hub") {
|
|
1691
|
+
const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
|
|
1167
1692
|
return {
|
|
1168
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
1169
|
-
details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1693
|
+
content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
|
|
1694
|
+
details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1170
1695
|
};
|
|
1171
1696
|
}
|
|
1172
1697
|
store.setSkillVisibility(pubSkillId, "public");
|
|
1173
1698
|
return {
|
|
1174
|
-
content: [{ type: "text", text: `Skill "${skill.name}" is now
|
|
1175
|
-
details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
|
|
1699
|
+
content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
|
|
1700
|
+
details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
|
|
1176
1701
|
};
|
|
1177
1702
|
}),
|
|
1178
1703
|
},
|
|
@@ -1185,20 +1710,46 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1185
1710
|
{
|
|
1186
1711
|
name: "skill_unpublish",
|
|
1187
1712
|
label: "Unpublish Skill",
|
|
1188
|
-
description:
|
|
1713
|
+
description:
|
|
1714
|
+
"Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
|
|
1715
|
+
"Use target='agents' (default), 'hub', or 'all'.",
|
|
1189
1716
|
parameters: Type.Object({
|
|
1190
1717
|
skillId: Type.String({ description: "The skill ID to unpublish" }),
|
|
1718
|
+
target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
|
|
1719
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1720
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1191
1721
|
}),
|
|
1192
1722
|
execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
|
|
1193
|
-
const { skillId: unpubSkillId } = params as { skillId: string };
|
|
1723
|
+
const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
|
|
1194
1724
|
const skill = store.getSkill(unpubSkillId);
|
|
1195
1725
|
if (!skill) {
|
|
1196
1726
|
return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
|
|
1197
1727
|
}
|
|
1198
|
-
|
|
1728
|
+
const resolvedTarget = resolveSkillUnpublishTarget(target);
|
|
1729
|
+
const messages: string[] = [];
|
|
1730
|
+
const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
|
|
1731
|
+
if (resolvedTarget === "hub" || resolvedTarget === "all") {
|
|
1732
|
+
try {
|
|
1733
|
+
await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
|
|
1734
|
+
details.hub = { unpublished: true };
|
|
1735
|
+
messages.push("removed from Hub sharing");
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1738
|
+
if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
|
|
1739
|
+
details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
|
|
1740
|
+
} else {
|
|
1741
|
+
throw err;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (resolvedTarget === "agents" || resolvedTarget === "all") {
|
|
1746
|
+
store.setSkillVisibility(unpubSkillId, "private");
|
|
1747
|
+
details.local = { visibility: "private" };
|
|
1748
|
+
messages.push("limited to this agent");
|
|
1749
|
+
}
|
|
1199
1750
|
return {
|
|
1200
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
1201
|
-
details
|
|
1751
|
+
content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
|
|
1752
|
+
details,
|
|
1202
1753
|
};
|
|
1203
1754
|
}),
|
|
1204
1755
|
},
|
|
@@ -1231,6 +1782,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1231
1782
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1232
1783
|
|
|
1233
1784
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1785
|
+
if (!allowPromptInjection) return {};
|
|
1234
1786
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1235
1787
|
|
|
1236
1788
|
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
@@ -1272,11 +1824,73 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1272
1824
|
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1273
1825
|
|
|
1274
1826
|
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1827
|
+
|
|
1828
|
+
// Hub fallback helper: search team shared memories when local search has no relevant results
|
|
1829
|
+
const hubFallback = async (): Promise<SearchHit[]> => {
|
|
1830
|
+
if (!ctx.config?.sharing?.enabled) return [];
|
|
1831
|
+
try {
|
|
1832
|
+
const hubResult = await hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" });
|
|
1833
|
+
if (hubResult.hits.length === 0) return [];
|
|
1834
|
+
ctx.log.debug(`auto-recall: hub fallback returned ${hubResult.hits.length} hit(s)`);
|
|
1835
|
+
return hubResult.hits.map((h) => ({
|
|
1836
|
+
summary: h.summary,
|
|
1837
|
+
original_excerpt: h.excerpt || h.summary,
|
|
1838
|
+
ref: { sessionKey: "", chunkId: h.remoteHitId, turnId: "", seq: 0 },
|
|
1839
|
+
score: 0.9,
|
|
1840
|
+
taskId: null,
|
|
1841
|
+
skillId: null,
|
|
1842
|
+
origin: "hub-remote" as const,
|
|
1843
|
+
source: { ts: h.source.ts, role: h.source.role, sessionKey: "" },
|
|
1844
|
+
}));
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
ctx.log.debug(`auto-recall: hub fallback failed (${err})`);
|
|
1847
|
+
return [];
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
if (result.hits.length === 0) {
|
|
1852
|
+
// Local found nothing — try hub before giving up
|
|
1853
|
+
const hubHits = await hubFallback();
|
|
1854
|
+
if (hubHits.length > 0) {
|
|
1855
|
+
result.hits.push(...hubHits);
|
|
1856
|
+
ctx.log.debug(`auto-recall: local empty, using ${hubHits.length} hub hit(s)`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1275
1859
|
if (result.hits.length === 0) {
|
|
1276
|
-
ctx.log.debug("auto-recall: no candidates found");
|
|
1860
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1277
1861
|
const dur = performance.now() - recallT0;
|
|
1278
1862
|
store.recordToolCall("memory_search", dur, true);
|
|
1279
1863
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
|
|
1864
|
+
|
|
1865
|
+
// Even without memory hits, try skill recall
|
|
1866
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1867
|
+
if (skillAutoRecallEarly) {
|
|
1868
|
+
try {
|
|
1869
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1870
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1871
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1872
|
+
if (topSkills.length > 0) {
|
|
1873
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1874
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1875
|
+
let badge = "";
|
|
1876
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1877
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1878
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1879
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1880
|
+
});
|
|
1881
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1882
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1883
|
+
skillLines.join("\n\n") +
|
|
1884
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1885
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1886
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1887
|
+
return { prependContext: skillContext };
|
|
1888
|
+
}
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1280
1894
|
if (query.length > 50) {
|
|
1281
1895
|
const noRecallHint =
|
|
1282
1896
|
"## Memory system — ACTION REQUIRED\n\n" +
|
|
@@ -1305,22 +1919,36 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1305
1919
|
const indexSet = new Set(filterResult.relevant);
|
|
1306
1920
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
1307
1921
|
} else {
|
|
1308
|
-
ctx.log.debug("auto-recall: LLM filter returned no relevant hits");
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1922
|
+
ctx.log.debug("auto-recall: LLM filter returned no relevant local hits, trying hub fallback");
|
|
1923
|
+
const hubHits = await hubFallback();
|
|
1924
|
+
if (hubHits.length > 0) {
|
|
1925
|
+
ctx.log.debug(`auto-recall: hub fallback provided ${hubHits.length} hit(s) after local filter yielded 0`);
|
|
1926
|
+
filteredHits = hubHits;
|
|
1927
|
+
} else {
|
|
1928
|
+
const dur = performance.now() - recallT0;
|
|
1929
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1930
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1931
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
1932
|
+
filtered: []
|
|
1933
|
+
}), dur, true);
|
|
1934
|
+
if (query.length > 50) {
|
|
1935
|
+
const noRecallHint =
|
|
1936
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1937
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
1938
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1939
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1940
|
+
return { prependContext: noRecallHint };
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1322
1943
|
}
|
|
1323
|
-
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (!sufficient && filteredHits.length > 0 && ctx.config?.sharing?.enabled) {
|
|
1948
|
+
const hubSupp = await hubFallback();
|
|
1949
|
+
if (hubSupp.length > 0) {
|
|
1950
|
+
ctx.log.debug(`auto-recall: local insufficient, supplementing with ${hubSupp.length} hub hit(s)`);
|
|
1951
|
+
filteredHits.push(...hubSupp);
|
|
1324
1952
|
}
|
|
1325
1953
|
}
|
|
1326
1954
|
|
|
@@ -1330,7 +1958,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1330
1958
|
|
|
1331
1959
|
const lines = filteredHits.map((h, i) => {
|
|
1332
1960
|
const excerpt = h.original_excerpt;
|
|
1333
|
-
const
|
|
1961
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1962
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1334
1963
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
1335
1964
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1336
1965
|
if (h.taskId) {
|
|
@@ -1365,17 +1994,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1365
1994
|
lines.join("\n\n"),
|
|
1366
1995
|
];
|
|
1367
1996
|
if (tipsText) contextParts.push(tipsText);
|
|
1997
|
+
|
|
1998
|
+
// ─── Skill auto-recall ───
|
|
1999
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
2000
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
2001
|
+
let skillSection = "";
|
|
2002
|
+
|
|
2003
|
+
if (skillAutoRecall) {
|
|
2004
|
+
try {
|
|
2005
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
2006
|
+
|
|
2007
|
+
// Source 1: direct skill search based on user query
|
|
2008
|
+
try {
|
|
2009
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
2010
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
2011
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
2012
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Source 2: skills linked to tasks from memory hits
|
|
2020
|
+
const taskIds = new Set<string>();
|
|
2021
|
+
for (const h of filteredHits) {
|
|
2022
|
+
if (h.taskId) {
|
|
2023
|
+
const t = store.getTask(h.taskId);
|
|
2024
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
for (const tid of taskIds) {
|
|
2028
|
+
const linked = store.getSkillsByTask(tid);
|
|
2029
|
+
for (const rs of linked) {
|
|
2030
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
2031
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
2037
|
+
|
|
2038
|
+
if (skillCandidates.length > 0) {
|
|
2039
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
2040
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
2041
|
+
let badge = "";
|
|
2042
|
+
if (manifest?.installed) badge = " [installed]";
|
|
2043
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
2044
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
2045
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
2046
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
2047
|
+
});
|
|
2048
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
2049
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
2050
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
2051
|
+
skillLines.join("\n\n");
|
|
2052
|
+
|
|
2053
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2054
|
+
try {
|
|
2055
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2056
|
+
} catch { /* best-effort */ }
|
|
2057
|
+
} else {
|
|
2058
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2059
|
+
}
|
|
2060
|
+
} catch (err) {
|
|
2061
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (skillSection) contextParts.push(skillSection);
|
|
1368
2066
|
const context = contextParts.join("\n");
|
|
1369
2067
|
|
|
1370
2068
|
const recallDur = performance.now() - recallT0;
|
|
1371
2069
|
store.recordToolCall("memory_search", recallDur, true);
|
|
1372
2070
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1373
|
-
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1374
|
-
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt }))
|
|
2071
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2072
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
|
|
1375
2073
|
}), recallDur, true);
|
|
1376
2074
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
1377
2075
|
|
|
1378
|
-
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
|
|
2076
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
1379
2077
|
|
|
1380
2078
|
if (!sufficient) {
|
|
1381
2079
|
const searchHint =
|
|
@@ -1602,6 +2300,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1602
2300
|
|
|
1603
2301
|
// ─── Memory Viewer (web UI) ───
|
|
1604
2302
|
|
|
2303
|
+
const derivedHubPort = gatewayPort + 11;
|
|
2304
|
+
|
|
1605
2305
|
const viewer = new ViewerServer({
|
|
1606
2306
|
store,
|
|
1607
2307
|
embedder,
|
|
@@ -1609,10 +2309,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1609
2309
|
log: ctx.log,
|
|
1610
2310
|
dataDir: stateDir,
|
|
1611
2311
|
ctx,
|
|
2312
|
+
defaultHubPort: derivedHubPort,
|
|
1612
2313
|
});
|
|
1613
|
-
|
|
1614
2314
|
const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
|
|
1615
|
-
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
|
|
2315
|
+
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder, defaultHubPort: derivedHubPort })
|
|
1616
2316
|
: null;
|
|
1617
2317
|
|
|
1618
2318
|
// ─── Service lifecycle ───
|