Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4066,10 +4066,15 @@ const memoryLanceDBProPlugin = {
const now = new Date(params.timestampMs ?? Date.now());
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1].split(".")[0];
// Session key/id stay out of `text`: it is the FTS index surface, and
// the `simple` tokenizer splits a key like
// `agent:main:cron:<uuid>:run:<uuid>` on its punctuation — so every session
// summary ends up indexed under `agent`, `main`, `cron`, `run`. A query
// mentioning any of those then BM25-matches every session summary in the
// store regardless of content. Both ids are already recorded structurally
// in metadata below, so provenance is unaffected.
const memoryText = [
`Session: ${dateStr} ${timeStr} UTC`,
`Session Key: ${params.sessionKey}`,
`Session ID: ${params.sessionId}`,
`Source: ${params.source}`,
"",
"Conversation Summary:",
Expand Down
39 changes: 39 additions & 0 deletions dist/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,45 @@ export class MemoryStore {
await this.ensureInitialized();
return await this.table.countRows();
}
/**
* Finds rows whose id starts with `prefix`, restricted to accessible
* scopes. Backs the documented "full UUID or 8+ char prefix" contract on
* memory_forget/memory_update: injected context shows agents truncated ids,
* so a unique-prefix lookup is the only way those handles can ever resolve.
* The prefix must be hex/dash shaped (validated here, defense in depth on
* top of the tool-layer classification) and at least 8 chars, so a short
* or malformed ref can never scan-match. Capped at `limit` matches: the
* caller only distinguishes zero / one / many.
*/
async findByIdPrefix(prefix, scopeFilter, limit = 5) {
await this.ensureInitialized();
if (isExplicitDenyAllScopeFilter(scopeFilter))
return [];
const normalized = prefix.trim().toLowerCase();
if (!/^[0-9a-f][0-9a-f-]{7,35}$/.test(normalized))
return [];
const safePrefix = escapeSqlLiteral(normalized);
const rows = await this.table
.query()
.where(`id LIKE '${safePrefix}%'`)
.limit(Math.max(1, limit))
.toArray();
return rows
.filter((row) => {
const rowScope = row.scope ?? "global";
return !scopeFilter || scopeFilter.length === 0 || scopeFilter.includes(rowScope);
})
.map((row) => ({
id: row.id,
text: row.text,
vector: Array.from(row.vector),
category: row.category,
scope: row.scope ?? "global",
importance: clampImportance(Number(row.importance)),
timestamp: normalizeMemoryTimestamp(row.timestamp, 0),
metadata: row.metadata || "{}",
}));
}
async getById(id, scopeFilter) {
await this.ensureInitialized();
if (isExplicitDenyAllScopeFilter(scopeFilter))
Expand Down
105 changes: 55 additions & 50 deletions dist/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,19 +321,47 @@ function formatIgnoredScopeNotice(resolvedScopes) {
: "(none)";
return `Ignored inaccessible scope "${resolvedScopes.ignoredScope}" and searched accessible scopes instead: ${scopes}.`;
}
async function resolveMemoryId(context, memoryRef, scopeFilter) {
const trimmed = memoryRef.trim();
export async function resolveMemoryId(context, memoryRef, scopeFilter) {
// Agents copy ids out of injected context, which truncates them and often
// appends an ellipsis ("407dec9c..."); strip that before classifying.
const trimmed = memoryRef.trim().replace(/[.…]+$/u, "");
if (!trimmed) {
return {
ok: false,
message: "memoryId/query 不能为空。",
details: { error: "empty_memory_ref" },
};
}
const uuidLike = /^[0-9a-f]{8}(-[0-9a-f]{4}){0,4}/i.test(trimmed);
if (uuidLike) {
const isFullUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed);
if (isFullUuid) {
return { ok: true, id: trimmed };
}
// Documented contract: "full UUID or 8+ char prefix". A hex-shaped ref
// that is not a complete UUID resolves as an id prefix within accessible
// scopes — unique match wins, multiple matches list candidates, and zero
// matches is an honest not-found (never a scan-match or a semantic guess).
const isIdPrefix = /^[0-9a-f][0-9a-f-]{7,35}$/i.test(trimmed);
if (isIdPrefix) {
const matches = await context.store.findByIdPrefix(trimmed, scopeFilter);
if (matches.length === 1) {
return { ok: true, id: matches[0].id };
}
if (matches.length > 1) {
const list = matches
.map((entry) => `- [${entry.id.slice(0, 8)}] ${entry.text.slice(0, 60)}${entry.text.length > 60 ? "..." : ""}`)
.join("\n");
return {
ok: false,
message: `Id prefix "${trimmed}" matches multiple memories. Use a longer prefix or the full id:\n${list}`,
details: { error: "ambiguous_id_prefix", prefix: trimmed },
};
}
return {
ok: false,
message: `Memory ${trimmed} not found or access denied.`,
details: { error: "not_found", id: trimmed },
};
}
const results = await retrieveWithRetry(context.retriever, {
query: trimmed,
limit: 5,
Expand Down Expand Up @@ -1176,25 +1204,32 @@ export function registerMemoryForgetTool(api, context) {
}
}
if (memoryId) {
const deleted = await context.store.delete(memoryId, scopeFilter);
const resolved = await resolveMemoryId(context, memoryId, scopeFilter);
if (resolved.ok === false) {
return {
content: [{ type: "text", text: resolved.message }],
details: resolved.details ?? { error: "not_found", id: memoryId },
};
}
const deleted = await context.store.delete(resolved.id, scopeFilter);
if (deleted) {
context.onMemoriesDeleted?.({ scopeFilter });
return {
content: [
{ type: "text", text: `Memory ${memoryId} forgotten.` },
{ type: "text", text: `Memory ${resolved.id} forgotten.` },
],
details: { action: "deleted", id: memoryId },
details: { action: "deleted", id: resolved.id },
};
}
else {
return {
content: [
{
type: "text",
text: `Memory ${memoryId} not found or access denied.`,
text: `Memory ${resolved.id} not found or access denied.`,
},
],
details: { error: "not_found", id: memoryId },
details: { error: "not_found", id: resolved.id },
};
}
}
Expand Down Expand Up @@ -1308,48 +1343,18 @@ export function registerMemoryUpdateTool(api, context) {
// Determine accessible scopes
const agentId = resolveRuntimeAgentId(runtimeContext.agentId, runtimeCtx);
const scopeFilter = resolveScopeFilter(runtimeContext.scopeManager, agentId);
// Resolve memoryId: if it doesn't look like a UUID, try search
let resolvedId = memoryId;
const uuidLike = /^[0-9a-f]{8}(-[0-9a-f]{4}){0,4}/i.test(memoryId);
if (!uuidLike) {
// Treat as search query
const results = await retrieveWithRetry(context.retriever, {
query: memoryId,
limit: 3,
scopeFilter,
}, () => context.store.count());
if (results.length === 0) {
return {
content: [
{
type: "text",
text: `No memory found matching "${memoryId}".`,
},
],
details: { error: "not_found", query: memoryId },
};
}
if (results.length === 1 || results[0].score > 0.85) {
resolvedId = results[0].entry.id;
}
else {
const list = results
.map((r) => `- [${r.entry.id.slice(0, 8)}] ${r.entry.text.slice(0, 60)}${r.entry.text.length > 60 ? "..." : ""}`)
.join("\n");
return {
content: [
{
type: "text",
text: `Multiple matches. Specify memoryId:\n${list}`,
},
],
details: {
action: "candidates",
candidates: sanitizeMemoryForSerialization(results),
},
};
}
// Resolve memoryId through the shared resolver: full UUID passes
// through, an 8+ char id prefix resolves against accessible rows
// (the tool's documented contract), anything else falls back to
// semantic search.
const resolution = await resolveMemoryId(context, memoryId, scopeFilter);
if (resolution.ok === false) {
return {
content: [{ type: "text", text: resolution.message }],
details: resolution.details ?? { error: "not_found", id: memoryId },
};
}
const resolvedId = resolution.id;
// If text changed, re-embed; reject noise
let newVector;
if (text) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"skills/**/*.md"
],
"scripts": {
"test": "node test/embedder-error-hints.test.mjs && node --test test/embedder-max-input-chars.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/extraction-prompt-structural-noise.test.mjs && node test/i18n-memory-triggers.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/smart-extractor-noise-gating.test.mjs && node test/memory-capability-runtime.test.mjs && node --test test/startup-health-diagnostics.test.mjs && node test/corpus-indexer.test.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/dreaming-engine.test.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs && node --test test/startup-check-timeout.test.mjs && node --test test/memory-subsession-prompt-hooks.test.mjs && node --test test/read-consistency-interval.test.mjs && node --test test/reflection-distiller-hook-skip.test.mjs && node --test test/register-scope-dedup.test.mjs && node --test test/raw-run-distiller-hooks.test.mjs && node --test test/autocapture-watermark-reset.test.mjs && node --test test/autocapture-internal-session-guard.test.mjs && node --test test/memory-categories-storage-map.test.mjs && node --test test/delete-invalidate-reflection-caches.test.mjs",
"test": "node test/embedder-error-hints.test.mjs && node --test test/embedder-max-input-chars.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/extraction-prompt-structural-noise.test.mjs && node test/i18n-memory-triggers.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/smart-extractor-noise-gating.test.mjs && node test/memory-capability-runtime.test.mjs && node --test test/startup-health-diagnostics.test.mjs && node test/corpus-indexer.test.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/dreaming-engine.test.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs && node --test test/startup-check-timeout.test.mjs && node --test test/memory-subsession-prompt-hooks.test.mjs && node --test test/read-consistency-interval.test.mjs && node --test test/reflection-distiller-hook-skip.test.mjs && node --test test/register-scope-dedup.test.mjs && node --test test/raw-run-distiller-hooks.test.mjs && node --test test/autocapture-watermark-reset.test.mjs && node --test test/autocapture-internal-session-guard.test.mjs && node --test test/memory-categories-storage-map.test.mjs && node --test test/delete-invalidate-reflection-caches.test.mjs && node --test test/memory-id-prefix-resolution.test.mjs",
"test:cli-smoke": "node scripts/run-ci-tests.mjs --group cli-smoke",
"test:core-regression": "node scripts/run-ci-tests.mjs --group core-regression",
"test:storage-and-schema": "node scripts/run-ci-tests.mjs --group storage-and-schema",
Expand Down
Loading
Loading