@memtensor/memos-local-openclaw-plugin 1.0.4-beta.1 → 1.0.4-beta.11
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 +24 -24
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +34 -2
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +2 -2
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +122 -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 +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +8 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +390 -106
- 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/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 +93 -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 +82 -8
- 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 +89 -20
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +374 -124
- 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 +2671 -879
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +30 -8
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +990 -198
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +700 -56
- package/openclaw.plugin.json +1 -1
- 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 +37 -1
- package/src/client/connector.ts +124 -28
- package/src/client/hub.ts +18 -0
- package/src/client/skill-sync.ts +14 -0
- package/src/config.ts +0 -2
- package/src/hub/server.ts +374 -97
- package/src/hub/user-manager.ts +48 -8
- package/src/index.ts +10 -2
- package/src/ingest/providers/index.ts +41 -7
- package/src/recall/engine.ts +86 -1
- package/src/shared/llm-call.ts +97 -9
- 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 +395 -148
- 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 +2671 -879
- package/src/viewer/server.ts +913 -182
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
|
}
|
|
@@ -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,89 @@ 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 now = Date.now();
|
|
390
|
+
const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
391
|
+
store.upsertHubMemory({
|
|
392
|
+
id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
393
|
+
sourceChunkId: chunk.id,
|
|
394
|
+
sourceUserId: hubClient.userId,
|
|
395
|
+
role: chunk.role,
|
|
396
|
+
content: chunk.content,
|
|
397
|
+
summary: chunk.summary ?? "",
|
|
398
|
+
kind: chunk.kind,
|
|
399
|
+
groupId,
|
|
400
|
+
visibility,
|
|
401
|
+
createdAt: existing?.createdAt ?? now,
|
|
402
|
+
updatedAt: now,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
407
|
+
visibility,
|
|
408
|
+
groupId,
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const unshareMemoryFromHub = async (
|
|
413
|
+
chunkId: string,
|
|
414
|
+
input?: { hubAddress?: string; userToken?: string },
|
|
415
|
+
): Promise<void> => {
|
|
416
|
+
const chunk = store.getChunk(chunkId);
|
|
417
|
+
if (!chunk) {
|
|
418
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
419
|
+
}
|
|
420
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
421
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
422
|
+
method: "POST",
|
|
423
|
+
body: JSON.stringify({ sourceChunkId: chunk.id }),
|
|
424
|
+
});
|
|
425
|
+
store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
|
|
426
|
+
};
|
|
427
|
+
|
|
304
428
|
// ─── Tool: memory_search ───
|
|
305
429
|
|
|
306
430
|
api.registerTool(
|
|
@@ -309,24 +433,43 @@ const memosLocalPlugin = {
|
|
|
309
433
|
label: "Memory Search",
|
|
310
434
|
description:
|
|
311
435
|
"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).",
|
|
436
|
+
"Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
|
|
437
|
+
"Supports optional maxResults, minScore, and role filtering when you need tighter control.",
|
|
315
438
|
parameters: Type.Object({
|
|
316
439
|
query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
|
|
440
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
|
|
441
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
|
|
442
|
+
minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
|
|
443
|
+
role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
|
|
444
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
445
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
317
446
|
}),
|
|
318
447
|
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
319
|
-
const {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
448
|
+
const {
|
|
449
|
+
query,
|
|
450
|
+
scope: rawScope,
|
|
451
|
+
maxResults,
|
|
452
|
+
minScore: rawMinScore,
|
|
453
|
+
role: rawRole,
|
|
454
|
+
hubAddress,
|
|
455
|
+
userToken,
|
|
456
|
+
} = params as {
|
|
457
|
+
query: string;
|
|
458
|
+
scope?: string;
|
|
459
|
+
maxResults?: number;
|
|
460
|
+
minScore?: number;
|
|
461
|
+
role?: string;
|
|
462
|
+
hubAddress?: string;
|
|
463
|
+
userToken?: string;
|
|
464
|
+
};
|
|
465
|
+
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
466
|
+
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
467
|
+
const searchScope = resolveMemorySearchScope(rawScope);
|
|
468
|
+
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
326
469
|
|
|
327
470
|
const agentId = currentAgentId;
|
|
328
|
-
const ownerFilter = [
|
|
329
|
-
const effectiveMaxResults =
|
|
471
|
+
const ownerFilter = [getCurrentOwner(), "public"];
|
|
472
|
+
const effectiveMaxResults = searchLimit;
|
|
330
473
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
331
474
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
332
475
|
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
@@ -337,9 +480,10 @@ const memosLocalPlugin = {
|
|
|
337
480
|
score: h.score,
|
|
338
481
|
summary: h.summary,
|
|
339
482
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
483
|
+
origin: h.origin || "local",
|
|
340
484
|
}));
|
|
341
485
|
|
|
342
|
-
if (result.hits.length === 0) {
|
|
486
|
+
if (result.hits.length === 0 && searchScope === "local") {
|
|
343
487
|
return {
|
|
344
488
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
345
489
|
details: { candidates: [], meta: result.meta },
|
|
@@ -363,11 +507,13 @@ const memosLocalPlugin = {
|
|
|
363
507
|
const indexSet = new Set(filterResult.relevant);
|
|
364
508
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
365
509
|
ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
|
|
366
|
-
} else {
|
|
510
|
+
} else if (searchScope === "local") {
|
|
367
511
|
return {
|
|
368
512
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
369
513
|
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
370
514
|
};
|
|
515
|
+
} else {
|
|
516
|
+
filteredHits = [];
|
|
371
517
|
}
|
|
372
518
|
}
|
|
373
519
|
|
|
@@ -389,29 +535,79 @@ const memosLocalPlugin = {
|
|
|
389
535
|
role: h.source.role,
|
|
390
536
|
score: h.score,
|
|
391
537
|
summary: h.summary,
|
|
538
|
+
origin: h.origin || "local",
|
|
392
539
|
};
|
|
393
540
|
});
|
|
394
541
|
|
|
395
542
|
if (searchScope !== "local") {
|
|
396
543
|
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
544
|
+
|
|
545
|
+
let filteredHubHits = hub.hits;
|
|
546
|
+
if (hub.hits.length > 0) {
|
|
547
|
+
const hubCandidates = hub.hits.map((h, i) => ({
|
|
548
|
+
index: filteredHits.length + i + 1,
|
|
549
|
+
role: (h.source?.role || "assistant") as string,
|
|
550
|
+
content: (h.summary || h.excerpt || "").slice(0, 300),
|
|
551
|
+
time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
552
|
+
}));
|
|
553
|
+
const localCandidatesForMerge = filteredHits.map((h, i) => ({
|
|
554
|
+
index: i + 1,
|
|
555
|
+
role: h.source.role,
|
|
556
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
557
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
558
|
+
}));
|
|
559
|
+
const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
|
|
560
|
+
const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
|
|
561
|
+
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
562
|
+
const relevantSet = new Set(mergedFilter.relevant);
|
|
563
|
+
const hubStartIdx = filteredHits.length + 1;
|
|
564
|
+
filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
|
|
565
|
+
filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
|
|
566
|
+
ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const originLabel = (h: SearchHit) => {
|
|
571
|
+
if (h.origin === "hub-memory") return " [团队缓存]";
|
|
572
|
+
if (h.origin === "local-shared") return " [本机共享]";
|
|
573
|
+
return "";
|
|
574
|
+
};
|
|
397
575
|
const localText = filteredHits.length > 0
|
|
398
576
|
? filteredHits.map((h, i) => {
|
|
399
577
|
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
400
|
-
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
578
|
+
return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
|
|
401
579
|
}).join("\n")
|
|
402
580
|
: "(none)";
|
|
403
|
-
const hubText =
|
|
404
|
-
?
|
|
581
|
+
const hubText = filteredHubHits.length > 0
|
|
582
|
+
? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
405
583
|
: "(none)";
|
|
406
584
|
|
|
585
|
+
const localDetailsFiltered = filteredHits.map((h) => {
|
|
586
|
+
let effectiveTaskId = h.taskId;
|
|
587
|
+
if (effectiveTaskId) {
|
|
588
|
+
const t = store.getTask(effectiveTaskId);
|
|
589
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
ref: h.ref,
|
|
593
|
+
chunkId: h.ref.chunkId,
|
|
594
|
+
taskId: effectiveTaskId,
|
|
595
|
+
skillId: h.skillId,
|
|
596
|
+
role: h.source.role,
|
|
597
|
+
score: h.score,
|
|
598
|
+
summary: h.summary,
|
|
599
|
+
origin: h.origin,
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
|
|
407
603
|
return {
|
|
408
604
|
content: [{
|
|
409
605
|
type: "text",
|
|
410
606
|
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
411
607
|
}],
|
|
412
608
|
details: {
|
|
413
|
-
local: { hits:
|
|
414
|
-
hub,
|
|
609
|
+
local: { hits: localDetailsFiltered, meta: result.meta },
|
|
610
|
+
hub: { ...hub, hits: filteredHubHits },
|
|
415
611
|
},
|
|
416
612
|
};
|
|
417
613
|
}
|
|
@@ -423,9 +619,15 @@ const memosLocalPlugin = {
|
|
|
423
619
|
};
|
|
424
620
|
}
|
|
425
621
|
|
|
622
|
+
const originTag = (o?: string) => {
|
|
623
|
+
if (o === "local-shared") return " [本机共享]";
|
|
624
|
+
if (o === "hub-memory") return " [团队缓存]";
|
|
625
|
+
if (o === "hub-remote") return " [团队]";
|
|
626
|
+
return "";
|
|
627
|
+
};
|
|
426
628
|
const lines = filteredHits.map((h, i) => {
|
|
427
629
|
const excerpt = h.original_excerpt;
|
|
428
|
-
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
630
|
+
const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)}`];
|
|
429
631
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
430
632
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
431
633
|
if (h.taskId) {
|
|
@@ -480,6 +682,7 @@ const memosLocalPlugin = {
|
|
|
480
682
|
score: h.score,
|
|
481
683
|
summary: h.summary,
|
|
482
684
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
685
|
+
origin: h.origin || "local",
|
|
483
686
|
};
|
|
484
687
|
}),
|
|
485
688
|
meta: result.meta,
|
|
@@ -843,7 +1046,9 @@ ${detail.content}`,
|
|
|
843
1046
|
{
|
|
844
1047
|
name: "network_team_info",
|
|
845
1048
|
label: "Network Team Info",
|
|
846
|
-
description:
|
|
1049
|
+
description:
|
|
1050
|
+
"Show current Hub connection status, signed-in user, role, and group memberships. " +
|
|
1051
|
+
"Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
|
|
847
1052
|
parameters: Type.Object({}),
|
|
848
1053
|
execute: trackTool("network_team_info", async () => {
|
|
849
1054
|
const status = await getHubStatus(store, ctx.config);
|
|
@@ -927,10 +1132,31 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
927
1132
|
};
|
|
928
1133
|
}
|
|
929
1134
|
|
|
1135
|
+
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1136
|
+
let footer = "\n\n---\n";
|
|
1137
|
+
|
|
1138
|
+
if (manifest && manifest.hasCompanionFiles) {
|
|
1139
|
+
const fileSummary = manifest.files
|
|
1140
|
+
.filter(f => f.type !== "eval")
|
|
1141
|
+
.map(f => `\`${f.relativePath}\``)
|
|
1142
|
+
.join(", ");
|
|
1143
|
+
footer += `**Companion files available:** ${fileSummary}\n`;
|
|
1144
|
+
footer += `→ call \`skill_files(skillId="${resolvedSkillId}")\` to list all files\n`;
|
|
1145
|
+
footer += `→ call \`skill_file_get(skillId="${resolvedSkillId}", path="...")\` to read a specific file\n`;
|
|
1146
|
+
if (manifest.installMode === "install_recommended") {
|
|
1147
|
+
footer += `→ **Recommended:** call \`skill_install(skillId="${resolvedSkillId}")\` for persistent workspace access (many/large files)\n`;
|
|
1148
|
+
}
|
|
1149
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1150
|
+
footer += `> Already installed at: ${manifest.installedPath}/\n`;
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
footer += `To install this skill for persistent use: call skill_install(skillId="${resolvedSkillId}")`;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
930
1156
|
return {
|
|
931
1157
|
content: [{
|
|
932
1158
|
type: "text",
|
|
933
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}
|
|
1159
|
+
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
934
1160
|
}],
|
|
935
1161
|
details: {
|
|
936
1162
|
skillId: skill.id,
|
|
@@ -938,6 +1164,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
938
1164
|
version: skill.version,
|
|
939
1165
|
status: skill.status,
|
|
940
1166
|
installed: skill.installed,
|
|
1167
|
+
companionFiles: manifest?.hasCompanionFiles ?? false,
|
|
1168
|
+
installMode: manifest?.installMode ?? "inline",
|
|
941
1169
|
},
|
|
942
1170
|
};
|
|
943
1171
|
}),
|
|
@@ -973,6 +1201,112 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
973
1201
|
{ name: "skill_install" },
|
|
974
1202
|
);
|
|
975
1203
|
|
|
1204
|
+
// ─── Tool: skill_files ───
|
|
1205
|
+
|
|
1206
|
+
api.registerTool(
|
|
1207
|
+
{
|
|
1208
|
+
name: "skill_files",
|
|
1209
|
+
label: "List Skill Companion Files",
|
|
1210
|
+
description:
|
|
1211
|
+
"List companion files (scripts, references, evals) for a skill. " +
|
|
1212
|
+
"Use this after skill_get to see what additional files are available. " +
|
|
1213
|
+
"Returns file names, sizes, and whether the skill recommends installation.",
|
|
1214
|
+
parameters: Type.Object({
|
|
1215
|
+
skillId: Type.String({ description: "The skill_id to inspect" }),
|
|
1216
|
+
}),
|
|
1217
|
+
execute: trackTool("skill_files", async (_toolCallId: any, params: any) => {
|
|
1218
|
+
const { skillId } = params as { skillId: string };
|
|
1219
|
+
ctx.log.debug(`skill_files called for skill=${skillId}`);
|
|
1220
|
+
|
|
1221
|
+
const manifest = skillInstaller.getCompanionManifest(skillId);
|
|
1222
|
+
if (!manifest) {
|
|
1223
|
+
return {
|
|
1224
|
+
content: [{ type: "text", text: `Skill not found: ${skillId}` }],
|
|
1225
|
+
details: { error: "not_found" },
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (!manifest.hasCompanionFiles) {
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: "text", text: "This skill has no companion files (scripts, references). The SKILL.md from skill_get contains everything." }],
|
|
1232
|
+
details: manifest,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const lines: string[] = [`## Companion Files (${manifest.files.length} files, ${Math.round(manifest.totalSize / 1024)}KB total)\n`];
|
|
1237
|
+
if (manifest.scriptsCount > 0) {
|
|
1238
|
+
lines.push(`### Scripts (${manifest.scriptsCount})`);
|
|
1239
|
+
for (const f of manifest.files.filter(f => f.type === "script")) {
|
|
1240
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (manifest.referencesCount > 0) {
|
|
1244
|
+
lines.push(`\n### References (${manifest.referencesCount})`);
|
|
1245
|
+
for (const f of manifest.files.filter(f => f.type === "reference")) {
|
|
1246
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes) → call \`skill_file_get(skillId="${skillId}", path="${f.relativePath}")\``);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (manifest.evalsCount > 0) {
|
|
1250
|
+
lines.push(`\n### Evals (${manifest.evalsCount})`);
|
|
1251
|
+
for (const f of manifest.files.filter(f => f.type === "eval")) {
|
|
1252
|
+
lines.push(`- \`${f.relativePath}\` (${f.size} bytes)`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (manifest.installMode === "install_recommended") {
|
|
1257
|
+
lines.push(`\n> **Recommendation:** This skill has many/large companion files. Consider \`skill_install(skillId="${skillId}")\` for persistent workspace access.`);
|
|
1258
|
+
}
|
|
1259
|
+
if (manifest.installed && manifest.installedPath) {
|
|
1260
|
+
lines.push(`\n> **Installed at:** ${manifest.installedPath}/`);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return {
|
|
1264
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1265
|
+
details: manifest,
|
|
1266
|
+
};
|
|
1267
|
+
}),
|
|
1268
|
+
},
|
|
1269
|
+
{ name: "skill_files" },
|
|
1270
|
+
);
|
|
1271
|
+
|
|
1272
|
+
// ─── Tool: skill_file_get ───
|
|
1273
|
+
|
|
1274
|
+
api.registerTool(
|
|
1275
|
+
{
|
|
1276
|
+
name: "skill_file_get",
|
|
1277
|
+
label: "Get Skill Companion File",
|
|
1278
|
+
description:
|
|
1279
|
+
"Read the content of a specific companion file (script, reference) from a skill. " +
|
|
1280
|
+
"Use after skill_files to retrieve a script or reference document. " +
|
|
1281
|
+
"Pass the relative path like 'scripts/deploy.sh' or 'references/api-notes.md'.",
|
|
1282
|
+
parameters: Type.Object({
|
|
1283
|
+
skillId: Type.String({ description: "The skill_id" }),
|
|
1284
|
+
path: Type.String({ description: "Relative path within the skill, e.g. 'scripts/deploy.sh'" }),
|
|
1285
|
+
}),
|
|
1286
|
+
execute: trackTool("skill_file_get", async (_toolCallId: any, params: any) => {
|
|
1287
|
+
const { skillId, path: filePath } = params as { skillId: string; path: string };
|
|
1288
|
+
ctx.log.debug(`skill_file_get called for skill=${skillId} path=${filePath}`);
|
|
1289
|
+
|
|
1290
|
+
const result = skillInstaller.readCompanionFile(skillId, filePath);
|
|
1291
|
+
if ("error" in result) {
|
|
1292
|
+
return {
|
|
1293
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
1294
|
+
details: result,
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const ext = filePath.split(".").pop() || "";
|
|
1299
|
+
const lang = { sh: "bash", py: "python", ts: "typescript", js: "javascript", json: "json", md: "markdown", yml: "yaml", yaml: "yaml" }[ext] || "";
|
|
1300
|
+
|
|
1301
|
+
return {
|
|
1302
|
+
content: [{ type: "text", text: `## ${filePath}\n\n\`\`\`${lang}\n${result.content}\n\`\`\`` }],
|
|
1303
|
+
details: { path: filePath, size: result.size },
|
|
1304
|
+
};
|
|
1305
|
+
}),
|
|
1306
|
+
},
|
|
1307
|
+
{ name: "skill_file_get" },
|
|
1308
|
+
);
|
|
1309
|
+
|
|
976
1310
|
// ─── Tool: memory_viewer ───
|
|
977
1311
|
|
|
978
1312
|
const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
|
|
@@ -988,6 +1322,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
988
1322
|
parameters: Type.Object({}),
|
|
989
1323
|
execute: trackTool("memory_viewer", async () => {
|
|
990
1324
|
ctx.log.debug(`memory_viewer called`);
|
|
1325
|
+
telemetry.trackViewerOpened();
|
|
991
1326
|
const url = `http://127.0.0.1:${viewerPort}`;
|
|
992
1327
|
return {
|
|
993
1328
|
content: [
|
|
@@ -1018,12 +1353,13 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1018
1353
|
api.registerTool(
|
|
1019
1354
|
{
|
|
1020
1355
|
name: "memory_write_public",
|
|
1021
|
-
label: "Write
|
|
1356
|
+
label: "Write Local Shared Memory",
|
|
1022
1357
|
description:
|
|
1023
|
-
"Write a piece of information to
|
|
1024
|
-
"Use this
|
|
1358
|
+
"Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
|
|
1359
|
+
"Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
|
|
1360
|
+
"If you already have a memory chunk and want to expose it, use memory_share instead.",
|
|
1025
1361
|
parameters: Type.Object({
|
|
1026
|
-
content: Type.String({ description: "The content to write to
|
|
1362
|
+
content: Type.String({ description: "The content to write to local shared memory" }),
|
|
1027
1363
|
summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
|
|
1028
1364
|
}),
|
|
1029
1365
|
execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
|
|
@@ -1068,7 +1404,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1068
1404
|
}
|
|
1069
1405
|
|
|
1070
1406
|
return {
|
|
1071
|
-
content: [{ type: "text", text: `
|
|
1407
|
+
content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
|
|
1072
1408
|
details: { chunkId, owner: "public" },
|
|
1073
1409
|
};
|
|
1074
1410
|
}),
|
|
@@ -1076,6 +1412,164 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1076
1412
|
{ name: "memory_write_public" },
|
|
1077
1413
|
);
|
|
1078
1414
|
|
|
1415
|
+
api.registerTool(
|
|
1416
|
+
{
|
|
1417
|
+
name: "memory_share",
|
|
1418
|
+
label: "Share Memory",
|
|
1419
|
+
description:
|
|
1420
|
+
"Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
|
|
1421
|
+
"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. " +
|
|
1422
|
+
"If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
|
|
1423
|
+
parameters: Type.Object({
|
|
1424
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1425
|
+
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
1426
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
|
|
1427
|
+
groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
|
|
1428
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1429
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1430
|
+
}),
|
|
1431
|
+
execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
|
|
1432
|
+
const {
|
|
1433
|
+
chunkId,
|
|
1434
|
+
target: rawTarget,
|
|
1435
|
+
visibility: rawVisibility,
|
|
1436
|
+
groupId,
|
|
1437
|
+
hubAddress,
|
|
1438
|
+
userToken,
|
|
1439
|
+
} = params as {
|
|
1440
|
+
chunkId: string;
|
|
1441
|
+
target?: string;
|
|
1442
|
+
visibility?: string;
|
|
1443
|
+
groupId?: string;
|
|
1444
|
+
hubAddress?: string;
|
|
1445
|
+
userToken?: string;
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const chunk = store.getChunk(chunkId);
|
|
1449
|
+
if (!chunk) {
|
|
1450
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const target = resolveMemoryShareTarget(rawTarget);
|
|
1454
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
1455
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1456
|
+
const messages: string[] = [];
|
|
1457
|
+
|
|
1458
|
+
if (target === "agents" || target === "both") {
|
|
1459
|
+
const local = store.markMemorySharedLocally(chunkId);
|
|
1460
|
+
if (!local.ok) {
|
|
1461
|
+
return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
|
|
1462
|
+
}
|
|
1463
|
+
details.local = {
|
|
1464
|
+
shared: true,
|
|
1465
|
+
owner: local.owner,
|
|
1466
|
+
originalOwner: local.originalOwner ?? null,
|
|
1467
|
+
};
|
|
1468
|
+
messages.push("shared to local agents");
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (target === "hub" || target === "both") {
|
|
1472
|
+
const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
|
|
1473
|
+
details.hub = {
|
|
1474
|
+
shared: true,
|
|
1475
|
+
memoryId: hub.memoryId,
|
|
1476
|
+
visibility: hub.visibility,
|
|
1477
|
+
groupId: hub.groupId,
|
|
1478
|
+
};
|
|
1479
|
+
messages.push(`shared to Hub (${hub.visibility})`);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return {
|
|
1483
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1484
|
+
details,
|
|
1485
|
+
};
|
|
1486
|
+
}),
|
|
1487
|
+
},
|
|
1488
|
+
{ name: "memory_share" },
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
api.registerTool(
|
|
1492
|
+
{
|
|
1493
|
+
name: "memory_unshare",
|
|
1494
|
+
label: "Unshare Memory",
|
|
1495
|
+
description:
|
|
1496
|
+
"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. " +
|
|
1497
|
+
"privateOwner is only needed for older public memories that were never tracked with an original owner.",
|
|
1498
|
+
parameters: Type.Object({
|
|
1499
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
|
|
1500
|
+
target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
|
|
1501
|
+
privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
|
|
1502
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1503
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1504
|
+
}),
|
|
1505
|
+
execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
|
|
1506
|
+
const {
|
|
1507
|
+
chunkId,
|
|
1508
|
+
target: rawTarget,
|
|
1509
|
+
privateOwner,
|
|
1510
|
+
hubAddress,
|
|
1511
|
+
userToken,
|
|
1512
|
+
} = params as {
|
|
1513
|
+
chunkId: string;
|
|
1514
|
+
target?: string;
|
|
1515
|
+
privateOwner?: string;
|
|
1516
|
+
hubAddress?: string;
|
|
1517
|
+
userToken?: string;
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const chunk = store.getChunk(chunkId);
|
|
1521
|
+
if (!chunk) {
|
|
1522
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const target = resolveMemoryUnshareTarget(rawTarget);
|
|
1526
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1527
|
+
const messages: string[] = [];
|
|
1528
|
+
|
|
1529
|
+
if (target === "agents" || target === "all") {
|
|
1530
|
+
const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1531
|
+
if (!local.ok) {
|
|
1532
|
+
return {
|
|
1533
|
+
content: [{
|
|
1534
|
+
type: "text",
|
|
1535
|
+
text: local.reason === "original_owner_missing"
|
|
1536
|
+
? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
|
|
1537
|
+
: `Failed to stop local sharing for memory ${chunkId}.`,
|
|
1538
|
+
}],
|
|
1539
|
+
details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
details.local = {
|
|
1543
|
+
shared: false,
|
|
1544
|
+
owner: local.owner,
|
|
1545
|
+
};
|
|
1546
|
+
messages.push("removed from local agent sharing");
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (target === "hub" || target === "all") {
|
|
1550
|
+
try {
|
|
1551
|
+
await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
|
|
1552
|
+
details.hub = { shared: false };
|
|
1553
|
+
messages.push("removed from Hub");
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1556
|
+
if (target === "all" && msg.includes("hub client connection is not configured")) {
|
|
1557
|
+
details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
|
|
1558
|
+
} else {
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
return {
|
|
1565
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1566
|
+
details,
|
|
1567
|
+
};
|
|
1568
|
+
}),
|
|
1569
|
+
},
|
|
1570
|
+
{ name: "memory_unshare" },
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1079
1573
|
// ─── Tool: skill_search ───
|
|
1080
1574
|
|
|
1081
1575
|
api.registerTool(
|
|
@@ -1083,16 +1577,16 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1083
1577
|
name: "skill_search",
|
|
1084
1578
|
label: "Skill Search",
|
|
1085
1579
|
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.",
|
|
1580
|
+
"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. " +
|
|
1581
|
+
"Use this when you need a capability or guide and don't have a matching skill at hand.",
|
|
1088
1582
|
parameters: Type.Object({
|
|
1089
1583
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1090
|
-
scope: Type.Optional(Type.String({ description: "Search scope: 'mix'
|
|
1584
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1091
1585
|
}),
|
|
1092
1586
|
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
1093
1587
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1094
1588
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1095
|
-
const currentOwner =
|
|
1589
|
+
const currentOwner = getCurrentOwner();
|
|
1096
1590
|
|
|
1097
1591
|
if (rawScope === "group" || rawScope === "all") {
|
|
1098
1592
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1108,7 +1602,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1108
1602
|
}
|
|
1109
1603
|
|
|
1110
1604
|
const localText = localHits.length > 0
|
|
1111
|
-
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (
|
|
1605
|
+
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
|
|
1112
1606
|
: "(none)";
|
|
1113
1607
|
const hubText = hub.hits.length > 0
|
|
1114
1608
|
? 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 +1624,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1130
1624
|
}
|
|
1131
1625
|
|
|
1132
1626
|
const text = hits.map((h, i) =>
|
|
1133
|
-
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (
|
|
1627
|
+
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
|
|
1134
1628
|
).join("\n");
|
|
1135
1629
|
|
|
1136
1630
|
return {
|
|
@@ -1148,31 +1642,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1148
1642
|
{
|
|
1149
1643
|
name: "skill_publish",
|
|
1150
1644
|
label: "Publish Skill",
|
|
1151
|
-
description:
|
|
1645
|
+
description:
|
|
1646
|
+
"Share a skill with local agents or publish it to the Hub. " +
|
|
1647
|
+
"Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
|
|
1648
|
+
"The old scope parameter is still accepted for backward compatibility.",
|
|
1152
1649
|
parameters: Type.Object({
|
|
1153
1650
|
skillId: Type.String({ description: "The skill ID to publish" }),
|
|
1154
|
-
|
|
1651
|
+
target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
|
|
1652
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
|
|
1653
|
+
scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
|
|
1155
1654
|
groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
|
|
1156
1655
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1157
1656
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1158
1657
|
}),
|
|
1159
1658
|
execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
|
|
1160
|
-
const {
|
|
1659
|
+
const {
|
|
1660
|
+
skillId: pubSkillId,
|
|
1661
|
+
target: rawTarget,
|
|
1662
|
+
visibility: rawVisibility,
|
|
1663
|
+
scope,
|
|
1664
|
+
groupId,
|
|
1665
|
+
hubAddress,
|
|
1666
|
+
userToken,
|
|
1667
|
+
} = params as {
|
|
1668
|
+
skillId: string;
|
|
1669
|
+
target?: string;
|
|
1670
|
+
visibility?: string;
|
|
1671
|
+
scope?: string;
|
|
1672
|
+
groupId?: string;
|
|
1673
|
+
hubAddress?: string;
|
|
1674
|
+
userToken?: string;
|
|
1675
|
+
};
|
|
1161
1676
|
const skill = store.getSkill(pubSkillId);
|
|
1162
1677
|
if (!skill) {
|
|
1163
1678
|
return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
|
|
1164
1679
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1680
|
+
const target = resolveSkillPublishTarget(rawTarget, scope);
|
|
1681
|
+
const visibility = resolveSkillHubVisibility(rawVisibility, scope);
|
|
1682
|
+
if (target === "hub") {
|
|
1683
|
+
const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
|
|
1167
1684
|
return {
|
|
1168
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
1169
|
-
details: { skillId: pubSkillId, name: skill.name, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1685
|
+
content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
|
|
1686
|
+
details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1170
1687
|
};
|
|
1171
1688
|
}
|
|
1172
1689
|
store.setSkillVisibility(pubSkillId, "public");
|
|
1173
1690
|
return {
|
|
1174
|
-
content: [{ type: "text", text: `Skill "${skill.name}" is now
|
|
1175
|
-
details: { skillId: pubSkillId, name: skill.name, visibility: "public", publishedToHub: false },
|
|
1691
|
+
content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
|
|
1692
|
+
details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
|
|
1176
1693
|
};
|
|
1177
1694
|
}),
|
|
1178
1695
|
},
|
|
@@ -1185,20 +1702,46 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1185
1702
|
{
|
|
1186
1703
|
name: "skill_unpublish",
|
|
1187
1704
|
label: "Unpublish Skill",
|
|
1188
|
-
description:
|
|
1705
|
+
description:
|
|
1706
|
+
"Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
|
|
1707
|
+
"Use target='agents' (default), 'hub', or 'all'.",
|
|
1189
1708
|
parameters: Type.Object({
|
|
1190
1709
|
skillId: Type.String({ description: "The skill ID to unpublish" }),
|
|
1710
|
+
target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
|
|
1711
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1712
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1191
1713
|
}),
|
|
1192
1714
|
execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
|
|
1193
|
-
const { skillId: unpubSkillId } = params as { skillId: string };
|
|
1715
|
+
const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
|
|
1194
1716
|
const skill = store.getSkill(unpubSkillId);
|
|
1195
1717
|
if (!skill) {
|
|
1196
1718
|
return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
|
|
1197
1719
|
}
|
|
1198
|
-
|
|
1720
|
+
const resolvedTarget = resolveSkillUnpublishTarget(target);
|
|
1721
|
+
const messages: string[] = [];
|
|
1722
|
+
const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
|
|
1723
|
+
if (resolvedTarget === "hub" || resolvedTarget === "all") {
|
|
1724
|
+
try {
|
|
1725
|
+
await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
|
|
1726
|
+
details.hub = { unpublished: true };
|
|
1727
|
+
messages.push("removed from Hub sharing");
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1730
|
+
if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
|
|
1731
|
+
details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
|
|
1732
|
+
} else {
|
|
1733
|
+
throw err;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (resolvedTarget === "agents" || resolvedTarget === "all") {
|
|
1738
|
+
store.setSkillVisibility(unpubSkillId, "private");
|
|
1739
|
+
details.local = { visibility: "private" };
|
|
1740
|
+
messages.push("limited to this agent");
|
|
1741
|
+
}
|
|
1199
1742
|
return {
|
|
1200
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
1201
|
-
details
|
|
1743
|
+
content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
|
|
1744
|
+
details,
|
|
1202
1745
|
};
|
|
1203
1746
|
}),
|
|
1204
1747
|
},
|
|
@@ -1231,6 +1774,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1231
1774
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1232
1775
|
|
|
1233
1776
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1777
|
+
if (!allowPromptInjection) return {};
|
|
1234
1778
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1235
1779
|
|
|
1236
1780
|
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
@@ -1273,10 +1817,40 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1273
1817
|
|
|
1274
1818
|
const result = await engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1275
1819
|
if (result.hits.length === 0) {
|
|
1276
|
-
ctx.log.debug("auto-recall: no candidates found");
|
|
1820
|
+
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1277
1821
|
const dur = performance.now() - recallT0;
|
|
1278
1822
|
store.recordToolCall("memory_search", dur, true);
|
|
1279
1823
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ candidates: [], filtered: [] }), dur, true);
|
|
1824
|
+
|
|
1825
|
+
// Even without memory hits, try skill recall
|
|
1826
|
+
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1827
|
+
if (skillAutoRecallEarly) {
|
|
1828
|
+
try {
|
|
1829
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1830
|
+
const skillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1831
|
+
const topSkills = skillHits.slice(0, skillLimit);
|
|
1832
|
+
if (topSkills.length > 0) {
|
|
1833
|
+
const skillLines = topSkills.map((sc, i) => {
|
|
1834
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1835
|
+
let badge = "";
|
|
1836
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1837
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1838
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1839
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → call \`skill_get(skillId="${sc.skillId}")\` for the full guide`;
|
|
1840
|
+
});
|
|
1841
|
+
const skillContext = "## Relevant skills from past experience\n\n" +
|
|
1842
|
+
"No direct memory matches were found, but these skills from past tasks may help:\n\n" +
|
|
1843
|
+
skillLines.join("\n\n") +
|
|
1844
|
+
"\n\nYou SHOULD call `skill_get` to retrieve the full guide before attempting the task.";
|
|
1845
|
+
ctx.log.info(`auto-recall-skill (no-memory path): injecting ${topSkills.length} skill(s)`);
|
|
1846
|
+
try { store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(topSkills), dur, true); } catch { /* best-effort */ }
|
|
1847
|
+
return { prependContext: skillContext };
|
|
1848
|
+
}
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
ctx.log.debug(`auto-recall-skill (no-memory path): failed: ${err}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1280
1854
|
if (query.length > 50) {
|
|
1281
1855
|
const noRecallHint =
|
|
1282
1856
|
"## Memory system — ACTION REQUIRED\n\n" +
|
|
@@ -1309,7 +1883,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1309
1883
|
const dur = performance.now() - recallT0;
|
|
1310
1884
|
store.recordToolCall("memory_search", dur, true);
|
|
1311
1885
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1312
|
-
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt })),
|
|
1886
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
1313
1887
|
filtered: []
|
|
1314
1888
|
}), dur, true);
|
|
1315
1889
|
if (query.length > 50) {
|
|
@@ -1330,7 +1904,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1330
1904
|
|
|
1331
1905
|
const lines = filteredHits.map((h, i) => {
|
|
1332
1906
|
const excerpt = h.original_excerpt;
|
|
1333
|
-
const
|
|
1907
|
+
const oTag = h.origin === "local-shared" ? " [本机共享]" : h.origin === "hub-memory" ? " [团队缓存]" : "";
|
|
1908
|
+
const parts: string[] = [`${i + 1}. [${h.source.role}]${oTag}`];
|
|
1334
1909
|
if (excerpt) parts.push(` ${excerpt}`);
|
|
1335
1910
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
1336
1911
|
if (h.taskId) {
|
|
@@ -1365,17 +1940,86 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1365
1940
|
lines.join("\n\n"),
|
|
1366
1941
|
];
|
|
1367
1942
|
if (tipsText) contextParts.push(tipsText);
|
|
1943
|
+
|
|
1944
|
+
// ─── Skill auto-recall ───
|
|
1945
|
+
const skillAutoRecall = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1946
|
+
const skillLimit = ctx.config.skillEvolution?.autoRecallSkillLimit ?? DEFAULTS.skillAutoRecallLimit;
|
|
1947
|
+
let skillSection = "";
|
|
1948
|
+
|
|
1949
|
+
if (skillAutoRecall) {
|
|
1950
|
+
try {
|
|
1951
|
+
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
1952
|
+
|
|
1953
|
+
// Source 1: direct skill search based on user query
|
|
1954
|
+
try {
|
|
1955
|
+
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1956
|
+
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
1957
|
+
if (!skillCandidateMap.has(sh.skillId)) {
|
|
1958
|
+
skillCandidateMap.set(sh.skillId, { name: sh.name, description: sh.description, skillId: sh.skillId, source: "query" });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Source 2: skills linked to tasks from memory hits
|
|
1966
|
+
const taskIds = new Set<string>();
|
|
1967
|
+
for (const h of filteredHits) {
|
|
1968
|
+
if (h.taskId) {
|
|
1969
|
+
const t = store.getTask(h.taskId);
|
|
1970
|
+
if (t && t.status !== "skipped") taskIds.add(h.taskId);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
for (const tid of taskIds) {
|
|
1974
|
+
const linked = store.getSkillsByTask(tid);
|
|
1975
|
+
for (const rs of linked) {
|
|
1976
|
+
if (!skillCandidateMap.has(rs.skill.id)) {
|
|
1977
|
+
skillCandidateMap.set(rs.skill.id, { name: rs.skill.name, description: rs.skill.description, skillId: rs.skill.id, source: `task:${tid}` });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const skillCandidates = [...skillCandidateMap.values()].slice(0, skillLimit);
|
|
1983
|
+
|
|
1984
|
+
if (skillCandidates.length > 0) {
|
|
1985
|
+
const skillLines = skillCandidates.map((sc, i) => {
|
|
1986
|
+
const manifest = skillInstaller.getCompanionManifest(sc.skillId);
|
|
1987
|
+
let badge = "";
|
|
1988
|
+
if (manifest?.installed) badge = " [installed]";
|
|
1989
|
+
else if (manifest?.installMode === "install_recommended") badge = " [has scripts, install recommended]";
|
|
1990
|
+
else if (manifest?.hasCompanionFiles) badge = " [has companion files]";
|
|
1991
|
+
const action = `call \`skill_get(skillId="${sc.skillId}")\``;
|
|
1992
|
+
return `${i + 1}. **${sc.name}**${badge} — ${sc.description.slice(0, 200)}\n → ${action}`;
|
|
1993
|
+
});
|
|
1994
|
+
skillSection = "\n\n## Relevant skills from past experience\n\n" +
|
|
1995
|
+
"The following skills were distilled from similar previous tasks. " +
|
|
1996
|
+
"You SHOULD call `skill_get` to retrieve the full guide before attempting the task.\n\n" +
|
|
1997
|
+
skillLines.join("\n\n");
|
|
1998
|
+
|
|
1999
|
+
ctx.log.info(`auto-recall-skill: injecting ${skillCandidates.length} skill(s): ${skillCandidates.map(s => s.name).join(", ")}`);
|
|
2000
|
+
try {
|
|
2001
|
+
store.recordApiLog("skill_search", { type: "auto_recall_skill", query }, JSON.stringify(skillCandidates), performance.now() - recallT0, true);
|
|
2002
|
+
} catch { /* best-effort */ }
|
|
2003
|
+
} else {
|
|
2004
|
+
ctx.log.debug("auto-recall-skill: no matching skills found");
|
|
2005
|
+
}
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
ctx.log.debug(`auto-recall-skill: failed: ${err}`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (skillSection) contextParts.push(skillSection);
|
|
1368
2012
|
const context = contextParts.join("\n");
|
|
1369
2013
|
|
|
1370
2014
|
const recallDur = performance.now() - recallT0;
|
|
1371
2015
|
store.recordToolCall("memory_search", recallDur, true);
|
|
1372
2016
|
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 }))
|
|
2017
|
+
candidates: result.hits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2018
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" }))
|
|
1375
2019
|
}), recallDur, true);
|
|
1376
2020
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
1377
2021
|
|
|
1378
|
-
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}`);
|
|
2022
|
+
ctx.log.info(`auto-recall: returning prependContext (${context.length} chars), sufficient=${sufficient}, skills=${skillSection ? "yes" : "no"}`);
|
|
1379
2023
|
|
|
1380
2024
|
if (!sufficient) {
|
|
1381
2025
|
const searchHint =
|