recallnest 2.6.1 → 3.0.1
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 +292 -0
- package/README.md +280 -195
- package/README_CN.md +266 -199
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/capture-engine.ts +26 -4
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/context-composer-stable.ts +5 -1
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +44 -5
- package/src/env-config.ts +71 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +248 -22
- package/src/kg-extractor.ts +17 -0
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +47 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/runtime-config.ts +9 -0
- package/src/scope-policy.ts +22 -1
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +13 -4
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +218 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- package/src/__tests__/access-tracker-novelty.test.ts +0 -304
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -945
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -430
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -276
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -780
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config.test.ts +0 -151
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -189
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -250
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -205
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -839
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
package/src/mcp-server.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Tool tiers:
|
|
10
10
|
* - core: Always exposed (7 tools)
|
|
11
11
|
* - advanced: Exposed by default, includes core (30 tools)
|
|
12
|
-
* - full: All tools including governance (
|
|
12
|
+
* - full: All tools including governance (44 tools)
|
|
13
13
|
*
|
|
14
14
|
* Control: RECALLNEST_MCP_TIER=core|advanced|full
|
|
15
15
|
*/
|
|
@@ -79,6 +79,7 @@ const TOOL_TIERS: Record<string, ToolTier> = {
|
|
|
79
79
|
distill_session: "advanced",
|
|
80
80
|
scan_skill_promotions: "governance",
|
|
81
81
|
promote_scan: "governance",
|
|
82
|
+
promote_synthesis: "governance",
|
|
82
83
|
manage_alias: "governance",
|
|
83
84
|
forget_memory: "advanced",
|
|
84
85
|
|
|
@@ -633,10 +633,11 @@ registerTool(
|
|
|
633
633
|
"dream",
|
|
634
634
|
"Run a full memory consolidation cycle (Orient, Gather, Consolidate, Prune). Side effect: may archive low-value entries and generate insight memories. Use when memory count is high and you need periodic maintenance.",
|
|
635
635
|
{
|
|
636
|
-
scope: z.string().min(1).max(160).optional().describe("Scope to consolidate, e.g. 'project:myapp'. Omit to consolidate across all scopes"),
|
|
636
|
+
scope: z.string().min(1).max(160).optional().describe("Scope to consolidate, e.g. 'project:myapp'. Matched exactly by default — a scope name is treated as itself, not as a prefix family. Omit to consolidate across all scopes"),
|
|
637
637
|
force: z.boolean().default(false).describe("Set to true to force consolidation even if recent write count is below the automatic threshold"),
|
|
638
|
+
familyMatch: z.boolean().default(false).describe("Set to true to also consolidate child scopes sharing this prefix (e.g. 'memory' would additionally pull in 'memory:pivot'). Off by default: merging sibling scopes into one clustering round mixes unrelated material."),
|
|
638
639
|
},
|
|
639
|
-
async ({ scope, force }) => {
|
|
640
|
+
async ({ scope, force, familyMatch }) => {
|
|
640
641
|
const resolvedScope = scope || "project:default";
|
|
641
642
|
const components = getComponents();
|
|
642
643
|
const result = await runDream({
|
|
@@ -646,6 +647,10 @@ registerTool(
|
|
|
646
647
|
scope: resolvedScope,
|
|
647
648
|
force,
|
|
648
649
|
kgStore: getKGStore(),
|
|
650
|
+
// MCP 调用方给的都是具体 scope 名,family 语义在这里是意外面:
|
|
651
|
+
// `dream({scope: "memory"})` 会连 `memory:pivot` 一起卷进去(2026-08-16 实测)。
|
|
652
|
+
// 默认改精确是**行为变更**,要 family 得显式要。
|
|
653
|
+
scopeMatch: familyMatch ? "family" : "exact",
|
|
649
654
|
});
|
|
650
655
|
return {
|
|
651
656
|
content: [{ type: "text" as const, text: formatDreamResult(result) }],
|
package/src/mcp-tools-core.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { buildSessionCheckpointResult } from "./session-engine.js";
|
|
|
13
13
|
import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
|
|
14
14
|
import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
|
|
15
15
|
import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
|
|
16
|
+
import { PROCESS_READER_ID, recentRecallHits } from "./recall-ledger.js";
|
|
16
17
|
import { buildWorkflowObservationRecord } from "./workflow-observation-engine.js";
|
|
17
18
|
import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
|
|
18
19
|
import type { RetrievalResult } from "./retriever.js";
|
|
@@ -65,7 +66,16 @@ export function registerCoreTools(deps: ToolRegistryDeps): void {
|
|
|
65
66
|
|
|
66
67
|
async function saveManagedObservation(observation: Parameters<typeof buildWorkflowObservationRecord>[0]): Promise<void> {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
// P0 join key:managed observation 是最该带它的一类——`resume_context` 本身
|
|
70
|
+
// 就是「读了一批记忆」的动作,它的成败与那批记忆直接相关。
|
|
71
|
+
// (api-server 那份同名函数刻意不加:HTTP server 一个进程服务多个客户端,
|
|
72
|
+
// 「一进程 ≈ 一会话」的前提在那里不成立,加了只会 join 出假配对。)
|
|
73
|
+
const recalledIds = recentRecallHits();
|
|
74
|
+
const record = buildWorkflowObservationRecord({
|
|
75
|
+
readerId: PROCESS_READER_ID,
|
|
76
|
+
...(recalledIds.length > 0 ? { recalledIds } : {}),
|
|
77
|
+
...(observation as Record<string, unknown>),
|
|
78
|
+
});
|
|
69
79
|
await workflowObservationStore.save(record);
|
|
70
80
|
} catch (error) {
|
|
71
81
|
console.error("[RecallNest MCP] Failed to persist managed workflow observation:", error);
|
|
@@ -8,13 +8,20 @@ import { formatConflictAudit, formatConflictClusters, formatConflictEscalation,
|
|
|
8
8
|
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
9
9
|
import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
|
|
10
10
|
import { archiveDirtyBriefAsset, listDirtyBriefAssets } from "./memory-assets.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
scanMemoryPromotions,
|
|
13
|
+
scanSynthesisPromotions,
|
|
14
|
+
buildPromoteScanDeps,
|
|
15
|
+
formatPromoteScanResult,
|
|
16
|
+
formatSynthesisPromoteResult,
|
|
17
|
+
} from "./memory-promotion.js";
|
|
12
18
|
import { aliasMapFilePath, expandQuery, explainUserAliases, listUserAliases, removeUserAlias, upsertUserAlias } from "./query-expander.js";
|
|
13
19
|
import { recordSkillOutcome } from "./skill-engine.js";
|
|
14
20
|
import { scanForPromotions, formatPromotionResult } from "./skill-promotion.js";
|
|
15
21
|
import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
|
|
16
22
|
import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
|
|
17
23
|
import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.js";
|
|
24
|
+
import { PROCESS_READER_ID, recentRecallHits } from "./recall-ledger.js";
|
|
18
25
|
import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
|
|
19
26
|
import { withLock } from "./distill-lock.js";
|
|
20
27
|
|
|
@@ -36,8 +43,14 @@ registerTool(
|
|
|
36
43
|
tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools involved"),
|
|
37
44
|
skillId: z.string().min(1).max(128).optional().describe("Optional skill id to bump. When present and outcome is success, the skill's successCount increments; otherwise failureCount increments. Missing/corrupt skills are skipped silently (does not block observation write)."),
|
|
38
45
|
idempotencyKey: z.string().min(1).max(160).optional().describe("Optional stable request key; repeated saves with the same key replace the prior observation"),
|
|
46
|
+
recalledIds: z.array(z.string().min(1).max(128)).max(64).optional().describe("Optional memory ids this task actually used. Omit to auto-fill from what this session retrieved in the last 30 minutes — that auto-fill is what makes (memory, outcome) value back-propagation possible."),
|
|
39
47
|
},
|
|
40
|
-
async ({ workflowId, outcome, summary, scope, source, signal, task, tags, tools, skillId, idempotencyKey }) => {
|
|
48
|
+
async ({ workflowId, outcome, summary, scope, source, signal, task, tags, tools, skillId, idempotencyKey, recalledIds }) => {
|
|
49
|
+
// P0 join key:不传就用本进程的检索账本自动补。这一步是价值回溯的前提——
|
|
50
|
+
// 少了它,库里就只剩「有结果没参与者」和「有参与者没结果」两堆对不上的数据。
|
|
51
|
+
const resolvedRecalledIds = recalledIds && recalledIds.length > 0
|
|
52
|
+
? recalledIds
|
|
53
|
+
: recentRecallHits();
|
|
41
54
|
const record = buildWorkflowObservationRecord({
|
|
42
55
|
workflowId,
|
|
43
56
|
outcome,
|
|
@@ -50,6 +63,8 @@ registerTool(
|
|
|
50
63
|
tools,
|
|
51
64
|
skillId,
|
|
52
65
|
idempotencyKey,
|
|
66
|
+
readerId: PROCESS_READER_ID,
|
|
67
|
+
...(resolvedRecalledIds.length > 0 ? { recalledIds: resolvedRecalledIds } : {}),
|
|
53
68
|
});
|
|
54
69
|
const stored = await workflowObservationStore.save(record);
|
|
55
70
|
|
|
@@ -332,6 +347,7 @@ registerTool(
|
|
|
332
347
|
getById: store.getById.bind(store),
|
|
333
348
|
vectorSearch: store.vectorSearch.bind(store),
|
|
334
349
|
update: async () => null, // no-op in dry-run
|
|
350
|
+
patchMetadataBatch: async () => 0, // no-op in dry-run(3a 批量写通道,2026-08-14)
|
|
335
351
|
};
|
|
336
352
|
const engine = new ConsolidationEngine(readOnlyStore, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries }, getKGStore());
|
|
337
353
|
const result = await engine.run(scope);
|
|
@@ -377,11 +393,13 @@ registerTool(
|
|
|
377
393
|
{
|
|
378
394
|
scope: z.string().min(1).max(160).describe("Project scope to scan for promotion candidates"),
|
|
379
395
|
minOccurrences: z.number().min(2).max(20).default(3).describe("Minimum similar cases to trigger a promotion suggestion"),
|
|
396
|
+
minDistinctSources: z.number().min(1).max(10).default(2).describe("How many DIFFERENT sources (episodes) a cluster must span. Counting similar entries is not the same as counting distinct experiences — the same pothole hit three times is one experience, not a rule. Set 1 to disable this check. Abstains (never rejects) when the entries carry no source pointer."),
|
|
380
397
|
},
|
|
381
|
-
async ({ scope, minOccurrences }) => {
|
|
398
|
+
async ({ scope, minOccurrences, minDistinctSources }) => {
|
|
382
399
|
const { store } = getComponents();
|
|
383
400
|
const result = await scanForPromotions(store, scope, {
|
|
384
401
|
minCaseOccurrences: minOccurrences,
|
|
402
|
+
minDistinctSources,
|
|
385
403
|
});
|
|
386
404
|
|
|
387
405
|
return {
|
|
@@ -472,4 +490,28 @@ registerTool(
|
|
|
472
490
|
};
|
|
473
491
|
},
|
|
474
492
|
);
|
|
493
|
+
|
|
494
|
+
registerTool(
|
|
495
|
+
"promote_synthesis",
|
|
496
|
+
"Scan dream-synthesized conclusions (cluster insights and cross-memory patterns, which are written on the evidence layer and therefore excluded from stable memory) and promote the well-supported ones into a durable copy. Eligibility comes from the conclusion's own validated evidence set, not from repetition. Default dry-run (no writes); when dryRun=false it writes durable entries through the same promote path as promote_memory, so canonicalKey dedup makes re-runs a revision rather than a duplicate. The synthesized row itself is never modified.",
|
|
497
|
+
{
|
|
498
|
+
scope: z.string().min(1).max(160).describe("Scope to scan, e.g. memory"),
|
|
499
|
+
dryRun: z.boolean().default(true).describe("When true (default), only report candidates without writing"),
|
|
500
|
+
minDistinctEvidence: z.number().int().min(2).max(20).default(2).describe("Min distinct, still-active evidence memories behind the conclusion"),
|
|
501
|
+
minImportance: z.number().min(0).max(1).default(0.6).describe("Min importance of the synthesized conclusion itself"),
|
|
502
|
+
},
|
|
503
|
+
async ({ scope, dryRun, minDistinctEvidence, minImportance }) => {
|
|
504
|
+
const { store, embedder } = getComponents();
|
|
505
|
+
const kgExtractor = getKGExtractor();
|
|
506
|
+
const deps = buildPromoteScanDeps({ store, embedder, conflictStore, kgExtractor });
|
|
507
|
+
const result = await scanSynthesisPromotions(deps, scope, { dryRun, minDistinctEvidence, minImportance });
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
content: [{
|
|
511
|
+
type: "text" as const,
|
|
512
|
+
text: formatSynthesisPromoteResult(result),
|
|
513
|
+
}],
|
|
514
|
+
};
|
|
515
|
+
},
|
|
516
|
+
);
|
|
475
517
|
}
|
package/src/memory-evolution.ts
CHANGED
|
@@ -115,6 +115,22 @@ export function patchEvolution(
|
|
|
115
115
|
return JSON.stringify(parsed);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* patchEvolution 的对象版:给 patchMetadataBatch 的 patchFn 用(那条通道进出都是
|
|
120
|
+
* 已解析的 metadata 对象,不该为了复用字符串版而反复 stringify/parse)。
|
|
121
|
+
* 就地修改并返回同一对象,语义与 patchEvolution 一致(浅合并 evolution 子对象)。
|
|
122
|
+
*/
|
|
123
|
+
export function patchEvolutionOnMeta(
|
|
124
|
+
meta: Record<string, unknown>,
|
|
125
|
+
patch: Partial<EvolutionMetadata>,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
const existing = meta.evolution !== null && typeof meta.evolution === "object" && !Array.isArray(meta.evolution)
|
|
128
|
+
? (meta.evolution as Partial<EvolutionMetadata>)
|
|
129
|
+
: undefined;
|
|
130
|
+
meta.evolution = { ...existing, ...patch };
|
|
131
|
+
return meta;
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
// ---------------------------------------------------------------------------
|
|
119
135
|
// Predicates
|
|
120
136
|
// ---------------------------------------------------------------------------
|
package/src/memory-output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatAgeLabel } from "./age-label.js";
|
|
1
2
|
import type { MemoryEntry } from "./store.js";
|
|
2
3
|
import type { RetrievalResult } from "./retriever.js";
|
|
3
4
|
import type { RetrievalProfileName } from "./retrieval-profiles.js";
|
|
@@ -41,6 +42,7 @@ export interface DistilledEvidence {
|
|
|
41
42
|
source: string;
|
|
42
43
|
scope: string;
|
|
43
44
|
date: string;
|
|
45
|
+
age: string;
|
|
44
46
|
retrievalPath: string;
|
|
45
47
|
snippet: string;
|
|
46
48
|
}
|
|
@@ -82,6 +84,37 @@ function getTierLabel(result: RetrievalResult): string {
|
|
|
82
84
|
return String(meta.tier || "peripheral");
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* 所在 session 里有用户贴图时,多给一行提示。
|
|
89
|
+
*
|
|
90
|
+
* 图本体不在库里,正文里通常也没有对图的描述——这一行是唯一能让读到这条
|
|
91
|
+
* 记忆的人意识到「这附近还有图没看」的信号。判断值不值得看,回 Deja 按
|
|
92
|
+
* session 读原文。
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ 这是 **session 级**标记,不是「本条记忆有图」:同一个 session 切出来
|
|
95
|
+
* 的每条记忆都带同一个数,图未必跟眼前这条有关。粒度换覆盖,明知而为——
|
|
96
|
+
* 图所在的那一轮常因正文太短根本没进库,只有盖到同 session 的其他轮次上
|
|
97
|
+
* 才可能被搜到。
|
|
98
|
+
*
|
|
99
|
+
* 无图时返回 null,调用方据此不输出这一行——不给无图记忆添噪音。
|
|
100
|
+
*/
|
|
101
|
+
function formatSessionImages(result: RetrievalResult): string | null {
|
|
102
|
+
const meta = parseMetadata(result.entry);
|
|
103
|
+
const own = typeof meta.sessionImages === "number" ? meta.sessionImages : 0;
|
|
104
|
+
const tool = typeof meta.sessionToolImages === "number" ? meta.sessionToolImages : 0;
|
|
105
|
+
if (own <= 0 && tool <= 0) return null;
|
|
106
|
+
|
|
107
|
+
// 两类分开报,因为它们答的是不同的问题:找「我发过的那张截图」看前者,
|
|
108
|
+
// 回溯「我当时生成的图 / 那个页面当时的样子」看后者。
|
|
109
|
+
// 措辞用英文,与同一块里的 prov / fresh 行保持一致(这个输出面是英文的)。
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
if (own > 0) parts.push(`${own} user-pasted`);
|
|
112
|
+
if (tool > 0) parts.push(`${tool} agent-made`);
|
|
113
|
+
const sess = typeof meta.sessionId === "string" ? meta.sessionId.slice(0, 8) : "?";
|
|
114
|
+
|
|
115
|
+
return `${parts.join(", ")} in this session · read sess=${sess}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
85
118
|
function getProvenanceSummary(result: RetrievalResult): string {
|
|
86
119
|
const provenance = extractMemoryProvenance({
|
|
87
120
|
scope: result.entry.scope,
|
|
@@ -332,6 +365,7 @@ function buildSearchRow(index: number, query: string, result: RetrievalResult):
|
|
|
332
365
|
getTierLabel(result).padEnd(10),
|
|
333
366
|
getSourceLabel(result).padEnd(7),
|
|
334
367
|
getDateLabel(result.entry.timestamp),
|
|
368
|
+
formatAgeLabel(result.entry.timestamp).padEnd(5),
|
|
335
369
|
getRetrievalPath(result).padEnd(20),
|
|
336
370
|
getFileLabel(result),
|
|
337
371
|
cleanSnippet(pickBestSnippet(query, result.entry.text), 120),
|
|
@@ -374,14 +408,16 @@ export function formatFullResults(
|
|
|
374
408
|
`Profile : ${context.profile}`,
|
|
375
409
|
`Hits : ${results.length}`,
|
|
376
410
|
"",
|
|
377
|
-
"# ID Score Category Tier Source Date Retrieval Path File / Snippet",
|
|
378
|
-
"-- -------- ----- ------------ ---------- ------- ---------- -------------------- --------------",
|
|
411
|
+
"# ID Score Category Tier Source Date Age Retrieval Path File / Snippet",
|
|
412
|
+
"-- -------- ----- ------------ ---------- ------- ---------- ----- -------------------- --------------",
|
|
379
413
|
];
|
|
380
414
|
|
|
381
415
|
for (let i = 0; i < results.length; i++) {
|
|
382
416
|
const row = buildSearchRow(i, context.query, results[i]);
|
|
383
|
-
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} | ${row[
|
|
417
|
+
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} ${row[9]} | ${row[10]}`);
|
|
384
418
|
lines.push(` prov : ${getProvenanceSummary(results[i])}`);
|
|
419
|
+
const imgs = formatSessionImages(results[i]);
|
|
420
|
+
if (imgs) lines.push(` imgs : ${imgs}`);
|
|
385
421
|
// Full mode: append metadata details
|
|
386
422
|
const meta = parseMetadata(results[i].entry);
|
|
387
423
|
const evolution = typeof meta.evolutionStatus === "string" ? meta.evolutionStatus : "-";
|
|
@@ -424,14 +460,16 @@ export function formatSearchResults(
|
|
|
424
460
|
`Profile : ${context.profile}`,
|
|
425
461
|
`Hits : ${results.length}`,
|
|
426
462
|
"",
|
|
427
|
-
"# ID Score Category Tier Source Date Retrieval Path File / Snippet",
|
|
428
|
-
"-- -------- ----- ------------ ---------- ------- ---------- -------------------- --------------",
|
|
463
|
+
"# ID Score Category Tier Source Date Age Retrieval Path File / Snippet",
|
|
464
|
+
"-- -------- ----- ------------ ---------- ------- ---------- ----- -------------------- --------------",
|
|
429
465
|
];
|
|
430
466
|
|
|
431
467
|
for (let i = 0; i < results.length; i++) {
|
|
432
468
|
const row = buildSearchRow(i, context.query, results[i]);
|
|
433
|
-
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} | ${row[
|
|
469
|
+
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} ${row[9]} | ${row[10]}`);
|
|
434
470
|
lines.push(` prov : ${getProvenanceSummary(results[i])}`);
|
|
471
|
+
const imgs = formatSessionImages(results[i]);
|
|
472
|
+
if (imgs) lines.push(` imgs : ${imgs}`);
|
|
435
473
|
// Freshness: only shown for memories that declared dependsOn (opt-in, cheap check).
|
|
436
474
|
const fresh = evaluateEntryFreshness(results[i].entry.metadata, freshnessCache);
|
|
437
475
|
if (fresh) lines.push(` fresh: ${fresh}`);
|
|
@@ -529,7 +567,7 @@ export function formatExplainResults(
|
|
|
529
567
|
const session = getSessionLabel(result);
|
|
530
568
|
const why = buildWhyMatched(context.query, result);
|
|
531
569
|
|
|
532
|
-
lines.push(`${i + 1}. ${result.entry.id.slice(0, 8)} | ${score} | ${getSourceLabel(result)} | ${getDateLabel(result.entry.timestamp)}`);
|
|
570
|
+
lines.push(`${i + 1}. ${result.entry.id.slice(0, 8)} | ${score} | ${getSourceLabel(result)} | ${getDateLabel(result.entry.timestamp)} (${formatAgeLabel(result.entry.timestamp)})`);
|
|
533
571
|
lines.push(` category: ${getCategoryLabel(result)}`);
|
|
534
572
|
lines.push(` tier : ${getTierLabel(result)}`);
|
|
535
573
|
lines.push(` path : ${retrieval}`);
|
|
@@ -592,6 +630,7 @@ export function summarizeResults(
|
|
|
592
630
|
source: getSourceLabel(result),
|
|
593
631
|
scope: result.entry.scope,
|
|
594
632
|
date: getDateLabel(result.entry.timestamp),
|
|
633
|
+
age: formatAgeLabel(result.entry.timestamp),
|
|
595
634
|
retrievalPath: getRetrievalPath(result),
|
|
596
635
|
snippet: pickBestSnippet(context.query, normalizeRecallText(result)),
|
|
597
636
|
});
|
|
@@ -654,7 +693,7 @@ export function distillResults(
|
|
|
654
693
|
|
|
655
694
|
lines.push("", "Evidence");
|
|
656
695
|
summary.evidence.forEach((item, index) => {
|
|
657
|
-
lines.push(`${index + 1}. ${item.source} | ${item.date} | ${item.retrievalPath} | ${item.snippet}`);
|
|
696
|
+
lines.push(`${index + 1}. ${item.source} | ${item.date} (${item.age}) | ${item.retrievalPath} | ${item.snippet}`);
|
|
658
697
|
});
|
|
659
698
|
|
|
660
699
|
lines.push("", "Reusable Memory Candidates");
|
package/src/memory-promotion.ts
CHANGED
|
@@ -244,3 +244,283 @@ export function formatPromoteScanResult(result: PromoteScanResult): string {
|
|
|
244
244
|
}
|
|
245
245
|
return lines.join("\n");
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Synthesis promotion: evidence → durable for dream-synthesized conclusions
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The layering this implements is "evidence retained → current projection → eligible for
|
|
254
|
+
* recall → ranked result". `scanMemoryPromotions` above covers one road into the durable
|
|
255
|
+
* layer: transcript-downgraded facts that recur often enough to be worth keeping. This
|
|
256
|
+
* covers the other one, which had no road at all.
|
|
257
|
+
*
|
|
258
|
+
* `buildDerivedBoundary` (consolidation-engine) stamps every dream-synthesized insight and
|
|
259
|
+
* cross-memory pattern as `layer: "evidence"`, deliberately — a model re-reading its own
|
|
260
|
+
* memories is a lead to its sources, not authority over them. But
|
|
261
|
+
* `shouldUseStableMemoryResult` refuses anything on the evidence layer, so the consequence
|
|
262
|
+
* was absolute: **a synthesized conclusion could never take part in stable memory, no
|
|
263
|
+
* matter how well supported it was.** The synthesis contract added in `efdb043` made those
|
|
264
|
+
* conclusions checkable (a verdict may abstain, and evidence ids are validated against the
|
|
265
|
+
* cluster) without giving them anywhere to go.
|
|
266
|
+
*
|
|
267
|
+
* ## Why the gate is not the one above
|
|
268
|
+
*
|
|
269
|
+
* The recurrence gate — "the same fact showed up in N separate places" — is the wrong
|
|
270
|
+
* question to ask a synthesis. A synthesis is *already* a cross-entry aggregate; demanding
|
|
271
|
+
* that the dream produce the same conclusion three times would be counting one thing three
|
|
272
|
+
* ways. What it carries instead is its own support set, so that is what gets checked:
|
|
273
|
+
* enough distinct sources, still live, under a contract version that validated them.
|
|
274
|
+
*
|
|
275
|
+
* ## What promotion does and does not touch
|
|
276
|
+
*
|
|
277
|
+
* The synthesis row is never modified — archive-first, and the evidence has to stay
|
|
278
|
+
* readable as evidence. Promotion writes a *separate* durable entry through the same
|
|
279
|
+
* `promoteMemory` path everything else uses, so it inherits canonicalKey dedup (re-running
|
|
280
|
+
* revises rather than duplicating), secret redaction, and `promotedFrom` provenance
|
|
281
|
+
* pointing back at the synthesis. The durable copy is what later recall can lean on.
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/** Synthesis contract version required before a derived row is eligible. */
|
|
285
|
+
export const MIN_PROMOTABLE_CONTRACT_VERSION = 2;
|
|
286
|
+
|
|
287
|
+
export interface SynthesisPromoteScanConfig {
|
|
288
|
+
/** Min distinct, still-live evidence memories behind the conclusion (default 2). */
|
|
289
|
+
minDistinctEvidence: number;
|
|
290
|
+
/** Min importance of the synthesis itself (default 0.6). */
|
|
291
|
+
minImportance: number;
|
|
292
|
+
/** Max derived rows fetched from the store (default 2000). */
|
|
293
|
+
listLimit: number;
|
|
294
|
+
/** When true (default), report candidates without writing anything. */
|
|
295
|
+
dryRun: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const DEFAULT_SYNTHESIS_PROMOTE_CONFIG: SynthesisPromoteScanConfig = {
|
|
299
|
+
minDistinctEvidence: 2,
|
|
300
|
+
minImportance: 0.6,
|
|
301
|
+
listLimit: 2000,
|
|
302
|
+
dryRun: true,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** Why an otherwise-derived row was not promoted. Reported, never silent. */
|
|
306
|
+
export type SynthesisSkipReason =
|
|
307
|
+
| "pre_contract"
|
|
308
|
+
| "already_promoted"
|
|
309
|
+
| "evidence_unresolvable"
|
|
310
|
+
| "too_few_distinct_evidence"
|
|
311
|
+
| "below_importance";
|
|
312
|
+
|
|
313
|
+
export interface SynthesisPromoteCandidate {
|
|
314
|
+
seedId: string;
|
|
315
|
+
seedText: string;
|
|
316
|
+
/** Durable category the conclusion is promoted into (from boundary.originalCategory). */
|
|
317
|
+
category: DurableMemoryCategory;
|
|
318
|
+
contractVersion: number;
|
|
319
|
+
/** Distinct evidence ids that are still active in the store. */
|
|
320
|
+
evidenceIds: string[];
|
|
321
|
+
importance: number;
|
|
322
|
+
/** Filled after a real promotion; null in dryRun mode. */
|
|
323
|
+
promoted: StoredPromotedMemoryRecord | null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface SynthesisPromoteScanResult {
|
|
327
|
+
candidates: SynthesisPromoteCandidate[];
|
|
328
|
+
/** Derived rows examined (carrying a distillation boundary), before gating. */
|
|
329
|
+
scannedSyntheses: number;
|
|
330
|
+
/** Per-reason counts for everything examined and rejected. */
|
|
331
|
+
skipped: Record<SynthesisSkipReason, number>;
|
|
332
|
+
promoted: number;
|
|
333
|
+
dryRun: boolean;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface SynthesisPromoteScanDeps {
|
|
337
|
+
store: Pick<MemoryStore, "list"> & {
|
|
338
|
+
getById?(id: string): Promise<MemoryEntry | null>;
|
|
339
|
+
get?(id: string): Promise<MemoryEntry | null>;
|
|
340
|
+
};
|
|
341
|
+
promote: PromoteFn;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readContractVersion(metadata?: string): number | null {
|
|
345
|
+
const parsed = parseMetadataObject(metadata);
|
|
346
|
+
const raw = parsed?.synthesis_contract;
|
|
347
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Ids the model itself named as support, deduplicated and order-preserved. */
|
|
351
|
+
function readEvidenceIds(metadata?: string): string[] | null {
|
|
352
|
+
const parsed = parseMetadataObject(metadata);
|
|
353
|
+
const raw = parsed?.evidenceMemories;
|
|
354
|
+
if (!Array.isArray(raw)) return null;
|
|
355
|
+
const ids = raw.filter((id): id is string => typeof id === "string" && id.trim().length > 0);
|
|
356
|
+
return [...new Set(ids)];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A dream-synthesized derivative — evidence layer written by the distillation path. */
|
|
360
|
+
function isSynthesisRow(entry: MemoryEntry): boolean {
|
|
361
|
+
const boundary = extractBoundaryMetadata(entry.metadata);
|
|
362
|
+
return boundary?.layer === "evidence" && boundary.authority === "distillation";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function loadEntry(
|
|
366
|
+
deps: SynthesisPromoteScanDeps,
|
|
367
|
+
id: string,
|
|
368
|
+
): Promise<MemoryEntry | null> {
|
|
369
|
+
if (deps.store.getById) return deps.store.getById(id);
|
|
370
|
+
if (deps.store.get) return deps.store.get(id);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Find synthesized conclusions whose own support set justifies a durable copy.
|
|
376
|
+
*
|
|
377
|
+
* Abstains rather than guesses: a row whose evidence ids cannot be resolved at all is
|
|
378
|
+
* counted under `evidence_unresolvable` and left alone. That mirrors the abstention path
|
|
379
|
+
* in `scanForPromotions`' cross-source check — when the provenance needed for the judgement
|
|
380
|
+
* is missing, not judging is the correct answer, not judging leniently.
|
|
381
|
+
*/
|
|
382
|
+
export async function scanSynthesisPromotions(
|
|
383
|
+
deps: SynthesisPromoteScanDeps,
|
|
384
|
+
scope: string,
|
|
385
|
+
config?: Partial<SynthesisPromoteScanConfig>,
|
|
386
|
+
): Promise<SynthesisPromoteScanResult> {
|
|
387
|
+
const cfg: SynthesisPromoteScanConfig = { ...DEFAULT_SYNTHESIS_PROMOTE_CONFIG, ...config };
|
|
388
|
+
|
|
389
|
+
const result: SynthesisPromoteScanResult = {
|
|
390
|
+
candidates: [],
|
|
391
|
+
scannedSyntheses: 0,
|
|
392
|
+
skipped: {
|
|
393
|
+
pre_contract: 0,
|
|
394
|
+
already_promoted: 0,
|
|
395
|
+
evidence_unresolvable: 0,
|
|
396
|
+
too_few_distinct_evidence: 0,
|
|
397
|
+
below_importance: 0,
|
|
398
|
+
},
|
|
399
|
+
promoted: 0,
|
|
400
|
+
dryRun: cfg.dryRun,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Synthesized insights take the cluster's majority category, so this cannot be narrowed
|
|
404
|
+
// to one category the way the downgraded-evidence scan is.
|
|
405
|
+
const rows = await deps.store.list([scope], undefined, cfg.listLimit, 0);
|
|
406
|
+
const syntheses = rows.filter((e) => isActiveMemory(e.metadata) && isSynthesisRow(e));
|
|
407
|
+
result.scannedSyntheses = syntheses.length;
|
|
408
|
+
|
|
409
|
+
for (const entry of syntheses) {
|
|
410
|
+
const contractVersion = readContractVersion(entry.metadata);
|
|
411
|
+
// Derivatives written before the synthesis contract were never validated — the whole
|
|
412
|
+
// point of the version stamp is that "which code produced this" stops being a guess.
|
|
413
|
+
if (contractVersion === null || contractVersion < MIN_PROMOTABLE_CONTRACT_VERSION) {
|
|
414
|
+
result.skipped.pre_contract += 1;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (hasPromotedToMarker(entry.metadata)) {
|
|
419
|
+
result.skipped.already_promoted += 1;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const declared = readEvidenceIds(entry.metadata);
|
|
424
|
+
if (declared === null || declared.length === 0) {
|
|
425
|
+
result.skipped.evidence_unresolvable += 1;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Evidence that has since been superseded or archived no longer supports anything.
|
|
430
|
+
const resolved: string[] = [];
|
|
431
|
+
let lookupFailed = false;
|
|
432
|
+
for (const id of declared) {
|
|
433
|
+
const source = await loadEntry(deps, id);
|
|
434
|
+
if (source === null) {
|
|
435
|
+
lookupFailed = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (isActiveMemory(source.metadata)) resolved.push(id);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Cannot see any of the support: abstain instead of promoting on an unchecked claim.
|
|
442
|
+
if (resolved.length === 0 && lookupFailed) {
|
|
443
|
+
result.skipped.evidence_unresolvable += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (resolved.length < cfg.minDistinctEvidence) {
|
|
448
|
+
result.skipped.too_few_distinct_evidence += 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if ((entry.importance ?? 0) < cfg.minImportance) {
|
|
453
|
+
result.skipped.below_importance += 1;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const boundary = extractBoundaryMetadata(entry.metadata);
|
|
458
|
+
const category = (boundary?.originalCategory ?? "events") as DurableMemoryCategory;
|
|
459
|
+
|
|
460
|
+
const candidate: SynthesisPromoteCandidate = {
|
|
461
|
+
seedId: entry.id,
|
|
462
|
+
seedText: entry.text,
|
|
463
|
+
category,
|
|
464
|
+
contractVersion,
|
|
465
|
+
evidenceIds: resolved,
|
|
466
|
+
importance: entry.importance,
|
|
467
|
+
promoted: null,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (!cfg.dryRun) {
|
|
471
|
+
candidate.promoted = await deps.promote({
|
|
472
|
+
memoryId: entry.id,
|
|
473
|
+
category,
|
|
474
|
+
scope,
|
|
475
|
+
importance: entry.importance,
|
|
476
|
+
});
|
|
477
|
+
result.promoted += 1;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
result.candidates.push(candidate);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function formatSynthesisPromoteResult(result: SynthesisPromoteScanResult): string {
|
|
487
|
+
const skippedTotal = Object.values(result.skipped).reduce((a, b) => a + b, 0);
|
|
488
|
+
const lines = [
|
|
489
|
+
`Synthesis promote-scan: ${result.scannedSyntheses} synthesized conclusion(s) examined, ${skippedTotal} skipped.`,
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
// Always print the rejection breakdown: a scan that promotes nothing should still say
|
|
493
|
+
// why, or "nothing found" and "everything filtered" look the same from outside.
|
|
494
|
+
const reasons = (Object.entries(result.skipped) as Array<[SynthesisSkipReason, number]>)
|
|
495
|
+
.filter(([, n]) => n > 0)
|
|
496
|
+
.map(([reason, n]) => `${reason}=${n}`);
|
|
497
|
+
if (reasons.length > 0) lines.push(`Skipped: ${reasons.join(", ")}`);
|
|
498
|
+
|
|
499
|
+
if (result.candidates.length === 0) {
|
|
500
|
+
lines.push("No promotion candidates found.");
|
|
501
|
+
return lines.join("\n");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
lines.push(
|
|
505
|
+
result.dryRun
|
|
506
|
+
? `Found ${result.candidates.length} candidate(s) (dry-run, nothing written):\n`
|
|
507
|
+
: `Promoted ${result.promoted} of ${result.candidates.length} candidate(s):\n`,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
for (const [i, c] of result.candidates.entries()) {
|
|
511
|
+
lines.push(`### ${i + 1}. [${c.category}] ${c.seedText.split("\n")[0].slice(0, 80)}`);
|
|
512
|
+
lines.push(
|
|
513
|
+
`Evidence: ${c.evidenceIds.length} distinct (${c.evidenceIds.map((id) => id.slice(0, 8)).join(", ")})`,
|
|
514
|
+
);
|
|
515
|
+
lines.push(`Contract: v${c.contractVersion} | importance: ${c.importance.toFixed(2)} | seed: ${c.seedId.slice(0, 8)}`);
|
|
516
|
+
if (c.promoted) {
|
|
517
|
+
lines.push(
|
|
518
|
+
c.promoted.disposition === "conflict" && c.promoted.conflictId
|
|
519
|
+
? `-> conflict ${c.promoted.conflictId.slice(0, 8)} (manual review)`
|
|
520
|
+
: `-> durable ${c.promoted.id.slice(0, 8)} (${c.promoted.disposition}, key ${c.promoted.canonicalKey})`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
lines.push("");
|
|
524
|
+
}
|
|
525
|
+
return lines.join("\n");
|
|
526
|
+
}
|
package/src/memory-store-port.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MemoryEntry, MemorySearchResult } from "./store.js";
|
|
2
|
+
import type { ScopeMatchMode } from "./scope-policy.js";
|
|
2
3
|
|
|
3
4
|
export interface MemoryStoreStats {
|
|
4
5
|
totalCount: number;
|
|
@@ -18,8 +19,16 @@ export type MemoryStoreUpdate = {
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export interface MemoryStorePort {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/** scopeMatch 见 scope-policy.ts 的 ScopeMatchMode:缺省 "family"(历史行为,
|
|
23
|
+
* 无冒号 scope 按前缀),"exact" 由调用方在"我给的是一个具体 scope 名"时显式声明。 */
|
|
24
|
+
stats(scopeFilter?: string[], scopeMatch?: ScopeMatchMode): Promise<MemoryStoreStats>;
|
|
25
|
+
list(
|
|
26
|
+
scopeFilter?: string[],
|
|
27
|
+
category?: string,
|
|
28
|
+
limit?: number,
|
|
29
|
+
offset?: number,
|
|
30
|
+
scopeMatch?: ScopeMatchMode,
|
|
31
|
+
): Promise<MemoryEntry[]>;
|
|
23
32
|
/** 按 id 批量取真实向量。list()/listPage() 为性能恒返回 vector:[](假空数组),
|
|
24
33
|
* 任何要做相似度/聚类的消费者必须用这个回填——promote_scan 与 dream 3b 都栽过同一坑。 */
|
|
25
34
|
getVectors(ids: string[]): Promise<Map<string, number[]>>;
|
|
@@ -42,5 +51,25 @@ export interface MemoryStorePort {
|
|
|
42
51
|
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
|
|
43
52
|
scopeFilter?: string[],
|
|
44
53
|
): Promise<MemoryEntry | null>;
|
|
45
|
-
|
|
54
|
+
/** metadata 批量读改写:单锁单 commit 写一批行(≤200/批)。patchFn 在锁内以库中
|
|
55
|
+
* 最新行为底座、按数组序依次应用(同批闭包可传递决策)。单条失败跳过不炸整批,
|
|
56
|
+
* 返回成功写入数。dream 3a/3b/auto-gc 的写放大治理入口(2026-08-14)。 */
|
|
57
|
+
patchMetadataBatch(
|
|
58
|
+
patches: Array<{
|
|
59
|
+
id: string;
|
|
60
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>;
|
|
61
|
+
}>,
|
|
62
|
+
scopeFilter?: string[],
|
|
63
|
+
): Promise<number>;
|
|
64
|
+
/** Remove version-group metadata from groups with only one surviving member.
|
|
65
|
+
* Implementations should take one membership snapshot and apply repairs under
|
|
66
|
+
* the same write lock so concurrent group creation cannot invalidate the plan. */
|
|
67
|
+
repairSingletonVersionGroups(): Promise<number>;
|
|
68
|
+
vectorSearch(
|
|
69
|
+
vector: number[],
|
|
70
|
+
limit?: number,
|
|
71
|
+
minScore?: number,
|
|
72
|
+
scopeFilter?: string[],
|
|
73
|
+
scopeMatch?: ScopeMatchMode,
|
|
74
|
+
): Promise<MemorySearchResult[]>;
|
|
46
75
|
}
|