recallnest 2.2.1 → 2.5.4
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 +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- 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/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- 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 +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -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__/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__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -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 +33 -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 +782 -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 +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -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 +216 -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 +84 -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 +321 -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__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -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-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -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 +250 -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__/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 +314 -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 +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -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-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.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-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -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 +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -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.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__/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 +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- 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 +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- 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 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- 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
|
@@ -135,7 +135,9 @@ export function buildDefaultCanonicalKey(params: {
|
|
|
135
135
|
? [params.category, slot.type, slot.brand, slot.item]
|
|
136
136
|
: slot.type === "reply-style"
|
|
137
137
|
? [params.category, slot.type, ...slot.traits]
|
|
138
|
-
:
|
|
138
|
+
: slot.type === "implicit-usage"
|
|
139
|
+
? [params.category, slot.type, slot.subject]
|
|
140
|
+
: [params.category, slot.type, slot.preferredTool, "over", slot.avoidedTool],
|
|
139
141
|
);
|
|
140
142
|
}
|
|
141
143
|
}
|
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
|
+
}
|