recallnest 3.0.1 → 4.0.0
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/CHANGELOG.md +203 -0
- package/README.md +42 -17
- package/README_CN.md +31 -15
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +10 -2
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +434 -68
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/env-config.ts +118 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +69 -104
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +276 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +3 -4
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +6 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-output.ts +11 -0
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +57 -9
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
|
@@ -1,39 +1,78 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { indexAsset,
|
|
2
|
+
import { indexAsset, pinMemory } from "./asset-sync.js";
|
|
3
3
|
import { createAuditLogger } from "./audit-log.js";
|
|
4
4
|
import { persistCaseMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
|
|
5
5
|
import { autoCapture } from "./capture-heuristic.js";
|
|
6
6
|
import { computeSynthesisUptake } from "./consolidation-engine.js";
|
|
7
7
|
import { runDataCheckup, formatCheckupReport } from "./data-checkup.js";
|
|
8
8
|
import { runDream, formatDreamResult } from "./dream-pipeline.js";
|
|
9
|
-
import { forgetMemory } from "./forget-engine.js";
|
|
9
|
+
import { forgetMemory, summarizeVerification, type ForgetPinArchive, type ForgetResult } from "./forget-engine.js";
|
|
10
10
|
import { exportMemoryGraph, formatGraphExportResult } from "./graph-export.js";
|
|
11
|
-
import { assetSummaryLine, buildBriefAsset,
|
|
11
|
+
import { archivePinAssetsForMemory, assetSummaryLine, buildBriefAsset, findPinAssetsForMemory, listMemoryAssets, listPinAssets, saveBriefAsset, writeExportArtifact } from "./memory-assets.js";
|
|
12
12
|
import { runMemoryLint, formatMemoryLintReport } from "./memory-lint.js";
|
|
13
13
|
import { distillResults, formatExplainResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
14
14
|
import { DurableMemoryCategorySchema, StoreMemorySourceSchema, DebugFramingSchema } from "./memory-schema.js";
|
|
15
15
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
16
16
|
import { persistSkill, retrieveSkills } from "./skill-engine.js";
|
|
17
17
|
import { SkillImplementationTypeSchema } from "./skill-schema.js";
|
|
18
|
+
import { formatBatchStoreReceipt } from "./store-receipts.js";
|
|
18
19
|
import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
|
|
19
|
-
import type { RetrievalResult } from "./retriever.js";
|
|
20
|
-
import type { MemoryStore } from "./store.js";
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
/** The data/pins directory, as forget_memory uses it when nothing else is injected. */
|
|
22
|
+
const defaultForgetPins: ForgetPinArchive = {
|
|
23
|
+
archiveForMemory: (id) => archivePinAssetsForMemory(id).length,
|
|
24
|
+
countForMemory: (id) => findPinAssetsForMemory(id).length,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* forget_memory 的输出:逐项报数,回读结果写在明处。
|
|
29
|
+
* 第一行只有在回读「干净」时才打勾:id 取不到了、哪一层都没数出残留、也没有哪一层查不了。
|
|
30
|
+
*/
|
|
31
|
+
function formatForgetResult(result: ForgetResult): string {
|
|
32
|
+
const id = result.memoryId.slice(0, 8);
|
|
33
|
+
const count = (n: number | null | undefined) => (n === null || n === undefined ? "n/a" : String(n));
|
|
34
|
+
const v = result.verification;
|
|
35
|
+
const state = v ? summarizeVerification(v) : "unverified";
|
|
36
|
+
|
|
37
|
+
let head: string;
|
|
38
|
+
if (state === "clean") {
|
|
39
|
+
head = `✅ Memory ${id} forgotten.`;
|
|
40
|
+
} else if (state === "leftovers" && v) {
|
|
41
|
+
const leftovers: string[] = [];
|
|
42
|
+
if (v.triggerRowsLeft) leftovers.push(`trigger rows ${v.triggerRowsLeft}`);
|
|
43
|
+
if (v.pinsLeft) leftovers.push(`pin files ${v.pinsLeft}`);
|
|
44
|
+
if (v.pinIndexRowsLeft) leftovers.push(`pin index rows ${v.pinIndexRowsLeft}`);
|
|
45
|
+
if (v.kgTriplesLeft) leftovers.push(`KG triples ${v.kgTriplesLeft}`);
|
|
46
|
+
head = `⚠️ Memory ${id} forgotten, but the read-back found leftovers: ${leftovers.join(", ")}.`;
|
|
47
|
+
} else {
|
|
48
|
+
const unchecked = v && v.unverified.length > 0 ? v.unverified.join(", ") : "primary";
|
|
49
|
+
head = `⚠️ Memory ${id} deleted, but the read-back could not check: ${unchecked}.`;
|
|
25
50
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
51
|
+
|
|
52
|
+
const lines = [
|
|
53
|
+
head,
|
|
54
|
+
`Privacy tier: ${result.evidence?.privacyTier || "unknown"}`,
|
|
55
|
+
`KG triples removed: ${result.kgTriplesRemoved ? "yes" : "no/N/A"}`,
|
|
56
|
+
`Trigger rows removed: ${count(result.triggerRowsRemoved)}`,
|
|
57
|
+
`Pins archived: ${count(result.pinsArchived)} (index rows removed: ${count(result.pinIndexRowsRemoved)})`,
|
|
58
|
+
`Cascade demoted: ${result.cascadeResult.demotedCount} related memories`,
|
|
59
|
+
`Read-back: ${v?.primary ?? "unverified"} (left: trigger rows ${count(v?.triggerRowsLeft)}, pin files ${count(v?.pinsLeft)}, pin index rows ${count(v?.pinIndexRowsLeft)}, KG triples ${count(v?.kgTriplesLeft)})`,
|
|
60
|
+
];
|
|
61
|
+
if (v && v.unverified.length > 0 && state !== "unverified") {
|
|
62
|
+
lines.push(`Could not check: ${v.unverified.join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.pinsArchived) {
|
|
65
|
+
// 归档是挪走不是删:文件里还有那段正文,调用方得知道它在哪
|
|
66
|
+
lines.push("Archived pin files were moved, not erased: they are in data/archive/forgotten-pins and still hold the snippet. Delete them there if the content itself has to go.");
|
|
67
|
+
}
|
|
68
|
+
if (result.evidence?.reason) {
|
|
69
|
+
lines.push(`Reason: ${result.evidence.reason}`);
|
|
70
|
+
}
|
|
71
|
+
return lines.join("\n");
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
export function registerAdvancedTools(deps: ToolRegistryDeps): void {
|
|
36
|
-
const { registerTool, getComponents, conflictStore, getKGExtractor, getKGStore, auditLogger } = deps;
|
|
75
|
+
const { registerTool, getComponents, conflictStore, getKGExtractor, getKGStore, auditLogger, forgetPins } = deps;
|
|
37
76
|
|
|
38
77
|
registerTool(
|
|
39
78
|
"store_workflow_pattern",
|
|
@@ -391,15 +430,16 @@ registerTool(
|
|
|
391
430
|
return { content: [{ type: "text" as const, text: `Memory not found: ${memory_id}` }] };
|
|
392
431
|
}
|
|
393
432
|
|
|
394
|
-
|
|
395
|
-
const asset = buildPinAsset(entryToRetrievalResult(entry), {
|
|
433
|
+
const pinned = await pinMemory(store, embedder, entry, scopeSelection.scopeFilter, {
|
|
396
434
|
title,
|
|
397
435
|
summary,
|
|
398
436
|
query,
|
|
399
437
|
profile: profileName || "default",
|
|
400
438
|
});
|
|
401
|
-
|
|
402
|
-
|
|
439
|
+
if (!pinned) {
|
|
440
|
+
return { content: [{ type: "text" as const, text: `Memory not found: ${memory_id}` }] };
|
|
441
|
+
}
|
|
442
|
+
const { asset, path } = pinned;
|
|
403
443
|
|
|
404
444
|
return {
|
|
405
445
|
content: [{
|
|
@@ -473,7 +513,7 @@ registerTool(
|
|
|
473
513
|
|
|
474
514
|
registerTool(
|
|
475
515
|
"forget_memory",
|
|
476
|
-
"Permanently forget a memory
|
|
516
|
+
"Permanently forget a memory: delete the primary entry, remove its KG triples and trigger rows, remove pins made from it (the file is archived, its indexed copy deleted), demote related memories, read back to confirm, and log an audit trail. Requires confirm=true for durable-tier memories. Use when the user explicitly requests a memory be forgotten, or to clean up sensitive/incorrect data.",
|
|
477
517
|
{
|
|
478
518
|
memoryId: z.string().min(1).max(128).describe("Memory ID to forget (full UUID or 8+ hex prefix)"),
|
|
479
519
|
confirm: z.boolean().default(false).describe("Required confirmation — must be true for durable-tier memories"),
|
|
@@ -481,13 +521,19 @@ registerTool(
|
|
|
481
521
|
scope: z.string().min(1).max(160).optional().describe("Optional scope filter for permission check"),
|
|
482
522
|
},
|
|
483
523
|
async ({ memoryId, confirm, reason, scope }) => {
|
|
484
|
-
const { store } = getComponents();
|
|
524
|
+
const { store, triggerStore } = getComponents();
|
|
485
525
|
const kgStoreInstance = getKGStore();
|
|
486
526
|
const auditLogger = createAuditLogger();
|
|
487
527
|
const scopeFilter = scope ? [scope] : undefined;
|
|
488
528
|
|
|
489
529
|
const result = await forgetMemory(
|
|
490
|
-
{
|
|
530
|
+
{
|
|
531
|
+
store,
|
|
532
|
+
kgStore: kgStoreInstance,
|
|
533
|
+
auditLogger,
|
|
534
|
+
triggerStore,
|
|
535
|
+
pins: forgetPins ?? defaultForgetPins,
|
|
536
|
+
},
|
|
491
537
|
{ memoryId, confirm, reason, scopeFilter },
|
|
492
538
|
);
|
|
493
539
|
|
|
@@ -497,20 +543,10 @@ registerTool(
|
|
|
497
543
|
throw new Error(result.error || "forget_memory failed");
|
|
498
544
|
}
|
|
499
545
|
|
|
500
|
-
const lines = [
|
|
501
|
-
`✅ Memory ${result.memoryId.slice(0, 8)} forgotten.`,
|
|
502
|
-
`Privacy tier: ${result.evidence?.privacyTier || "unknown"}`,
|
|
503
|
-
`KG triples removed: ${result.kgTriplesRemoved ? "yes" : "no/N/A"}`,
|
|
504
|
-
`Cascade demoted: ${result.cascadeResult.demotedCount} related memories`,
|
|
505
|
-
];
|
|
506
|
-
if (result.evidence?.reason) {
|
|
507
|
-
lines.push(`Reason: ${result.evidence.reason}`);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
546
|
return {
|
|
511
547
|
content: [{
|
|
512
548
|
type: "text" as const,
|
|
513
|
-
text:
|
|
549
|
+
text: formatForgetResult(result),
|
|
514
550
|
}],
|
|
515
551
|
};
|
|
516
552
|
},
|
|
@@ -555,9 +591,14 @@ registerTool(
|
|
|
555
591
|
// 在产无人消费的内容或读打点断链,是能报警的健康度数字。
|
|
556
592
|
// cap 取 totalCount 自适应库大小(2026-07-23 实测库已 117K,固定 cap 追不上增长)。
|
|
557
593
|
const uptake = await computeSynthesisUptake(store, stats.totalCount + 1_000);
|
|
594
|
+
// 两行两个口径(2026-09-09):第一行是老数字,保留可比但标明它可被检索刷高、只能读成
|
|
595
|
+
// "被碰过";第二行 distinctReaderCount≥2 是更难被扰动的口径。判断"合成产物有没有用"看第二行。
|
|
558
596
|
const uptakeLine = uptake.uptakeRate === null
|
|
559
|
-
? "
|
|
560
|
-
: `
|
|
597
|
+
? " touched ≥1: n/a (no derived insights found)"
|
|
598
|
+
: ` touched ≥1 (accessCount>0; every search hit counts — inflatable, not a usefulness signal): ${(uptake.uptakeRate * 100).toFixed(1)}% (${uptake.derivedRead}/${uptake.derivedTotal})`;
|
|
599
|
+
const multiReaderLine = uptake.multiReaderRate === null
|
|
600
|
+
? " read by ≥2 distinct readers: n/a"
|
|
601
|
+
: ` read by ≥2 distinct readers (harder to inflate — use this one): ${(uptake.multiReaderRate * 100).toFixed(1)}% (${uptake.derivedReadByMultiple}/${uptake.derivedTotal})`;
|
|
561
602
|
|
|
562
603
|
const lines = [
|
|
563
604
|
`Total entries: ${stats.totalCount}`,
|
|
@@ -575,6 +616,7 @@ registerTool(
|
|
|
575
616
|
`Synthesis uptake${uptake.truncated ? ` (scan capped at ${uptake.scanned})` : ""}:`,
|
|
576
617
|
` derived insights: ${uptake.derivedTotal}`,
|
|
577
618
|
uptakeLine,
|
|
619
|
+
multiReaderLine,
|
|
578
620
|
];
|
|
579
621
|
|
|
580
622
|
return {
|
|
@@ -761,7 +803,7 @@ registerTool(
|
|
|
761
803
|
|
|
762
804
|
registerTool(
|
|
763
805
|
"retrieve_skill",
|
|
764
|
-
"Retrieve
|
|
806
|
+
"Retrieve agent-readable runbooks matching a task description by semantic similarity. Read-only. RecallNest returns stored instructions; it does not execute them.",
|
|
765
807
|
{
|
|
766
808
|
query: z.string().min(1).max(300).describe("Natural language task description to match, e.g. 'deploy the app to production'"),
|
|
767
809
|
scope: z.string().min(1).max(160).optional().describe("Restrict to skills in a specific scope, e.g. 'project:myapp'. Omit to search all scopes"),
|
|
@@ -838,17 +880,10 @@ registerTool(
|
|
|
838
880
|
})),
|
|
839
881
|
});
|
|
840
882
|
|
|
841
|
-
const counts = { new: 0, deduped: 0, updated: 0 };
|
|
842
|
-
for (const r of stored) {
|
|
843
|
-
if (r.disposition === "deduped") counts.deduped++;
|
|
844
|
-
else if (r.disposition === "updated") counts.updated++;
|
|
845
|
-
else counts.new++;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
883
|
return {
|
|
849
884
|
content: [{
|
|
850
885
|
type: "text" as const,
|
|
851
|
-
text:
|
|
886
|
+
text: formatBatchStoreReceipt(stored),
|
|
852
887
|
}],
|
|
853
888
|
};
|
|
854
889
|
},
|
package/src/mcp-tools-core.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { persistMemory } from "./capture-engine.js";
|
|
3
|
+
import { formatStoreMemoryReceipt } from "./store-receipts.js";
|
|
3
4
|
import { composeLightResumeContext, composeResumeContext } from "./context-composer.js";
|
|
4
5
|
import { renderMemories } from "./context-renderer.js";
|
|
5
6
|
import { formatSearchResults, formatBriefResults, formatFullResults, formatCollapsedResults } from "./memory-output.js";
|
|
6
|
-
import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled } from "./memory-schema.js";
|
|
7
|
+
import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled, MemoryTriggersSchema } from "./memory-schema.js";
|
|
7
8
|
import type { PredictionContext } from "./prediction-engine.js";
|
|
8
9
|
import { setReminder, checkTriggers, fireReminder, suggestPredictedReminders, formatSuggestedReminders } from "./prospective-memory.js";
|
|
9
10
|
import { resolveRecallMode } from "./runtime-config.js";
|
|
10
11
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
11
12
|
import { createScopeSuggester } from "./scope-suggester.js";
|
|
12
13
|
import { buildSessionCheckpointResult } from "./session-engine.js";
|
|
14
|
+
import { mirrorCheckpointWithDeadline } from "./checkpoint-mirror.js";
|
|
13
15
|
import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
|
|
14
16
|
import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
|
|
15
17
|
import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
|
|
@@ -95,6 +97,7 @@ registerTool(
|
|
|
95
97
|
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
96
98
|
topicTag: z.string().min(1).max(60).optional().describe("Optional topic tag for intra-scope partitioning (e.g. 'auth', 'deploy', 'testing'). Auto-detected if omitted."),
|
|
97
99
|
privacyTier: PrivacyTierSchema.default("durable").describe("Privacy tier: ephemeral (auto-expire, no KG), private (persist, no KG), durable (default), shared (cross-scope)"),
|
|
100
|
+
triggers: MemoryTriggersSchema.describe("写入时预演触发器:这条记忆「她以后会怎么问起」的 2–5 句口语问法,各说一个角度、别互为变体、别复述正文。只参与召回、不进正文。scope 为 memory:pivot 时必填(§5.3)。例:[\"那个跑分要不要再跑一次\", \"别人的榜我们要不要也去排个名\"]"),
|
|
98
101
|
validUntil: z.union([z.string(), z.number()]).optional().describe("Optional expiration: ISO date string or ms timestamp. Memory will be deprioritized after this time."),
|
|
99
102
|
eventTime: z.union([z.string(), z.number()]).optional().describe("Optional event time: when the event actually happened (ISO date or ms), distinct from storage time."),
|
|
100
103
|
confidence: z.union([
|
|
@@ -106,8 +109,8 @@ registerTool(
|
|
|
106
109
|
]).optional().describe("Optional confidence override: number (0-1) or {score, reliability}. Auto-assigned from source if omitted."),
|
|
107
110
|
dependsOn: DependsOnSchema.optional().describe("Optional freshness dependencies (borrowed). Declare what this memory depends on so recall shows a cheap validity verdict (exact/compatible/uncertain/invalid). Items: {kind:'file'|'git-rev', ref, expected?}. file → ref=path, expected=mtime-ms string; git-rev → ref=repo path, expected=commit hash (short ok). expected may be an array: first=exact, rest=compatible set. Omit expected for existence-only checks."),
|
|
108
111
|
},
|
|
109
|
-
async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, validUntil, eventTime, confidence, dependsOn }) => {
|
|
110
|
-
const { store, embedder } = getComponents();
|
|
112
|
+
async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, triggers, validUntil, eventTime, confidence, dependsOn }) => {
|
|
113
|
+
const { store, embedder, triggerStore } = getComponents();
|
|
111
114
|
const kgExtractor = getKGExtractor();
|
|
112
115
|
const stored = await persistMemory({
|
|
113
116
|
store,
|
|
@@ -115,6 +118,8 @@ registerTool(
|
|
|
115
118
|
conflictStore,
|
|
116
119
|
kgExtractor,
|
|
117
120
|
auditLogger,
|
|
121
|
+
triggerStore,
|
|
122
|
+
embedQueryBatch: (texts: string[]) => embedder.embedBatchQuery(texts),
|
|
118
123
|
}, {
|
|
119
124
|
text,
|
|
120
125
|
category,
|
|
@@ -125,6 +130,7 @@ registerTool(
|
|
|
125
130
|
canonicalKey,
|
|
126
131
|
topicTag,
|
|
127
132
|
privacyTier,
|
|
133
|
+
triggers,
|
|
128
134
|
// F3: Pass temporal validity params (extracted by persistMemory before Zod parse)
|
|
129
135
|
validUntil,
|
|
130
136
|
eventTime,
|
|
@@ -137,16 +143,7 @@ registerTool(
|
|
|
137
143
|
return {
|
|
138
144
|
content: [{
|
|
139
145
|
type: "text" as const,
|
|
140
|
-
text:
|
|
141
|
-
`Stored memory ${stored.id.slice(0, 8)}`,
|
|
142
|
-
`Disposition: ${stored.disposition}`,
|
|
143
|
-
...(stored.rejectionReason ? [`Rejection reason: ${stored.rejectionReason}`] : []),
|
|
144
|
-
`Category: ${stored.category}`,
|
|
145
|
-
`Scope: ${stored.resolvedScope}`,
|
|
146
|
-
`Canonical key: ${stored.canonicalKey}`,
|
|
147
|
-
...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
|
|
148
|
-
`Stored at: ${stored.storedAt}`,
|
|
149
|
-
].join("\n"),
|
|
146
|
+
text: formatStoreMemoryReceipt(stored, { requestedTriggers: triggers?.length ?? 0, scope }),
|
|
150
147
|
}],
|
|
151
148
|
};
|
|
152
149
|
}
|
|
@@ -215,6 +212,11 @@ registerTool(
|
|
|
215
212
|
...result,
|
|
216
213
|
record: storedRecord,
|
|
217
214
|
}));
|
|
215
|
+
// Mirror into the memory table so search_memory can find it; never throws, capped wait.
|
|
216
|
+
await mirrorCheckpointWithDeadline(() => {
|
|
217
|
+
const { store, embedder } = getComponents();
|
|
218
|
+
return { store, embedder };
|
|
219
|
+
}, storedRecord);
|
|
218
220
|
return {
|
|
219
221
|
content: [{
|
|
220
222
|
type: "text" as const,
|
|
@@ -332,7 +334,9 @@ registerTool(
|
|
|
332
334
|
return {
|
|
333
335
|
content: [{
|
|
334
336
|
type: "text" as const,
|
|
335
|
-
text: lightResult.
|
|
337
|
+
text: lightResult.resolvedScope
|
|
338
|
+
? `Scope: ${lightResult.resolvedScope}\n${lightResult.text}`
|
|
339
|
+
: lightResult.text,
|
|
336
340
|
}],
|
|
337
341
|
};
|
|
338
342
|
}
|
|
@@ -373,11 +377,11 @@ registerTool(
|
|
|
373
377
|
|
|
374
378
|
registerTool(
|
|
375
379
|
"search_memory",
|
|
376
|
-
"Search indexed memories by
|
|
380
|
+
"Search indexed memories by semantic relevance (vector search + reranking; BM25 keyword fusion runs only when retrieval.mode is \"hybrid\", and the default is vector-only) and return ranked results with optional temporal filtering. With the default mode there is no keyword channel, so exact identifiers such as commit hashes or variable names may not surface — use a raw-text search for those. The shown score is a fused ranking score (0-100%), NOT pure cosine similarity — read it as relative ranking within this result set, not as match confidence. Read-only, but may fire stored reminders as a side effect. Use proactively at the start of tasks, when debugging, writing, or when the user references past work.",
|
|
377
381
|
{
|
|
378
382
|
query: z.string().describe("Search query — natural language or keywords"),
|
|
379
383
|
limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
|
|
380
|
-
scope: z.string().optional().describe("
|
|
384
|
+
scope: z.string().optional().describe("Explicit scope; reuse Scope returned by resume_context. May be omitted when sessionId or configured environment supplies a scope, or allScopes=true explicitly requests cross-scope search. Omission alone does not mean global search."),
|
|
381
385
|
sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
|
|
382
386
|
allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
|
|
383
387
|
includeRelatedScopes: z.boolean().default(false).describe("When true, also query configured related scopes from scopeRelations and show them in a separate sidecar section. Requires an explicit or inferred scoped search; never changes the main result ranking."),
|
|
@@ -389,7 +393,7 @@ registerTool(
|
|
|
389
393
|
graph: z.boolean().default(false).optional().describe("Enable KG graph traversal (PPR) for relationship-aware search. Use when query involves entity relationships (e.g. 'what tools does Alice use', 'Bob的朋友')."),
|
|
390
394
|
includeArchived: z.boolean().default(false).optional().describe("When true, also return archived/superseded/consolidated memories (default: only active)"),
|
|
391
395
|
detail_level: z.enum(["brief", "normal", "full", "adaptive"]).default("normal").optional()
|
|
392
|
-
.describe("Result detail level: brief (ID+score+one-liner),
|
|
396
|
+
.describe("Result detail level: normal (default — the full text of each hit, in rank order. One entry is capped at 2400 chars. Full text is handed out until an 8k-token budget is used up; hits past that point get a query-aware snippet instead of being dropped, so a long list can run past 8k by one snippet per remaining hit, and each related-scope block has its own budget. Any shortened entry says how much of it is shown and gives the id for memory_drill_down), brief (ID+score+one-liner), full (table with 120-char snippets plus extra metadata), adaptive (full text only for hits scoring above the threshold, query-aware snippet for the rest, 8k-token budget)"),
|
|
393
397
|
topicTag: z.string().min(1).max(60).optional()
|
|
394
398
|
.describe("Filter by topic tag (e.g. 'auth', 'deploy', 'testing'). Only returns memories tagged with this topic."),
|
|
395
399
|
reconstruct: z.boolean().default(false).describe(
|
|
@@ -531,6 +535,8 @@ registerTool(
|
|
|
531
535
|
includeArchived,
|
|
532
536
|
topicTag,
|
|
533
537
|
source: "auto-recall",
|
|
538
|
+
// 旁路标 auto-recall 只为不做强化写入;查询本身是人发起的,不过琐碎查询闸
|
|
539
|
+
trivialQueryGate: false,
|
|
534
540
|
validAt: validAt ? new Date(validAt).getTime() : undefined,
|
|
535
541
|
includeExpired: includeExpired ?? undefined,
|
|
536
542
|
}, {
|
|
@@ -7,7 +7,7 @@ import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflic
|
|
|
7
7
|
import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
|
|
8
8
|
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
9
9
|
import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
|
|
10
|
-
import { archiveDirtyBriefAsset,
|
|
10
|
+
import { archiveDirtyBriefAsset, formatDirtyBriefEmptyMessage, formatDirtyBriefReadErrors, scanDirtyBriefAssets } from "./memory-assets.js";
|
|
11
11
|
import {
|
|
12
12
|
scanMemoryPromotions,
|
|
13
13
|
scanSynthesisPromotions,
|
|
@@ -266,15 +266,18 @@ registerTool(
|
|
|
266
266
|
"List memory briefs generated before current cleanup rules that may need re-indexing. Read-only. Use when auditing brief quality or before running clean_dirty_briefs.",
|
|
267
267
|
{},
|
|
268
268
|
async () => {
|
|
269
|
-
const
|
|
269
|
+
const scan = scanDirtyBriefAssets();
|
|
270
|
+
const rows = scan.items;
|
|
270
271
|
if (rows.length === 0) {
|
|
271
|
-
return { content: [{ type: "text" as const, text:
|
|
272
|
+
return { content: [{ type: "text" as const, text: formatDirtyBriefEmptyMessage(scan) }] };
|
|
272
273
|
}
|
|
273
274
|
const lines = [
|
|
274
275
|
"Brief ID Title Scope Reasons",
|
|
275
276
|
"-------- ----- ----- ----------------------------------------",
|
|
276
277
|
...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`),
|
|
277
278
|
];
|
|
279
|
+
const readErrors = formatDirtyBriefReadErrors(scan.readErrors);
|
|
280
|
+
if (readErrors) lines.push("", readErrors);
|
|
278
281
|
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
279
282
|
}
|
|
280
283
|
);
|
|
@@ -287,9 +290,12 @@ registerTool(
|
|
|
287
290
|
},
|
|
288
291
|
async ({ apply }) => {
|
|
289
292
|
const { store } = getComponents();
|
|
290
|
-
const
|
|
293
|
+
const scan = scanDirtyBriefAssets();
|
|
294
|
+
const rows = scan.items;
|
|
295
|
+
const readErrors = formatDirtyBriefReadErrors(scan.readErrors);
|
|
296
|
+
const readErrorsTail = readErrors ? `\n\n${readErrors}` : "";
|
|
291
297
|
if (rows.length === 0) {
|
|
292
|
-
return { content: [{ type: "text" as const, text:
|
|
298
|
+
return { content: [{ type: "text" as const, text: formatDirtyBriefEmptyMessage(scan) }] };
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
if (!apply) {
|
|
@@ -297,7 +303,7 @@ registerTool(
|
|
|
297
303
|
return {
|
|
298
304
|
content: [{
|
|
299
305
|
type: "text" as const,
|
|
300
|
-
text: `Dirty briefs detected: ${rows.length}\n\n${preview}\n\nCall clean_dirty_briefs with apply=true to archive them
|
|
306
|
+
text: `Dirty briefs detected: ${rows.length}\n\n${preview}\n\nCall clean_dirty_briefs with apply=true to archive them.${readErrorsTail}`,
|
|
301
307
|
}],
|
|
302
308
|
};
|
|
303
309
|
}
|
|
@@ -321,7 +327,7 @@ registerTool(
|
|
|
321
327
|
return {
|
|
322
328
|
content: [{
|
|
323
329
|
type: "text" as const,
|
|
324
|
-
text: `Dirty briefs: ${rows.length}\nArchived: ${archived}\nIndex rows deleted: ${deleted}`,
|
|
330
|
+
text: `Dirty briefs: ${rows.length}\nArchived: ${archived}\nIndex rows deleted: ${deleted}${readErrorsTail}`,
|
|
325
331
|
}],
|
|
326
332
|
};
|
|
327
333
|
}
|
package/src/memory-assets.ts
CHANGED
|
@@ -76,6 +76,7 @@ const DATA_DIR = resolve(metaDir(import.meta), "../data");
|
|
|
76
76
|
const PINS_DIR = join(DATA_DIR, "pins");
|
|
77
77
|
const ASSETS_DIR = join(DATA_DIR, "assets");
|
|
78
78
|
const ARCHIVE_DIR = join(DATA_DIR, "archive", "dirty-briefs");
|
|
79
|
+
const FORGOTTEN_PINS_DIR = join(DATA_DIR, "archive", "forgotten-pins");
|
|
79
80
|
const EXPORTS_DIR = join(DATA_DIR, "exports");
|
|
80
81
|
|
|
81
82
|
function ensureDir(dir: string): string {
|
|
@@ -112,6 +113,7 @@ function retrievalPath(result: RetrievalResult): string {
|
|
|
112
113
|
if (result.sources.vector) parts.push("vector");
|
|
113
114
|
if (result.sources.bm25) parts.push("bm25");
|
|
114
115
|
if (result.sources.reranked) parts.push("reranked");
|
|
116
|
+
if (result.sources.trigger) parts.push("trigger");
|
|
115
117
|
return parts.join("+") || "direct";
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -231,7 +233,98 @@ export function listPinAssets(limit = 20): Array<PinAsset & { path: string }> {
|
|
|
231
233
|
return items;
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
|
|
236
|
+
/**
|
|
237
|
+
* 来源是这条记忆的 pin 文件(不限条数)。pins 目录不存在就返回空,不顺手建目录。
|
|
238
|
+
*
|
|
239
|
+
* 「读不了」和「读出来不是 JSON」分开处理:
|
|
240
|
+
* - 读不了(EIO、EACCES、iCloud 占位文件的 EDEADLK……)直接抛出。说不清这个文件属于谁,
|
|
241
|
+
* 就不能当它不存在——forget 靠这里判断「还有没有 pin」,吞掉错误等于谎报没有。
|
|
242
|
+
* - 不是 JSON 的跳过:list_pins 和 resume_context 同样读不了它,它进不了任何输出。
|
|
243
|
+
*/
|
|
244
|
+
export function findPinAssetsForMemory(
|
|
245
|
+
memoryId: string,
|
|
246
|
+
pinsDir: string = PINS_DIR,
|
|
247
|
+
): Array<PinAsset & { path: string }> {
|
|
248
|
+
if (!existsSync(pinsDir)) return [];
|
|
249
|
+
const items: Array<PinAsset & { path: string }> = [];
|
|
250
|
+
for (const dirent of readdirSync(pinsDir, { withFileTypes: true })) {
|
|
251
|
+
if (dirent.isDirectory() || !dirent.name.endsWith(".json")) continue;
|
|
252
|
+
const path = join(pinsDir, dirent.name);
|
|
253
|
+
const raw = readFileSync(path, "utf-8");
|
|
254
|
+
let parsed: PinAsset;
|
|
255
|
+
try {
|
|
256
|
+
parsed = JSON.parse(raw) as PinAsset;
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (parsed?.source?.memoryId === memoryId) items.push({ ...parsed, path });
|
|
261
|
+
}
|
|
262
|
+
return items;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* archivePinAssetsForMemory 挪到一半出错时抛这个。已经挪走的不挪回去:数量与新路径带在错误上,
|
|
267
|
+
* 调用方(forget 引擎)据此如实报「已经动过什么」,而不是当作什么都没发生。
|
|
268
|
+
*/
|
|
269
|
+
export class PinArchiveIncompleteError extends Error {
|
|
270
|
+
/** 出错之前已经挪到归档目录的文件(新路径)。 */
|
|
271
|
+
readonly archived: string[];
|
|
272
|
+
/** 同上,条数。forget 引擎按这个字段名读(ForgetPinArchive 的约定)。 */
|
|
273
|
+
readonly archivedCount: number;
|
|
274
|
+
|
|
275
|
+
constructor(cause: unknown, archived: string[], total: number) {
|
|
276
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
277
|
+
super(`${reason} (${archived.length} of ${total} pin file(s) had already been moved to the archive)`, { cause });
|
|
278
|
+
this.name = "PinArchiveIncompleteError";
|
|
279
|
+
this.archived = [...archived];
|
|
280
|
+
this.archivedCount = archived.length;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 把来源是这条记忆的 pin 挪出 pins 目录(forget 用)。
|
|
286
|
+
*
|
|
287
|
+
* pin 文件是记忆正文的一份拷贝(标题、摘要、320 字片段),会被 list_pins 列出、被 resume_context
|
|
288
|
+
* 拼进上下文;记忆被 forget 之后它不该继续出现。这里挪到 data/archive/forgotten-pins/ 而不是删:
|
|
289
|
+
* 文件离开了会被读到的目录,误删时还能挪回。同名不覆盖。
|
|
290
|
+
* 返回挪到的新路径。读不了或挪不动就抛错,由调用方决定要不要继续;已经挪走几个之后才出错的,
|
|
291
|
+
* 抛 PinArchiveIncompleteError,已挪走的数量与新路径带在错误上(不挪回去)。
|
|
292
|
+
*
|
|
293
|
+
* 只管文件这一半。pin 在主表里还有一行可检索的索引副本(asset-sync.ts),那一半由
|
|
294
|
+
* forget 引擎在同一步里删。
|
|
295
|
+
*/
|
|
296
|
+
export function archivePinAssetsForMemory(
|
|
297
|
+
memoryId: string,
|
|
298
|
+
dirs: { pinsDir?: string; archiveDir?: string } = {},
|
|
299
|
+
): string[] {
|
|
300
|
+
const matches = findPinAssetsForMemory(memoryId, dirs.pinsDir ?? PINS_DIR);
|
|
301
|
+
if (matches.length === 0) return [];
|
|
302
|
+
const archiveDir = ensureDir(dirs.archiveDir ?? FORGOTTEN_PINS_DIR);
|
|
303
|
+
const moved: string[] = [];
|
|
304
|
+
for (const asset of matches) {
|
|
305
|
+
const stem = basename(asset.path, ".json");
|
|
306
|
+
let target = join(archiveDir, `${stem}.json`);
|
|
307
|
+
for (let n = 2; existsSync(target); n++) {
|
|
308
|
+
target = join(archiveDir, `${stem}-${n}.json`);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
renameSync(asset.path, target);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
// 第一个就挪不动:什么都没变,原样抛。后面的才挪不动:前面的已经离开 pins 目录了,得让调用方知道。
|
|
314
|
+
if (moved.length === 0) throw err;
|
|
315
|
+
throw new PinArchiveIncompleteError(err, moved, matches.length);
|
|
316
|
+
}
|
|
317
|
+
moved.push(target);
|
|
318
|
+
}
|
|
319
|
+
return moved;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface AssetReadError {
|
|
323
|
+
path: string;
|
|
324
|
+
reason: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readJsonRecords<T extends MemoryAsset>(dir: string, limit = 20): { items: Array<T & { path: string }>; readErrors: AssetReadError[] } {
|
|
235
328
|
ensureDir(dir);
|
|
236
329
|
const files = readdirSync(dir)
|
|
237
330
|
.filter(name => name.endsWith(".json"))
|
|
@@ -240,15 +333,21 @@ function listJsonRecords<T extends MemoryAsset>(dir: string, limit = 20): Array<
|
|
|
240
333
|
.slice(0, limit);
|
|
241
334
|
|
|
242
335
|
const items: Array<T & { path: string }> = [];
|
|
336
|
+
const readErrors: AssetReadError[] = [];
|
|
243
337
|
for (const path of files) {
|
|
244
338
|
try {
|
|
245
339
|
const parsed = JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
246
340
|
items.push({ ...parsed, path });
|
|
247
|
-
} catch {
|
|
248
|
-
|
|
341
|
+
} catch (err) {
|
|
342
|
+
readErrors.push({ path, reason: err instanceof Error ? err.message : String(err) });
|
|
249
343
|
}
|
|
250
344
|
}
|
|
251
|
-
return items;
|
|
345
|
+
return { items, readErrors };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function listJsonRecords<T extends MemoryAsset>(dir: string, limit = 20): Array<T & { path: string }> {
|
|
349
|
+
// 读不了的文件在这里照旧跳过;要知道有哪些读不了,用 readJsonRecords。
|
|
350
|
+
return readJsonRecords<T>(dir, limit).items;
|
|
252
351
|
}
|
|
253
352
|
|
|
254
353
|
export function listBriefAssets(limit = 20): Array<BriefAsset & { path: string }> {
|
|
@@ -275,8 +374,14 @@ function detectDirtyBriefReasons(asset: BriefAsset): string[] {
|
|
|
275
374
|
return reasons;
|
|
276
375
|
}
|
|
277
376
|
|
|
278
|
-
export
|
|
279
|
-
|
|
377
|
+
export interface DirtyBriefScan {
|
|
378
|
+
items: DirtyBriefAssetRecord[];
|
|
379
|
+
/** 读取或解析失败的文件:没有判定、不会被归档,原样留在资产目录里。 */
|
|
380
|
+
readErrors: AssetReadError[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function toDirtyBriefRecords(assets: Array<BriefAsset & { path: string }>): DirtyBriefAssetRecord[] {
|
|
384
|
+
return assets
|
|
280
385
|
.map((asset) => {
|
|
281
386
|
const reasons = detectDirtyBriefReasons(asset);
|
|
282
387
|
return {
|
|
@@ -288,8 +393,39 @@ export function listDirtyBriefAssets(limit = 200): DirtyBriefAssetRecord[] {
|
|
|
288
393
|
.filter((asset) => asset.reasons.length > 0);
|
|
289
394
|
}
|
|
290
395
|
|
|
291
|
-
export function
|
|
292
|
-
const
|
|
396
|
+
export function scanDirtyBriefAssets(limit = 200, assetsDir?: string): DirtyBriefScan {
|
|
397
|
+
const { items, readErrors } = readJsonRecords<BriefAsset>(assetsDir ?? getAssetsDir(), limit);
|
|
398
|
+
return { items: toDirtyBriefRecords(items), readErrors };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function listDirtyBriefAssets(limit = 200): DirtyBriefAssetRecord[] {
|
|
402
|
+
return scanDirtyBriefAssets(limit).items;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 脏 brief 扫描结果里「没有要清的」那句话。有文件读不了时不说「没有」,
|
|
407
|
+
* 免得把坏文件藏起来:真干净才回 "No dirty briefs found."。
|
|
408
|
+
*/
|
|
409
|
+
export function formatDirtyBriefEmptyMessage(scan: DirtyBriefScan): string {
|
|
410
|
+
if (scan.readErrors.length === 0) return "No dirty briefs found.";
|
|
411
|
+
return [
|
|
412
|
+
"No dirty briefs among readable files, but the scan is incomplete.",
|
|
413
|
+
formatDirtyBriefReadErrors(scan.readErrors),
|
|
414
|
+
].join("\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 读不了的文件清单;没有读取失败时返回空串。 */
|
|
418
|
+
export function formatDirtyBriefReadErrors(readErrors: AssetReadError[]): string {
|
|
419
|
+
if (readErrors.length === 0) return "";
|
|
420
|
+
const noun = readErrors.length === 1 ? "file" : "files";
|
|
421
|
+
return [
|
|
422
|
+
`${readErrors.length} brief ${noun} could not be read (left in place, not archived):`,
|
|
423
|
+
...readErrors.map((item) => ` ${item.path} — ${item.reason}`),
|
|
424
|
+
].join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function archiveDirtyBriefAsset(asset: DirtyBriefAssetRecord, archiveDir?: string): string {
|
|
428
|
+
const dir = archiveDir ? ensureDir(archiveDir) : getDirtyBriefArchiveDir();
|
|
293
429
|
const targetPath = join(dir, basename(asset.path));
|
|
294
430
|
renameSync(asset.path, targetPath);
|
|
295
431
|
return targetPath;
|
package/src/memory-boundaries.ts
CHANGED
|
@@ -99,6 +99,25 @@ export function isTranscriptScope(scope: string): boolean {
|
|
|
99
99
|
return TRANSCRIPT_SCOPE_PREFIXES.some((prefix) => scope.startsWith(prefix));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Scope of the session-checkpoint mirror (src/checkpoint-mirror.ts): one `events` row per session.
|
|
104
|
+
* Lives here so the retriever can refer to it without importing the mirror module.
|
|
105
|
+
*/
|
|
106
|
+
export const CHECKPOINT_MIRROR_SCOPE = "checkpoint";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Scopes a retrieval must keep out of its candidate pool. The vector / BM25 candidate pool is
|
|
110
|
+
* cut to at most 20 rows before the category filter runs, so rows that the category filter is
|
|
111
|
+
* certain to drop still take candidate slots from the rows it would keep. Mirror rows are all
|
|
112
|
+
* `events`; any other category filter therefore drops every one of them — excluding the scope up
|
|
113
|
+
* front is lossless and keeps category searches as they were before the mirror existed.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveExcludedScopes(params: { category?: string; excludeScopes?: string[] }): string[] {
|
|
116
|
+
const excluded = new Set(params.excludeScopes ?? []);
|
|
117
|
+
if (params.category && params.category !== "events") excluded.add(CHECKPOINT_MIRROR_SCOPE);
|
|
118
|
+
return [...excluded];
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
export function isDurableMemoryScope(scope: string): boolean {
|
|
103
122
|
return scope.startsWith("memory:") || scope.startsWith("asset:");
|
|
104
123
|
}
|
package/src/memory-evolution.ts
CHANGED
|
@@ -158,6 +158,15 @@ export function recordAccess(metadata: string | undefined): string {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* recordAccess 的对象版:给 patchMetadataBatch 的 patchFn 用——在写锁内、以库里最新的元数据为底座加访问计数,
|
|
163
|
+
* 不再拿检索时读到的元数据整行写回(那样会把检索与写回之间别的进程写下的状态盖掉)。
|
|
164
|
+
*/
|
|
165
|
+
export function recordAccessOnMeta(meta: Record<string, unknown>, now: number = Date.now()): Record<string, unknown> {
|
|
166
|
+
const evo = parseEvolution(JSON.stringify(meta));
|
|
167
|
+
return patchEvolutionOnMeta(meta, { accessCount: evo.accessCount + 1, lastAccessedAt: now });
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
// ---------------------------------------------------------------------------
|
|
162
171
|
// Supersede / Consolidate helpers
|
|
163
172
|
// ---------------------------------------------------------------------------
|