recallnest 2.3.0 → 2.6.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 +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/api-server-security.test.ts +102 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +116 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +945 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +262 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +129 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +780 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +282 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/noise-prototype-bank.test.ts +280 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/ui-server-security.test.ts +93 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ALIAS_RULES, expandQueryWithAliases, explainAliasExpansion } from "./aliases.js";
|
|
3
|
+
import { buildConflictAuditSummary, clusterConflicts } from "./conflict-advisor.js";
|
|
4
|
+
import { resolveConflictCandidate } from "./conflict-engine.js";
|
|
5
|
+
import { escalateConflicts } from "./conflict-escalation.js";
|
|
6
|
+
import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
|
|
7
|
+
import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
|
|
8
|
+
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
9
|
+
import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
|
|
10
|
+
import { archiveDirtyBriefAsset, listDirtyBriefAssets } from "./memory-assets.js";
|
|
11
|
+
import { scanMemoryPromotions, buildPromoteScanDeps, formatPromoteScanResult } from "./memory-promotion.js";
|
|
12
|
+
import { aliasMapFilePath, expandQuery, explainUserAliases, listUserAliases, removeUserAlias, upsertUserAlias } from "./query-expander.js";
|
|
13
|
+
import { recordSkillOutcome } from "./skill-engine.js";
|
|
14
|
+
import { scanForPromotions, formatPromotionResult } from "./skill-promotion.js";
|
|
15
|
+
import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
|
|
16
|
+
import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
|
|
17
|
+
import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.js";
|
|
18
|
+
import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
|
|
19
|
+
import { withLock } from "./distill-lock.js";
|
|
20
|
+
|
|
21
|
+
export function registerGovernanceTools(deps: ToolRegistryDeps): void {
|
|
22
|
+
const { registerTool, getComponents, conflictStore, workflowObservationStore, getKGExtractor, getKGStore } = deps;
|
|
23
|
+
|
|
24
|
+
registerTool(
|
|
25
|
+
"workflow_observe",
|
|
26
|
+
"Store an append-only workflow observation for self-evolution. Use this to record whether a continuity primitive or reusable workflow succeeded, failed, was corrected by the user, or was missed entirely. Optionally pass `skillId` to also bump that skill's successCount/failureCount — the canonical way to close the skill feedback loop.",
|
|
27
|
+
{
|
|
28
|
+
workflowId: z.string().min(1).max(120).describe("Workflow primitive id, such as resume_context or checkpoint_session"),
|
|
29
|
+
outcome: WorkflowObservationOutcomeSchema.default("success").describe("success | failure | corrected | missed"),
|
|
30
|
+
summary: z.string().min(1).max(400).describe("Short description of what happened"),
|
|
31
|
+
scope: z.string().min(1).max(160).optional().describe("Optional scope such as project:recallnest"),
|
|
32
|
+
source: z.string().min(1).max(40).default("agent").describe("Source label such as agent, smoke, eval, or manual"),
|
|
33
|
+
signal: z.string().min(1).max(120).optional().describe("Optional failure/correction signal tag"),
|
|
34
|
+
task: z.string().min(1).max(240).optional().describe("Optional related task"),
|
|
35
|
+
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
36
|
+
tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools involved"),
|
|
37
|
+
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
|
+
idempotencyKey: z.string().min(1).max(160).optional().describe("Optional stable request key; repeated saves with the same key replace the prior observation"),
|
|
39
|
+
},
|
|
40
|
+
async ({ workflowId, outcome, summary, scope, source, signal, task, tags, tools, skillId, idempotencyKey }) => {
|
|
41
|
+
const record = buildWorkflowObservationRecord({
|
|
42
|
+
workflowId,
|
|
43
|
+
outcome,
|
|
44
|
+
summary,
|
|
45
|
+
scope,
|
|
46
|
+
source,
|
|
47
|
+
signal,
|
|
48
|
+
task,
|
|
49
|
+
tags,
|
|
50
|
+
tools,
|
|
51
|
+
skillId,
|
|
52
|
+
idempotencyKey,
|
|
53
|
+
});
|
|
54
|
+
const stored = await workflowObservationStore.save(record);
|
|
55
|
+
|
|
56
|
+
let skillOutcomeNote = "";
|
|
57
|
+
if (skillId) {
|
|
58
|
+
try {
|
|
59
|
+
const { store } = getComponents();
|
|
60
|
+
const result = await recordSkillOutcome(store, skillId, outcome);
|
|
61
|
+
if (result.updated) {
|
|
62
|
+
skillOutcomeNote = `\nSkill ${skillId.slice(0, 8)} → success=${result.successCount} failure=${result.failureCount}`;
|
|
63
|
+
} else {
|
|
64
|
+
console.warn(`[workflow_observe] skillId=${skillId} update skipped: ${result.reason}`);
|
|
65
|
+
skillOutcomeNote = `\nSkill ${skillId.slice(0, 8)} → not updated (${result.reason})`;
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.warn(`[workflow_observe] recordSkillOutcome threw for skillId=${skillId}:`, err);
|
|
69
|
+
skillOutcomeNote = `\nSkill ${skillId.slice(0, 8)} → update errored, observation still saved`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: "text" as const,
|
|
76
|
+
text: formatWorkflowObservationSaved(stored) + skillOutcomeNote,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
registerTool(
|
|
83
|
+
"workflow_health",
|
|
84
|
+
"Inspect workflow observation health: 7d/30d report for one workflow or dashboard of degraded workflows. Read-only. Use when checking if continuity primitives are succeeding or degrading.",
|
|
85
|
+
{
|
|
86
|
+
workflowId: z.string().min(1).max(120).optional().describe("Optional workflow primitive id"),
|
|
87
|
+
scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
|
|
88
|
+
limit: z.number().int().min(1).max(30).default(10).describe("Dashboard result limit when workflowId is omitted"),
|
|
89
|
+
},
|
|
90
|
+
async ({ workflowId, scope, limit }) => {
|
|
91
|
+
const text = workflowId
|
|
92
|
+
? formatWorkflowHealthReport(await inspectWorkflowHealth(workflowObservationStore, { workflowId, scope }))
|
|
93
|
+
: formatWorkflowHealthDashboard(await inspectWorkflowDashboard(workflowObservationStore, { scope, limit }), scope);
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text" as const,
|
|
97
|
+
text,
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
registerTool(
|
|
104
|
+
"workflow_evidence",
|
|
105
|
+
"Generate an evidence pack for a workflow primitive with recent issues, top signals, and suggested actions. Read-only. Use when investigating why a workflow is degraded and you need concrete failure examples.",
|
|
106
|
+
{
|
|
107
|
+
workflowId: z.string().min(1).max(120).describe("Workflow primitive id, e.g. 'resume_context'"),
|
|
108
|
+
scope: z.string().min(1).max(160).optional().describe("Scope filter, e.g. 'project:recallnest'"),
|
|
109
|
+
limit: z.number().int().min(1).max(20).default(5).describe("Max recent issue observations to include"),
|
|
110
|
+
},
|
|
111
|
+
async ({ workflowId, scope, limit }) => {
|
|
112
|
+
const pack = await buildWorkflowEvidence(workflowObservationStore, {
|
|
113
|
+
workflowId,
|
|
114
|
+
scope,
|
|
115
|
+
limit,
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
content: [{
|
|
119
|
+
type: "text" as const,
|
|
120
|
+
text: formatWorkflowEvidencePack(pack),
|
|
121
|
+
}],
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
registerTool(
|
|
127
|
+
"list_conflicts",
|
|
128
|
+
"List or inspect conflict candidates where promoted evidence disagrees with existing durable memory. Read-only. Use when reviewing pending conflicts before resolution.",
|
|
129
|
+
{
|
|
130
|
+
conflictId: z.string().min(1).max(128).optional().describe("Conflict ID to inspect a single record, e.g. 'c1d2e3f4'"),
|
|
131
|
+
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
132
|
+
attention: z.enum(CONFLICT_ATTENTION_LEVELS).optional().describe("Optional lifecycle filter: fresh / aging / stale / escalated / resolved"),
|
|
133
|
+
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
134
|
+
groupBy: z.enum(["record", "cluster"]).default("record").describe("Whether to list individual conflicts or grouped clusters"),
|
|
135
|
+
limit: z.number().int().min(1).max(50).default(20).describe("Max conflicts to list"),
|
|
136
|
+
},
|
|
137
|
+
async ({ conflictId, status, attention, canonicalKey, groupBy, limit }) => {
|
|
138
|
+
if (conflictId) {
|
|
139
|
+
const record = await conflictStore.getById(conflictId);
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: "text" as const,
|
|
143
|
+
text: record ? formatConflictRecord(record) : `Conflict not found: ${conflictId}`,
|
|
144
|
+
}],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const records = (await conflictStore.listRecent({ status, canonicalKey, limit: Math.max(limit * 2, limit) }))
|
|
149
|
+
.filter((record) => !attention || summarizeConflictLifecycle(record).attention === attention)
|
|
150
|
+
.slice(0, limit);
|
|
151
|
+
return {
|
|
152
|
+
content: [{
|
|
153
|
+
type: "text" as const,
|
|
154
|
+
text: groupBy === "cluster"
|
|
155
|
+
? formatConflictClusters(clusterConflicts(records))
|
|
156
|
+
: formatConflictList(records),
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
registerTool(
|
|
163
|
+
"resolve_conflict",
|
|
164
|
+
"Resolve a conflict candidate by keeping existing, accepting incoming, or merging texts. Side effect: updates conflict status and may modify durable memory. Use when list_conflicts shows open conflicts that need a decision.",
|
|
165
|
+
{
|
|
166
|
+
conflictId: z.string().min(1).max(128).describe("Conflict ID to resolve"),
|
|
167
|
+
resolution: z.enum(["accept_incoming", "keep_existing", "merge"]).describe("How to resolve the conflict"),
|
|
168
|
+
mergedText: z.string().min(1).max(2000).optional().describe("Optional merged text override when resolution is merge"),
|
|
169
|
+
notes: z.string().min(1).max(320).optional().describe("Optional operator notes"),
|
|
170
|
+
},
|
|
171
|
+
async ({ conflictId, resolution, mergedText, notes }) => {
|
|
172
|
+
const { store, embedder } = getComponents();
|
|
173
|
+
const result = await resolveConflictCandidate({
|
|
174
|
+
store,
|
|
175
|
+
embedder,
|
|
176
|
+
conflictStore,
|
|
177
|
+
}, {
|
|
178
|
+
conflictId,
|
|
179
|
+
resolution,
|
|
180
|
+
...(mergedText ? { mergedText } : {}),
|
|
181
|
+
notes,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
content: [{
|
|
186
|
+
type: "text" as const,
|
|
187
|
+
text: formatConflictResolution(result),
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
registerTool(
|
|
194
|
+
"audit_conflicts",
|
|
195
|
+
"Generate a conflict audit summary showing priority clusters by staleness and escalation level. Read-only. Use when triaging which conflict clusters to resolve first.",
|
|
196
|
+
{
|
|
197
|
+
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
198
|
+
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
199
|
+
limit: z.number().int().min(1).max(500).default(100).describe("How many conflict records to scan"),
|
|
200
|
+
top: z.number().int().min(1).max(20).default(5).describe("How many priority clusters to show"),
|
|
201
|
+
},
|
|
202
|
+
async ({ status, canonicalKey, limit, top }) => {
|
|
203
|
+
const records = await conflictStore.listRecent({
|
|
204
|
+
status,
|
|
205
|
+
canonicalKey,
|
|
206
|
+
limit,
|
|
207
|
+
});
|
|
208
|
+
const summary = buildConflictAuditSummary(records, top);
|
|
209
|
+
return {
|
|
210
|
+
content: [{
|
|
211
|
+
type: "text" as const,
|
|
212
|
+
text: formatConflictAudit(summary),
|
|
213
|
+
}],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
registerTool(
|
|
219
|
+
"escalate_conflicts",
|
|
220
|
+
"Preview or apply conflict aging policy to mark stale conflicts for operator review. Side effect: when apply=true, persists escalation metadata. Use when conflicts have aged past their attention threshold.",
|
|
221
|
+
{
|
|
222
|
+
attention: z.enum(["stale", "escalated"]).default("stale").describe("Only consider stale or escalated conflicts"),
|
|
223
|
+
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
224
|
+
limit: z.number().int().min(1).max(500).default(100).describe("How many open conflicts to scan"),
|
|
225
|
+
top: z.number().int().min(1).max(20).default(10).describe("How many eligible conflicts to include"),
|
|
226
|
+
apply: z.boolean().default(false).describe("When false, preview only. When true, persist escalation metadata."),
|
|
227
|
+
notes: z.string().min(1).max(320).optional().describe("Optional operator note when applying escalation"),
|
|
228
|
+
},
|
|
229
|
+
async ({ attention, canonicalKey, limit, top, apply, notes }) => {
|
|
230
|
+
const result = await escalateConflicts({
|
|
231
|
+
conflictStore,
|
|
232
|
+
}, {
|
|
233
|
+
attention,
|
|
234
|
+
canonicalKey,
|
|
235
|
+
limit,
|
|
236
|
+
top,
|
|
237
|
+
apply,
|
|
238
|
+
notes,
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
content: [{
|
|
242
|
+
type: "text" as const,
|
|
243
|
+
text: formatConflictEscalation(result),
|
|
244
|
+
}],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
registerTool(
|
|
250
|
+
"list_dirty_briefs",
|
|
251
|
+
"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.",
|
|
252
|
+
{},
|
|
253
|
+
async () => {
|
|
254
|
+
const rows = listDirtyBriefAssets();
|
|
255
|
+
if (rows.length === 0) {
|
|
256
|
+
return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
|
|
257
|
+
}
|
|
258
|
+
const lines = [
|
|
259
|
+
"Brief ID Title Scope Reasons",
|
|
260
|
+
"-------- ----- ----- ----------------------------------------",
|
|
261
|
+
...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`),
|
|
262
|
+
];
|
|
263
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
registerTool(
|
|
268
|
+
"clean_dirty_briefs",
|
|
269
|
+
"Archive dirty briefs and remove their indexed asset entries. Side effect: when apply=true, moves briefs to archive and deletes index rows. Use when list_dirty_briefs shows stale briefs that need cleanup.",
|
|
270
|
+
{
|
|
271
|
+
apply: z.boolean().default(false).describe("When false, preview only (no writes). When true, archive briefs and delete indexed rows."),
|
|
272
|
+
},
|
|
273
|
+
async ({ apply }) => {
|
|
274
|
+
const { store } = getComponents();
|
|
275
|
+
const rows = listDirtyBriefAssets();
|
|
276
|
+
if (rows.length === 0) {
|
|
277
|
+
return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!apply) {
|
|
281
|
+
const preview = rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`).join("\n");
|
|
282
|
+
return {
|
|
283
|
+
content: [{
|
|
284
|
+
type: "text" as const,
|
|
285
|
+
text: `Dirty briefs detected: ${rows.length}\n\n${preview}\n\nCall clean_dirty_briefs with apply=true to archive them.`,
|
|
286
|
+
}],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let archived = 0;
|
|
291
|
+
let deleted = 0;
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
try {
|
|
294
|
+
archiveDirtyBriefAsset(row);
|
|
295
|
+
archived += 1;
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error("[recallnest] Failed to archive dirty brief:", err instanceof Error ? err.message : String(err));
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
deleted += await store.bulkDelete([row.scope]);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error("[recallnest] Failed to delete dirty brief index rows:", err instanceof Error ? err.message : String(err));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
content: [{
|
|
308
|
+
type: "text" as const,
|
|
309
|
+
text: `Dirty briefs: ${rows.length}\nArchived: ${archived}\nIndex rows deleted: ${deleted}`,
|
|
310
|
+
}],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
registerTool(
|
|
316
|
+
"consolidate_memories",
|
|
317
|
+
"Run semantic consolidation: cluster similar memories, merge near-duplicates, and detect contradictions. Side effect: when apply=true, archives merged entries. Use when a scope has grown large and needs deduplication.",
|
|
318
|
+
{
|
|
319
|
+
scope: z.string().min(1).max(160).describe("Scope to consolidate (e.g. project:recallnest)"),
|
|
320
|
+
clusterThreshold: z.number().min(0.5).max(1.0).default(0.82).describe("Min similarity to form a cluster (default 0.82)"),
|
|
321
|
+
mergeThreshold: z.number().min(0.5).max(1.0).default(0.92).describe("Min similarity to merge/archive (default 0.92)"),
|
|
322
|
+
maxEntries: z.number().min(10).max(2000).default(500).describe("Max entries to scan (default 500)"),
|
|
323
|
+
apply: z.boolean().default(false).describe("When false, preview only (scan + report without archiving). When true, actually merge/archive."),
|
|
324
|
+
},
|
|
325
|
+
async ({ scope, clusterThreshold, mergeThreshold, maxEntries, apply }) => {
|
|
326
|
+
const { store } = getComponents();
|
|
327
|
+
|
|
328
|
+
if (!apply) {
|
|
329
|
+
// Dry-run: use a read-only wrapper that blocks writes
|
|
330
|
+
const readOnlyStore = {
|
|
331
|
+
list: store.list.bind(store),
|
|
332
|
+
getById: store.getById.bind(store),
|
|
333
|
+
vectorSearch: store.vectorSearch.bind(store),
|
|
334
|
+
update: async () => null, // no-op in dry-run
|
|
335
|
+
};
|
|
336
|
+
const engine = new ConsolidationEngine(readOnlyStore, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries }, getKGStore());
|
|
337
|
+
const result = await engine.run(scope);
|
|
338
|
+
return {
|
|
339
|
+
content: [{
|
|
340
|
+
type: "text" as const,
|
|
341
|
+
text: `[DRY-RUN] ${formatConsolidationResult(result)}\n\nRe-run with apply=true to execute merges.`,
|
|
342
|
+
}],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// P0-1: serialize applied consolidation per scope across the 11 mcp-server processes.
|
|
347
|
+
// Skip (not queue) if another process is already consolidating this scope — the
|
|
348
|
+
// store-write lock already prevents write corruption; this just avoids redundant work.
|
|
349
|
+
const outcome = await withLock(
|
|
350
|
+
`consolidate-${scope}`,
|
|
351
|
+
async () => {
|
|
352
|
+
const engine = new ConsolidationEngine(store, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries }, getKGStore());
|
|
353
|
+
return engine.run(scope);
|
|
354
|
+
},
|
|
355
|
+
{ onBusy: "skip", expireMs: 600_000 },
|
|
356
|
+
);
|
|
357
|
+
if (!outcome.ran) {
|
|
358
|
+
return {
|
|
359
|
+
content: [{
|
|
360
|
+
type: "text" as const,
|
|
361
|
+
text: `⏭️ consolidate_memories skipped: another process is consolidating scope "${scope}".`,
|
|
362
|
+
}],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
content: [{
|
|
367
|
+
type: "text" as const,
|
|
368
|
+
text: formatConsolidationResult(outcome.result),
|
|
369
|
+
}],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
registerTool(
|
|
375
|
+
"scan_skill_promotions",
|
|
376
|
+
"Scan cases and patterns in a scope for potential promotion to reusable skills. Read-only. Use when you want to discover recurring procedures that deserve formalization as skills.",
|
|
377
|
+
{
|
|
378
|
+
scope: z.string().min(1).max(160).describe("Project scope to scan for promotion candidates"),
|
|
379
|
+
minOccurrences: z.number().min(2).max(20).default(3).describe("Minimum similar cases to trigger a promotion suggestion"),
|
|
380
|
+
},
|
|
381
|
+
async ({ scope, minOccurrences }) => {
|
|
382
|
+
const { store } = getComponents();
|
|
383
|
+
const result = await scanForPromotions(store, scope, {
|
|
384
|
+
minCaseOccurrences: minOccurrences,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
content: [{
|
|
389
|
+
type: "text" as const,
|
|
390
|
+
text: formatPromotionResult(result),
|
|
391
|
+
}],
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
registerTool(
|
|
397
|
+
"manage_alias",
|
|
398
|
+
"Manage user-level query alias rules (data/alias-map.json). Matching aliases append canonical tokens to the query in the BM25 channel only ('我的桥' → '+telegram-ai-bridge') — they deliberately do NOT alter the vector embedding, so casual aliases cannot cause semantic drift. Actions: add/remove/list, plus explain to debug which rules (builtin full-channel + user BM25-only) fire for a query. Use when a colloquial entity name fails to recall its canonical project/tool.",
|
|
399
|
+
{
|
|
400
|
+
action: z.enum(["add", "remove", "list", "explain"]).describe("add: upsert a rule; remove: delete by trigger; list: all rules (builtin + user); explain: show which rules match a query"),
|
|
401
|
+
trigger: z.string().min(1).max(60).optional().describe("Alias literal to match in queries, case-insensitive inclusion (required for add/remove)"),
|
|
402
|
+
expansions: z.array(z.string().min(1).max(60)).max(8).optional().describe("Canonical tokens to append (required for add, max 8)"),
|
|
403
|
+
query: z.string().min(1).max(500).optional().describe("Query to explain (required for explain)"),
|
|
404
|
+
},
|
|
405
|
+
async ({ action, trigger, expansions, query }) => {
|
|
406
|
+
const text = (() => {
|
|
407
|
+
switch (action) {
|
|
408
|
+
case "add": {
|
|
409
|
+
if (!trigger || !expansions?.length) throw new Error("invalid input: manage_alias add 需要 trigger 与 expansions");
|
|
410
|
+
const result = upsertUserAlias(trigger, expansions);
|
|
411
|
+
return result.ok
|
|
412
|
+
? `${result.action}: "${result.entry.trigger}" → [${result.entry.expansions.join(", ")}](写入 ${aliasMapFilePath()},BM25 通道立即生效)`
|
|
413
|
+
: `拒绝: ${result.reason}`;
|
|
414
|
+
}
|
|
415
|
+
case "remove": {
|
|
416
|
+
if (!trigger) throw new Error("invalid input: manage_alias remove 需要 trigger");
|
|
417
|
+
return removeUserAlias(trigger)
|
|
418
|
+
? `removed: "${trigger}"`
|
|
419
|
+
: `未找到 trigger "${trigger}"(只能删用户规则;builtin 规则在 src/aliases.ts)`;
|
|
420
|
+
}
|
|
421
|
+
case "list": {
|
|
422
|
+
const user = listUserAliases();
|
|
423
|
+
return [
|
|
424
|
+
`Builtin rules (${ALIAS_RULES.length}, read-only, full-channel vector+BM25):`,
|
|
425
|
+
...ALIAS_RULES.map(r => ` /${r.pattern.source}/ → ${r.expansion}${r.comment ? ` (${r.comment})` : ""}`),
|
|
426
|
+
``,
|
|
427
|
+
`User rules (${user.length}, BM25-only, ${aliasMapFilePath()}):`,
|
|
428
|
+
...(user.length ? user.map(r => ` "${r.trigger}" → [${r.expansions.join(", ")}]`) : [" (none)"]),
|
|
429
|
+
].join("\n");
|
|
430
|
+
}
|
|
431
|
+
case "explain": {
|
|
432
|
+
if (!query) throw new Error("invalid input: manage_alias explain 需要 query");
|
|
433
|
+
const builtinMatched = explainAliasExpansion(query);
|
|
434
|
+
const userMatched = explainUserAliases(query);
|
|
435
|
+
return [
|
|
436
|
+
`Query: ${query}`,
|
|
437
|
+
`Full-channel expanded (builtin): ${expandQueryWithAliases(query) === query ? "(unchanged)" : expandQueryWithAliases(query)}`,
|
|
438
|
+
`BM25-channel expanded (builtin synonyms + user aliases): ${expandQuery(query) === query ? "(unchanged)" : expandQuery(query)}`,
|
|
439
|
+
`Matched builtin rules (${builtinMatched.length}):`,
|
|
440
|
+
...builtinMatched.map(m => ` /${m.pattern}/ → ${m.expansion}`),
|
|
441
|
+
`Matched user aliases (${userMatched.length}):`,
|
|
442
|
+
...userMatched.map(m => ` "${m.trigger}" → [${m.expansions.join(", ")}]`),
|
|
443
|
+
].join("\n");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
|
|
448
|
+
return { content: [{ type: "text" as const, text }] };
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
registerTool(
|
|
453
|
+
"promote_scan",
|
|
454
|
+
"Scan transcript-downgraded evidence (profile/preferences sunk to events) and auto-promote recurring high-importance facts back to durable memory. Default dry-run (no writes); when dryRun=false it creates durable entries (dedup is idempotent). Use to backfill sparse durable profile/preferences from accumulated transcript evidence.",
|
|
455
|
+
{
|
|
456
|
+
scope: z.string().min(1).max(160).describe("Scope to scan, e.g. cc:project:recallnest"),
|
|
457
|
+
dryRun: z.boolean().default(true).describe("When true (default), only report candidates without writing"),
|
|
458
|
+
minOccurrences: z.number().int().min(2).max(20).default(3).describe("Min cluster members before promoting"),
|
|
459
|
+
minImportance: z.number().min(0).max(1).default(0.6).describe("Min average cluster importance to promote"),
|
|
460
|
+
},
|
|
461
|
+
async ({ scope, dryRun, minOccurrences, minImportance }) => {
|
|
462
|
+
const { store, embedder } = getComponents();
|
|
463
|
+
const kgExtractor = getKGExtractor();
|
|
464
|
+
const deps = buildPromoteScanDeps({ store, embedder, conflictStore, kgExtractor });
|
|
465
|
+
const result = await scanMemoryPromotions(deps, scope, { dryRun, minOccurrences, minImportance });
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
content: [{
|
|
469
|
+
type: "text" as const,
|
|
470
|
+
text: formatPromoteScanResult(result),
|
|
471
|
+
}],
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
}
|
package/src/memory-boundaries.ts
CHANGED
|
@@ -61,8 +61,29 @@ export interface IngestBoundaryResolution {
|
|
|
61
61
|
boundary: MemoryBoundaryMetadata;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// 在栈各端的 transcript 来源与 scope 前缀。
|
|
65
|
+
// ⚠️ 接入新的 AI 端时必须同步这两处 —— 漏掉的端不会被认成 evidence 层,
|
|
66
|
+
// 其会话碎片既不会被降权,也会被 layer admission 当成 durable 结论放行。
|
|
67
|
+
// (kimi / antigravity 就漏了:kimi 2026-07-19 进栈、antigravity 07-29 进栈,
|
|
68
|
+
// 直到 08-01 才发现库里的 `kimi:` scope 一直被当结论对待。)
|
|
69
|
+
//
|
|
70
|
+
// minis(2026-08-12 加,iPhone 上的 Minis agent,经 iCloud 文件信箱回流对话记录):
|
|
71
|
+
// 这一次是**接入前先补**,不是事后发现——补的动机值得记下来,因为 Minis 比前面几端更危险。
|
|
72
|
+
// 它的 CLI 把 agent 自己的工具调用回传也标成 `type:"user"` / `role:"user"`(实测一场对话
|
|
73
|
+
// 137 条里 69 条 user,其中仅 11 条是真人发言,另外 58 条是 `[Tool result: ...]`)。
|
|
74
|
+
// 所以漏掉它不只是"碎片被当结论",而是**把 shell 输出提炼成用户说过的话**——
|
|
75
|
+
// 例如 `ssh: connect to host ... timed out` 会变成一条"用户说过"的记忆。
|
|
76
|
+
// 导出侧已在上游过滤(只导真人对话),这里是第二道闸:即使某次导出没过滤干净,
|
|
77
|
+
// 走 ingest 的内容也会被正确降权到 evidence 层,而不是当成 durable 结论。
|
|
78
|
+
const TRANSCRIPT_SOURCES = new Set(["cc", "codex", "gemini", "kimi", "antigravity", "minis"]);
|
|
79
|
+
const TRANSCRIPT_SCOPE_PREFIXES = [
|
|
80
|
+
"cc:",
|
|
81
|
+
"codex:",
|
|
82
|
+
"gemini:",
|
|
83
|
+
"kimi:",
|
|
84
|
+
"antigravity:",
|
|
85
|
+
"minis:",
|
|
86
|
+
];
|
|
66
87
|
|
|
67
88
|
export function getConflictPolicyForCategory(category: DurableMemoryCategory): MemoryConflictPolicy {
|
|
68
89
|
return category === "events" || category === "cases"
|
|
@@ -135,7 +156,9 @@ export function buildDefaultCanonicalKey(params: {
|
|
|
135
156
|
? [params.category, slot.type, slot.brand, slot.item]
|
|
136
157
|
: slot.type === "reply-style"
|
|
137
158
|
? [params.category, slot.type, ...slot.traits]
|
|
138
|
-
:
|
|
159
|
+
: slot.type === "implicit-usage"
|
|
160
|
+
? [params.category, slot.type, slot.subject]
|
|
161
|
+
: [params.category, slot.type, slot.preferredTool, "over", slot.avoidedTool],
|
|
139
162
|
);
|
|
140
163
|
}
|
|
141
164
|
}
|
package/src/memory-evolution.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { parseEmotion, isEmotionScoringEnabled } from "./memory-schema.js";
|
|
16
|
+
import { computeUsageStatus } from "./usage-tracker.js";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Types
|
|
@@ -328,6 +329,16 @@ export function computeDecayScore(
|
|
|
328
329
|
importance: number,
|
|
329
330
|
now?: number,
|
|
330
331
|
metadata?: string,
|
|
332
|
+
): number {
|
|
333
|
+
return computeDecayScoreInternal(evo, importance, now, metadata, 1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function computeDecayScoreInternal(
|
|
337
|
+
evo: EvolutionMetadata,
|
|
338
|
+
importance: number,
|
|
339
|
+
now: number | undefined,
|
|
340
|
+
metadata: string | undefined,
|
|
341
|
+
frequencyFactor: number,
|
|
331
342
|
): number {
|
|
332
343
|
const ts = now ?? Date.now();
|
|
333
344
|
|
|
@@ -340,7 +351,7 @@ export function computeDecayScore(
|
|
|
340
351
|
const recencyBoost = evo.lastAccessedAt
|
|
341
352
|
? Math.pow(0.5, Math.max(0, (ts - evo.lastAccessedAt) / 86_400_000) / 30) // 30-day half-life for recency
|
|
342
353
|
: 0.5; // never accessed → neutral
|
|
343
|
-
const frequencyScore = Math.min(1, rawFreq * recencyBoost);
|
|
354
|
+
const frequencyScore = Math.min(1, rawFreq * recencyBoost) * Math.max(0, Math.min(1, frequencyFactor));
|
|
344
355
|
|
|
345
356
|
const clampedImportance = Math.max(0, Math.min(1, importance));
|
|
346
357
|
|
|
@@ -363,3 +374,64 @@ export function computeDecayScore(
|
|
|
363
374
|
IMPORTANCE_WEIGHT_BASE * clampedImportance
|
|
364
375
|
);
|
|
365
376
|
}
|
|
377
|
+
|
|
378
|
+
function parseMetadataObject(metadata: string | undefined): Record<string, unknown> {
|
|
379
|
+
if (!metadata) return {};
|
|
380
|
+
try {
|
|
381
|
+
const parsed: unknown = JSON.parse(metadata);
|
|
382
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
383
|
+
? parsed as Record<string, unknown>
|
|
384
|
+
: {};
|
|
385
|
+
} catch {
|
|
386
|
+
return {};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readUsageCounts(meta: Record<string, unknown>): { injection: number; use: number } {
|
|
391
|
+
const usage = meta.usage;
|
|
392
|
+
const usageObj = usage && typeof usage === "object" && !Array.isArray(usage)
|
|
393
|
+
? usage as Record<string, unknown>
|
|
394
|
+
: {};
|
|
395
|
+
return {
|
|
396
|
+
injection: typeof meta.accessCount === "number" ? meta.accessCount : 0,
|
|
397
|
+
use: typeof usageObj.useCount === "number" ? usageObj.useCount : 0,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isPendingProspectiveReminder(meta: Record<string, unknown>, now: number): boolean {
|
|
402
|
+
const prospective = meta.prospective;
|
|
403
|
+
if (!prospective || typeof prospective !== "object" || Array.isArray(prospective)) return false;
|
|
404
|
+
const obj = prospective as Record<string, unknown>;
|
|
405
|
+
if (obj.status !== "pending") return false;
|
|
406
|
+
if (typeof obj.expiresAt !== "string" || obj.expiresAt.length === 0) return true;
|
|
407
|
+
const expiresAt = Date.parse(obj.expiresAt);
|
|
408
|
+
return Number.isNaN(expiresAt) || expiresAt >= now;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isUsageDecayProtected(meta: Record<string, unknown>, importance: number, now: number): boolean {
|
|
412
|
+
if (importance >= 0.95) return true;
|
|
413
|
+
if (typeof meta.promotedTo === "string" && meta.promotedTo.trim().length > 0) return true;
|
|
414
|
+
return isPendingProspectiveReminder(meta, now);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* GC-only usage-adjusted decay score.
|
|
419
|
+
*
|
|
420
|
+
* Cold means "repeatedly surfaced but never actually cited"; in that case the
|
|
421
|
+
* frequency term is discounted instead of rewarding unhelpful exposure. This is
|
|
422
|
+
* intentionally separate from computeDecayScore(), which is still used by
|
|
423
|
+
* online retrieval ranking.
|
|
424
|
+
*/
|
|
425
|
+
export function computeUsageAdjustedDecayScore(
|
|
426
|
+
evo: EvolutionMetadata,
|
|
427
|
+
importance: number,
|
|
428
|
+
now?: number,
|
|
429
|
+
metadata?: string,
|
|
430
|
+
): number {
|
|
431
|
+
const ts = now ?? Date.now();
|
|
432
|
+
const meta = parseMetadataObject(metadata);
|
|
433
|
+
const counts = readUsageCounts(meta);
|
|
434
|
+
const status = computeUsageStatus(counts.injection, counts.use);
|
|
435
|
+
const factor = status === "cold" && !isUsageDecayProtected(meta, importance, ts) ? 0.2 : 1;
|
|
436
|
+
return computeDecayScoreInternal(evo, importance, ts, metadata, factor);
|
|
437
|
+
}
|
|
@@ -125,6 +125,10 @@ function computeTierAccessSignal(
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ⚠️ 约束(2026-07-21 通用Wiki上下文记忆Agent方案借鉴审计 P1-4):本函数用 accessCount 参与
|
|
129
|
+
// importance 计算(importance←accessSignal,越常访问 importance 越高的马太逻辑)。当前为孤儿代码——
|
|
130
|
+
// 全树仅测试引用、生产侧零 import。接线前须评估与「usage 只进检索先验、绝不抬 importance/事实置信」
|
|
131
|
+
// 立场的冲突:频次抬 importance 会自增强(越常召回越权威),违反该立场。接线须 Alice 明确批准。
|
|
128
132
|
export function computeRebalancedImportance(
|
|
129
133
|
currentImportance: number,
|
|
130
134
|
tier: MemoryTier,
|