recallnest 1.4.1 → 2.2.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/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- 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 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- 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 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- 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-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__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
package/src/mcp-server.ts
CHANGED
|
@@ -31,10 +31,12 @@ const TOOL_TIERS: Record<string, ToolTier> = {
|
|
|
31
31
|
store_memory: "core",
|
|
32
32
|
checkpoint_session: "core",
|
|
33
33
|
latest_checkpoint: "core",
|
|
34
|
+
list_tools: "core",
|
|
34
35
|
|
|
35
36
|
set_reminder: "core",
|
|
36
37
|
|
|
37
38
|
// Advanced
|
|
39
|
+
batch_store: "advanced",
|
|
38
40
|
auto_capture: "advanced",
|
|
39
41
|
store_case: "advanced",
|
|
40
42
|
store_workflow_pattern: "advanced",
|
|
@@ -46,8 +48,18 @@ const TOOL_TIERS: Record<string, ToolTier> = {
|
|
|
46
48
|
list_assets: "advanced",
|
|
47
49
|
list_pins: "advanced",
|
|
48
50
|
memory_stats: "advanced",
|
|
51
|
+
data_checkup: "advanced",
|
|
52
|
+
memory_lint: "advanced",
|
|
53
|
+
export_graph: "advanced",
|
|
54
|
+
dream: "advanced",
|
|
49
55
|
memory_drill_down: "advanced",
|
|
50
56
|
export_memory: "advanced",
|
|
57
|
+
store_skill: "advanced",
|
|
58
|
+
retrieve_skill: "advanced",
|
|
59
|
+
import_conversations: "advanced",
|
|
60
|
+
distill_session: "advanced",
|
|
61
|
+
scan_skill_promotions: "governance",
|
|
62
|
+
forget_memory: "advanced",
|
|
51
63
|
|
|
52
64
|
// Governance (CLI-only, not in MCP by default)
|
|
53
65
|
workflow_observe: "governance",
|
|
@@ -71,21 +83,25 @@ function shouldRegisterTool(toolName: string): boolean {
|
|
|
71
83
|
return true;
|
|
72
84
|
}
|
|
73
85
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
86
|
+
import { autoRegisterBabelMemory } from "./language-hook.js";
|
|
74
87
|
import { z } from "zod";
|
|
75
88
|
import type { RetrievalResult } from "./retriever.js";
|
|
76
89
|
import type { MemoryStore } from "./store.js";
|
|
77
|
-
import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
90
|
+
import { distillResults, formatExplainResults, formatSearchResults, formatBriefResults, formatFullResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
78
91
|
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, listDirtyBriefAssets, listMemoryAssets, listPinAssets, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
|
|
79
92
|
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
|
|
80
93
|
import { createComponentResolver, loadConfig, loadDotEnv, resolveRecallMode } from "./runtime-config.js";
|
|
81
|
-
import { DurableMemoryCategorySchema, StoreMemorySourceSchema } from "./memory-schema.js";
|
|
94
|
+
import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled } from "./memory-schema.js";
|
|
82
95
|
import { persistCaseMemory, persistMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
|
|
96
|
+
import { persistSkill, retrieveSkills } from "./skill-engine.js";
|
|
97
|
+
import { SkillImplementationTypeSchema } from "./skill-schema.js";
|
|
98
|
+
import { scanForPromotions, formatPromotionResult } from "./skill-promotion.js";
|
|
83
99
|
import { autoCapture } from "./capture-heuristic.js";
|
|
84
100
|
import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
|
|
85
101
|
import { renderMemories, type RenderMode } from "./context-renderer.js";
|
|
86
102
|
import { buildSessionCheckpointResult } from "./session-engine.js";
|
|
87
103
|
import { SessionCheckpointStore } from "./session-store.js";
|
|
88
|
-
import { composeResumeContext } from "./context-composer.js";
|
|
104
|
+
import { composeLightResumeContext, composeResumeContext } from "./context-composer.js";
|
|
89
105
|
import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
|
|
90
106
|
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
91
107
|
import { KGStore } from "./kg-store.js";
|
|
@@ -93,6 +109,10 @@ import { createKGExtractor, isKGModeEnabled, type KGExtractor } from "./kg-extra
|
|
|
93
109
|
import { resolveConflictCandidate } from "./conflict-engine.js";
|
|
94
110
|
import { escalateConflicts } from "./conflict-escalation.js";
|
|
95
111
|
import { ConflictCandidateStore } from "./conflict-store.js";
|
|
112
|
+
import { runDataCheckup, formatCheckupReport } from "./data-checkup.js";
|
|
113
|
+
import { runMemoryLint, formatMemoryLintReport } from "./memory-lint.js";
|
|
114
|
+
import { exportMemoryGraph, formatGraphExportResult } from "./graph-export.js";
|
|
115
|
+
import { runDream, formatDreamResult } from "./dream-pipeline.js";
|
|
96
116
|
import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
|
|
97
117
|
import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
|
|
98
118
|
import { buildConflictAuditSummary, clusterConflicts } from "./conflict-advisor.js";
|
|
@@ -103,7 +123,11 @@ import { WorkflowObservationStore } from "./workflow-observation-store.js";
|
|
|
103
123
|
import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
|
|
104
124
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
105
125
|
import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
|
|
106
|
-
import { setReminder, checkTriggers, fireReminder, formatReminders } from "./prospective-memory.js";
|
|
126
|
+
import { setReminder, checkTriggers, fireReminder, formatReminders, suggestPredictedReminders, formatSuggestedReminders, acceptPredictedReminder, demotePredictedReminder } from "./prospective-memory.js";
|
|
127
|
+
import type { PredictionContext } from "./prediction-engine.js";
|
|
128
|
+
import { forgetMemory, forgetByScope } from "./forget-engine.js";
|
|
129
|
+
import { createAuditLogger } from "./audit-log.js";
|
|
130
|
+
// distill_session uses dynamic import("./session-distiller.js") at call time
|
|
107
131
|
|
|
108
132
|
function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
|
|
109
133
|
if (!entry) {
|
|
@@ -167,12 +191,16 @@ const server = new McpServer({
|
|
|
167
191
|
type ToolSchema = Parameters<typeof server.tool>[2];
|
|
168
192
|
type ToolHandler = Parameters<typeof server.tool>[3];
|
|
169
193
|
|
|
194
|
+
/** Map of all registered tool names to their descriptions (populated during registration). */
|
|
195
|
+
const TOOL_DESCRIPTIONS = new Map<string, string>();
|
|
196
|
+
|
|
170
197
|
function registerTool(name: string, description: string, schema: ToolSchema, handler: ToolHandler): void {
|
|
171
198
|
if (!shouldRegisterTool(name)) {
|
|
172
199
|
// stdout is reserved for MCP JSON-RPC on stdio transports.
|
|
173
200
|
console.error(`[MCP] Skipping ${name} (tier: ${TOOL_TIERS[name]})`);
|
|
174
201
|
return;
|
|
175
202
|
}
|
|
203
|
+
TOOL_DESCRIPTIONS.set(name, description);
|
|
176
204
|
server.tool(name, description, schema, handler);
|
|
177
205
|
}
|
|
178
206
|
|
|
@@ -214,7 +242,7 @@ registerTool(
|
|
|
214
242
|
|
|
215
243
|
registerTool(
|
|
216
244
|
"workflow_health",
|
|
217
|
-
"Inspect workflow observation health
|
|
245
|
+
"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.",
|
|
218
246
|
{
|
|
219
247
|
workflowId: z.string().min(1).max(120).optional().describe("Optional workflow primitive id"),
|
|
220
248
|
scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
|
|
@@ -235,10 +263,10 @@ registerTool(
|
|
|
235
263
|
|
|
236
264
|
registerTool(
|
|
237
265
|
"workflow_evidence",
|
|
238
|
-
"Generate an evidence pack for a workflow primitive
|
|
266
|
+
"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.",
|
|
239
267
|
{
|
|
240
|
-
workflowId: z.string().min(1).max(120).describe("Workflow primitive id"),
|
|
241
|
-
scope: z.string().min(1).max(160).optional().describe("
|
|
268
|
+
workflowId: z.string().min(1).max(120).describe("Workflow primitive id, e.g. 'resume_context'"),
|
|
269
|
+
scope: z.string().min(1).max(160).optional().describe("Scope filter, e.g. 'project:recallnest'"),
|
|
242
270
|
limit: z.number().int().min(1).max(20).default(5).describe("Max recent issue observations to include"),
|
|
243
271
|
},
|
|
244
272
|
async ({ workflowId, scope, limit }) => {
|
|
@@ -267,8 +295,19 @@ registerTool(
|
|
|
267
295
|
source: StoreMemorySourceSchema.default("manual").describe("How this memory was captured"),
|
|
268
296
|
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
269
297
|
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
298
|
+
topicTag: z.string().min(1).max(60).optional().describe("Optional topic tag for intra-scope partitioning (e.g. 'auth', 'deploy', 'testing'). Auto-detected if omitted."),
|
|
299
|
+
privacyTier: PrivacyTierSchema.default("durable").describe("Privacy tier: ephemeral (auto-expire, no KG), private (persist, no KG), durable (default), shared (cross-scope)"),
|
|
300
|
+
validUntil: z.union([z.string(), z.number()]).optional().describe("Optional expiration: ISO date string or ms timestamp. Memory will be deprioritized after this time."),
|
|
301
|
+
eventTime: z.union([z.string(), z.number()]).optional().describe("Optional event time: when the event actually happened (ISO date or ms), distinct from storage time."),
|
|
302
|
+
confidence: z.union([
|
|
303
|
+
z.number().min(0).max(1),
|
|
304
|
+
z.object({
|
|
305
|
+
score: z.number().min(0).max(1),
|
|
306
|
+
reliability: z.enum(["direct", "inferred", "hearsay"]).optional(),
|
|
307
|
+
}),
|
|
308
|
+
]).optional().describe("Optional confidence override: number (0-1) or {score, reliability}. Auto-assigned from source if omitted."),
|
|
270
309
|
},
|
|
271
|
-
async ({ text, category, importance, scope, source, tags, canonicalKey }) => {
|
|
310
|
+
async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, validUntil, eventTime, confidence }) => {
|
|
272
311
|
const { store, embedder } = getComponents();
|
|
273
312
|
const stored = await persistMemory({
|
|
274
313
|
store,
|
|
@@ -283,6 +322,13 @@ registerTool(
|
|
|
283
322
|
source,
|
|
284
323
|
tags,
|
|
285
324
|
canonicalKey,
|
|
325
|
+
topicTag,
|
|
326
|
+
privacyTier,
|
|
327
|
+
// F3: Pass temporal validity params (extracted by persistMemory before Zod parse)
|
|
328
|
+
validUntil,
|
|
329
|
+
eventTime,
|
|
330
|
+
// F1: Pass confidence override (extracted by persistMemory before Zod parse)
|
|
331
|
+
confidence,
|
|
286
332
|
});
|
|
287
333
|
|
|
288
334
|
return {
|
|
@@ -357,7 +403,7 @@ registerTool(
|
|
|
357
403
|
|
|
358
404
|
registerTool(
|
|
359
405
|
"set_reminder",
|
|
360
|
-
"Set a prospective memory reminder
|
|
406
|
+
"Set a prospective memory reminder that auto-triggers during future search_memory calls when the trigger keywords match. Side effect: stores a reminder entry. Use when you need a future nudge tied to a specific context.",
|
|
361
407
|
{
|
|
362
408
|
trigger: z.string().min(1).max(200).describe("Trigger condition — keywords that should activate this reminder"),
|
|
363
409
|
action: z.string().min(1).max(500).describe("What to remind about when the trigger fires"),
|
|
@@ -505,7 +551,7 @@ registerTool(
|
|
|
505
551
|
|
|
506
552
|
registerTool(
|
|
507
553
|
"promote_memory",
|
|
508
|
-
"Promote an evidence memory into durable memory. Use
|
|
554
|
+
"Promote an evidence memory into durable memory with an authority upgrade. Side effect: creates a new durable entry linked to the source evidence. Use when a transcript snippet or imported artifact contains a fact worth keeping across windows.",
|
|
509
555
|
{
|
|
510
556
|
memoryId: z.string().min(1).max(128).describe("Existing evidence memory ID or unique prefix"),
|
|
511
557
|
text: z.string().min(1).max(4000).optional().describe("Optional cleaned durable text; defaults to the source entry text"),
|
|
@@ -547,9 +593,9 @@ registerTool(
|
|
|
547
593
|
|
|
548
594
|
registerTool(
|
|
549
595
|
"list_conflicts",
|
|
550
|
-
"List or inspect
|
|
596
|
+
"List or inspect conflict candidates where promoted evidence disagrees with existing durable memory. Read-only. Use when reviewing pending conflicts before resolution.",
|
|
551
597
|
{
|
|
552
|
-
conflictId: z.string().min(1).max(128).optional().describe("
|
|
598
|
+
conflictId: z.string().min(1).max(128).optional().describe("Conflict ID to inspect a single record, e.g. 'c1d2e3f4'"),
|
|
553
599
|
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
554
600
|
attention: z.enum(CONFLICT_ATTENTION_LEVELS).optional().describe("Optional lifecycle filter: fresh / aging / stale / escalated / resolved"),
|
|
555
601
|
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
@@ -583,7 +629,7 @@ registerTool(
|
|
|
583
629
|
|
|
584
630
|
registerTool(
|
|
585
631
|
"resolve_conflict",
|
|
586
|
-
"Resolve a
|
|
632
|
+
"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.",
|
|
587
633
|
{
|
|
588
634
|
conflictId: z.string().min(1).max(128).describe("Conflict ID to resolve"),
|
|
589
635
|
resolution: z.enum(["accept_incoming", "keep_existing", "merge"]).describe("How to resolve the conflict"),
|
|
@@ -614,7 +660,7 @@ registerTool(
|
|
|
614
660
|
|
|
615
661
|
registerTool(
|
|
616
662
|
"audit_conflicts",
|
|
617
|
-
"Generate a
|
|
663
|
+
"Generate a conflict audit summary showing priority clusters by staleness and escalation level. Read-only. Use when triaging which conflict clusters to resolve first.",
|
|
618
664
|
{
|
|
619
665
|
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
620
666
|
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
@@ -639,7 +685,7 @@ registerTool(
|
|
|
639
685
|
|
|
640
686
|
registerTool(
|
|
641
687
|
"escalate_conflicts",
|
|
642
|
-
"Preview or apply
|
|
688
|
+
"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.",
|
|
643
689
|
{
|
|
644
690
|
attention: z.enum(["stale", "escalated"]).default("stale").describe("Only consider stale or escalated conflicts"),
|
|
645
691
|
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
@@ -712,10 +758,10 @@ registerTool(
|
|
|
712
758
|
|
|
713
759
|
registerTool(
|
|
714
760
|
"latest_checkpoint",
|
|
715
|
-
"Fetch the
|
|
761
|
+
"Fetch the most recent saved checkpoint for a session or shared scope. Read-only. Use when you need to inspect current work state without running a full resume_context.",
|
|
716
762
|
{
|
|
717
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
718
|
-
scope: z.string().min(1).max(160).optional().describe("
|
|
763
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier filter, e.g. 'abc123'"),
|
|
764
|
+
scope: z.string().min(1).max(160).optional().describe("Shared scope filter, e.g. 'project:recallnest'"),
|
|
719
765
|
},
|
|
720
766
|
async ({ sessionId, scope }) => {
|
|
721
767
|
const latest = await checkpointStore.getLatest({ sessionId, scope });
|
|
@@ -730,7 +776,7 @@ registerTool(
|
|
|
730
776
|
|
|
731
777
|
registerTool(
|
|
732
778
|
"resume_context",
|
|
733
|
-
"Compose startup context for a fresh window by combining
|
|
779
|
+
"Compose startup context for a fresh window by combining durable memory, patterns, cases, and the latest checkpoint. Read-only. Use when entering a new session and you need to recover prior decisions, open loops, and next actions.",
|
|
734
780
|
{
|
|
735
781
|
task: z.string().min(1).max(500).optional().describe("Optional current task or question to bias recall"),
|
|
736
782
|
scope: z.string().min(1).max(160).optional().describe("Optional shared scope for project or terminal continuity"),
|
|
@@ -738,7 +784,7 @@ registerTool(
|
|
|
738
784
|
limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
|
|
739
785
|
includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
|
|
740
786
|
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
741
|
-
mode: z.enum(["full", "summary", "off"]).optional().describe("Override recall mode (default
|
|
787
|
+
mode: z.enum(["full", "light", "summary", "off"]).optional().describe("Override recall mode: 'full' (default), 'light' (<300 tokens), 'summary' (checkpoint only), 'off'"),
|
|
742
788
|
},
|
|
743
789
|
async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
|
|
744
790
|
const effectiveMode = resolveRecallMode(config, modeOverride);
|
|
@@ -785,6 +831,44 @@ registerTool(
|
|
|
785
831
|
};
|
|
786
832
|
}
|
|
787
833
|
|
|
834
|
+
// --- light mode: <300 token ultra-light wake-up ---
|
|
835
|
+
if (effectiveMode === "light") {
|
|
836
|
+
const { retriever: lightRetriever } = getComponents(profileName);
|
|
837
|
+
const lightScope = resolveScopeSelection({
|
|
838
|
+
scope,
|
|
839
|
+
sessionId,
|
|
840
|
+
operation: "resume_context",
|
|
841
|
+
allowUnscoped: true,
|
|
842
|
+
});
|
|
843
|
+
const lightResult = await composeLightResumeContext({
|
|
844
|
+
retriever: lightRetriever,
|
|
845
|
+
checkpointStore,
|
|
846
|
+
}, {
|
|
847
|
+
task,
|
|
848
|
+
scope: lightScope.resolvedScope,
|
|
849
|
+
sessionId,
|
|
850
|
+
limitPerSection: limitPerSection,
|
|
851
|
+
includeLatestCheckpoint,
|
|
852
|
+
profile: profileName,
|
|
853
|
+
});
|
|
854
|
+
await saveManagedObservation({
|
|
855
|
+
workflowId: "resume_context",
|
|
856
|
+
outcome: "success",
|
|
857
|
+
summary: `Managed resume_context returned light-mode context (~${lightResult.text.length} chars).`,
|
|
858
|
+
scope: lightScope.resolvedScope || scope || "global",
|
|
859
|
+
source: "managed:recallnest",
|
|
860
|
+
signal: "managed-resume-light",
|
|
861
|
+
task,
|
|
862
|
+
tags: ["managed", "recallnest", "light-mode"],
|
|
863
|
+
});
|
|
864
|
+
return {
|
|
865
|
+
content: [{
|
|
866
|
+
type: "text" as const,
|
|
867
|
+
text: lightResult.text,
|
|
868
|
+
}],
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
788
872
|
// --- full mode: existing compose behavior ---
|
|
789
873
|
const { retriever, profile } = getComponents(profileName);
|
|
790
874
|
const scopeSelection = resolveScopeSelection({
|
|
@@ -822,7 +906,7 @@ registerTool(
|
|
|
822
906
|
// --- search_memory tool ---
|
|
823
907
|
registerTool(
|
|
824
908
|
"search_memory",
|
|
825
|
-
"
|
|
909
|
+
"Search indexed memories by semantic similarity and return ranked results with optional temporal filtering. Read-only, but may fire stored reminders as a side effect. Use proactively at the start of tasks, when debugging, writing, or when the user references past work.",
|
|
826
910
|
{
|
|
827
911
|
query: z.string().describe("Search query — natural language or keywords"),
|
|
828
912
|
limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
|
|
@@ -835,19 +919,37 @@ registerTool(
|
|
|
835
919
|
after: z.string().optional().describe("Filter memories stored after this date (ISO format YYYY-MM-DD, or relative like '最近30天', 'last 7 days')"),
|
|
836
920
|
before: z.string().optional().describe("Filter memories stored before this date (ISO format YYYY-MM-DD, or relative)"),
|
|
837
921
|
graph: z.boolean().default(false).optional().describe("Enable KG graph traversal (PPR) for relationship-aware search. Use when query involves entity relationships (e.g. 'what tools does Alice use', 'Bob的朋友')."),
|
|
922
|
+
includeArchived: z.boolean().default(false).optional().describe("When true, also return archived/superseded/consolidated memories (default: only active)"),
|
|
923
|
+
detail_level: z.enum(["brief", "normal", "full"]).default("normal").optional()
|
|
924
|
+
.describe("Result detail level: brief (ID+score+one-liner), normal (default, current behavior), full (include metadata)"),
|
|
925
|
+
topicTag: z.string().min(1).max(60).optional()
|
|
926
|
+
.describe("Filter by topic tag (e.g. 'auth', 'deploy', 'testing'). Only returns memories tagged with this topic."),
|
|
927
|
+
reconstruct: z.boolean().default(false).describe(
|
|
928
|
+
"Return LLM-synthesized reconstruction alongside raw results. Requires RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true."
|
|
929
|
+
),
|
|
930
|
+
validAt: z.string().optional().describe("Query memories valid at a specific point in time (ISO date, e.g. '2025-06-15'). Returns only memories whose validity window covers this date."),
|
|
931
|
+
includeExpired: z.boolean().default(false).optional().describe("When true, include expired memories in results (demoted 80%). Default: only active/non-expired."),
|
|
838
932
|
},
|
|
839
|
-
async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph }) => {
|
|
933
|
+
async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph, includeArchived, detail_level, topicTag, reconstruct, validAt, includeExpired }) => {
|
|
840
934
|
const { retriever, profile } = getComponents(profileName);
|
|
841
935
|
// Ensure KG store is attached to non-default profile retrievers for PPR
|
|
842
936
|
if (graph && kgStoreInstance) retriever.setKGStore(kgStoreInstance);
|
|
937
|
+
// Attach LLM client for constructive retrieval if available
|
|
938
|
+
if (reconstruct && llm) retriever.setLLMClient(llm);
|
|
843
939
|
let results = await retriever.retrieve(buildRetrievalContext({
|
|
844
940
|
query,
|
|
845
|
-
limit: (after || before) ? limit * 3 : limit,
|
|
941
|
+
limit: (after || before || topicTag) ? limit * 3 : limit,
|
|
846
942
|
category,
|
|
847
943
|
scope,
|
|
848
944
|
sessionId,
|
|
849
945
|
allScopes,
|
|
850
946
|
graph,
|
|
947
|
+
includeArchived,
|
|
948
|
+
topicTag,
|
|
949
|
+
reconstruct,
|
|
950
|
+
// F3: Temporal validity filtering
|
|
951
|
+
validAt: validAt ? new Date(validAt).getTime() : undefined,
|
|
952
|
+
includeExpired: includeExpired ?? undefined,
|
|
851
953
|
}, {
|
|
852
954
|
operation: "search_memory",
|
|
853
955
|
}));
|
|
@@ -896,10 +998,59 @@ registerTool(
|
|
|
896
998
|
}
|
|
897
999
|
}
|
|
898
1000
|
|
|
1001
|
+
// HP-predictive: Surface predicted reminders alongside search results
|
|
1002
|
+
let suggestedText = "";
|
|
1003
|
+
if (isPredictiveMemoryEnabled()) {
|
|
1004
|
+
try {
|
|
1005
|
+
const recentCheckpoints = await checkpointStore.listRecent({ scope, limit: 5 });
|
|
1006
|
+
const recentObservations = await workflowObservationStore.listRecent({ scope, limit: 20 });
|
|
1007
|
+
const predictionCtx: PredictionContext = {
|
|
1008
|
+
checkpoints: recentCheckpoints,
|
|
1009
|
+
workflowObservations: recentObservations,
|
|
1010
|
+
frequentMemories: [], // Populated by access tracker in future iteration
|
|
1011
|
+
uncoveredTopics: results.length === 0 && query ? [query] : [],
|
|
1012
|
+
};
|
|
1013
|
+
const suggestions = await suggestPredictedReminders(store, embedder, predictionCtx, scope ?? "global");
|
|
1014
|
+
suggestedText = formatSuggestedReminders(suggestions);
|
|
1015
|
+
} catch {
|
|
1016
|
+
// Prediction failure is non-critical — silently skip
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const level = detail_level ?? "normal";
|
|
1021
|
+
const sections: string[] = [];
|
|
1022
|
+
|
|
1023
|
+
// Phase 4: Read reconstruction from first-class field (no metadata hack)
|
|
1024
|
+
const reconstruction = (results as import("./retriever.js").RetrievalResultSet).reconstruction;
|
|
1025
|
+
if (reconstruction?.reconstructed) {
|
|
1026
|
+
const sourceIds = reconstruction.sources.map(s => s.id).join(", ");
|
|
1027
|
+
const sourceTypes = [...new Set(reconstruction.sources.map(s => s.source.type))].join(", ");
|
|
1028
|
+
sections.push(
|
|
1029
|
+
`## Reconstructed Context (confidence: ${reconstruction.confidence.toFixed(2)}, coverage: ${reconstruction.coverage.toFixed(2)})\n${reconstruction.reconstructed}\n\nSources (${sourceTypes}): ${sourceIds}`
|
|
1030
|
+
);
|
|
1031
|
+
// Render contradictions if detected
|
|
1032
|
+
if (reconstruction.contradictions.length > 0) {
|
|
1033
|
+
const conflictLines = reconstruction.contradictions.map(c =>
|
|
1034
|
+
`- \u26a0\ufe0f ${c.description} [${c.memoryIds.join(" vs ")}]`
|
|
1035
|
+
);
|
|
1036
|
+
sections.push(`### Contradictions Detected\n${conflictLines.join("\n")}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let body: string;
|
|
1041
|
+
if (level === "brief") {
|
|
1042
|
+
body = formatBriefResults(results, { query });
|
|
1043
|
+
} else if (level === "full") {
|
|
1044
|
+
body = formatFullResults(results, { query, profile: profile.name });
|
|
1045
|
+
} else {
|
|
1046
|
+
body = formatSearchResults(results, { query, profile: profile.name });
|
|
1047
|
+
}
|
|
1048
|
+
sections.push(body);
|
|
1049
|
+
|
|
899
1050
|
return {
|
|
900
1051
|
content: [{
|
|
901
1052
|
type: "text" as const,
|
|
902
|
-
text:
|
|
1053
|
+
text: sections.join("\n\n") + reminderText + suggestedText,
|
|
903
1054
|
}],
|
|
904
1055
|
};
|
|
905
1056
|
}
|
|
@@ -907,15 +1058,15 @@ registerTool(
|
|
|
907
1058
|
|
|
908
1059
|
registerTool(
|
|
909
1060
|
"explain_memory",
|
|
910
|
-
"Explain why
|
|
1061
|
+
"Explain why memories matched a query: retrieval path, freshness, scope, and matched terms. Read-only. Use when search results seem unexpected and you need to debug ranking or scope filtering.",
|
|
911
1062
|
{
|
|
912
|
-
query: z.string().describe("Search query — natural language or keywords"),
|
|
913
|
-
limit: z.number().min(1).max(20).default(5).describe("
|
|
914
|
-
scope: z.string().optional().describe("
|
|
915
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
916
|
-
allScopes: z.boolean().default(false).describe("
|
|
917
|
-
category: DurableMemoryCategorySchema.optional().describe("Filter by memory category"),
|
|
918
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
1063
|
+
query: z.string().describe("Search query to explain — natural language or keywords, e.g. 'auth migration'"),
|
|
1064
|
+
limit: z.number().min(1).max(20).default(5).describe("Maximum number of matched results to analyze and explain (default: 5)"),
|
|
1065
|
+
scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:myapp'. Omit to use default scope"),
|
|
1066
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1067
|
+
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1068
|
+
category: DurableMemoryCategorySchema.optional().describe("Filter results by memory category, e.g. 'preference', 'decision', 'fact'"),
|
|
1069
|
+
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking: 'debug' for technical, 'fact-check' for precision"),
|
|
919
1070
|
},
|
|
920
1071
|
async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName }) => {
|
|
921
1072
|
const { retriever, profile } = getComponents(profileName);
|
|
@@ -940,14 +1091,14 @@ registerTool(
|
|
|
940
1091
|
|
|
941
1092
|
registerTool(
|
|
942
1093
|
"distill_memory",
|
|
943
|
-
"Distill retrieved memories into a compact briefing with source map, takeaways, and reusable evidence.",
|
|
1094
|
+
"Distill retrieved memories into a compact briefing with source map, key takeaways, and reusable evidence. Use this when you need a synthesized summary of stored knowledge on a topic rather than raw search results. Returns a structured briefing with citations. Read-only — does not modify stored memories.",
|
|
944
1095
|
{
|
|
945
|
-
query: z.string().describe("
|
|
946
|
-
limit: z.number().min(1).max(20).default(8).describe("
|
|
947
|
-
scope: z.string().optional().describe("
|
|
948
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
949
|
-
allScopes: z.boolean().default(false).describe("
|
|
950
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
1096
|
+
query: z.string().describe("Natural language topic or task to distill, e.g. 'authentication migration decisions'"),
|
|
1097
|
+
limit: z.number().min(1).max(20).default(8).describe("Maximum number of retrieved memories to include in the distillation (default: 8)"),
|
|
1098
|
+
scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
|
|
1099
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1100
|
+
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1101
|
+
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
|
|
951
1102
|
},
|
|
952
1103
|
async ({ query, limit, scope, sessionId, allScopes, profile: profileName }) => {
|
|
953
1104
|
const { retriever, profile } = getComponents(profileName || "writing");
|
|
@@ -971,15 +1122,15 @@ registerTool(
|
|
|
971
1122
|
|
|
972
1123
|
registerTool(
|
|
973
1124
|
"brief_memory",
|
|
974
|
-
"Create a structured memory brief
|
|
1125
|
+
"Create a structured memory brief by retrieving and summarizing relevant memories, then persist it as a reusable asset indexed for future recall. Use this when you want to consolidate scattered knowledge on a topic into a single retrievable document. Side effect: writes a new brief asset to disk and indexes it in the vector store for future search.",
|
|
975
1126
|
{
|
|
976
|
-
query: z.string().describe("
|
|
977
|
-
limit: z.number().min(1).max(20).default(8).describe("
|
|
978
|
-
scope: z.string().optional().describe("
|
|
979
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
980
|
-
allScopes: z.boolean().default(false).describe("
|
|
981
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
982
|
-
title: z.string().optional().describe("
|
|
1127
|
+
query: z.string().describe("Natural language topic or task to brief, e.g. 'deployment pipeline architecture decisions'"),
|
|
1128
|
+
limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the brief (default: 8)"),
|
|
1129
|
+
scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
|
|
1130
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1131
|
+
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1132
|
+
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
|
|
1133
|
+
title: z.string().optional().describe("Human-readable title for the brief asset, e.g. 'Q1 Auth Migration Summary'. Auto-generated if omitted"),
|
|
983
1134
|
},
|
|
984
1135
|
async ({ query, limit, scope, sessionId, allScopes, profile: profileName, title }) => {
|
|
985
1136
|
const { retriever, profile, store, embedder } = getComponents(profileName || "writing");
|
|
@@ -1012,16 +1163,16 @@ registerTool(
|
|
|
1012
1163
|
|
|
1013
1164
|
registerTool(
|
|
1014
1165
|
"pin_memory",
|
|
1015
|
-
"
|
|
1166
|
+
"Pin a retrieved memory as a high-importance reusable asset on disk. Side effect: boosts importance to 0.95, writes pin asset file, and indexes it. Use when a search result is critical and should be surfaced in future recalls.",
|
|
1016
1167
|
{
|
|
1017
|
-
memory_id: z.string().describe("Memory ID or unique prefix from search/explain output"),
|
|
1018
|
-
scope: z.string().optional().describe("
|
|
1019
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
1020
|
-
allScopes: z.boolean().default(false).describe("When true,
|
|
1021
|
-
title: z.string().optional().describe("
|
|
1022
|
-
summary: z.string().optional().describe("
|
|
1023
|
-
query: z.string().optional().describe("
|
|
1024
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
1168
|
+
memory_id: z.string().describe("Memory ID or unique prefix from search/explain output, e.g. 'a1b2c3d4'"),
|
|
1169
|
+
scope: z.string().optional().describe("Explicit scope filter, e.g. 'project:recallnest'"),
|
|
1170
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session:<id> scope, e.g. 'abc123'"),
|
|
1171
|
+
allScopes: z.boolean().default(false).describe("When true, allow cross-scope reads to find the memory"),
|
|
1172
|
+
title: z.string().optional().describe("Human-readable title for the pin, e.g. 'Auth migration decision'"),
|
|
1173
|
+
summary: z.string().optional().describe("Short summary override for the pinned asset"),
|
|
1174
|
+
query: z.string().optional().describe("Original query that led to this pin, e.g. 'auth decisions'"),
|
|
1175
|
+
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'debug'"),
|
|
1025
1176
|
},
|
|
1026
1177
|
async ({ memory_id, scope, sessionId, allScopes, title, summary, query, profile: profileName }) => {
|
|
1027
1178
|
const { store, embedder } = getComponents(profileName);
|
|
@@ -1057,15 +1208,15 @@ registerTool(
|
|
|
1057
1208
|
|
|
1058
1209
|
registerTool(
|
|
1059
1210
|
"export_memory",
|
|
1060
|
-
"Export a distilled memory briefing to a markdown or
|
|
1211
|
+
"Export a distilled memory briefing to a markdown or JSON file on disk. Side effect: writes an export artifact file. Use when you need an offline-readable snapshot of knowledge on a topic.",
|
|
1061
1212
|
{
|
|
1062
|
-
query: z.string().describe("Topic or task to export"),
|
|
1063
|
-
limit: z.number().min(1).max(20).default(8).describe("
|
|
1064
|
-
scope: z.string().optional().describe("
|
|
1065
|
-
sessionId: z.string().min(1).max(160).optional().describe("
|
|
1066
|
-
allScopes: z.boolean().default(false).describe("
|
|
1067
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
1068
|
-
format: z.enum(["md", "json"]).default("md").describe("Export format"),
|
|
1213
|
+
query: z.string().describe("Topic or task to export, e.g. 'auth migration decisions'"),
|
|
1214
|
+
limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the export (default: 8)"),
|
|
1215
|
+
scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:recallnest'. Omit to use default scope"),
|
|
1216
|
+
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1217
|
+
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1218
|
+
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'writing'"),
|
|
1219
|
+
format: z.enum(["md", "json"]).default("md").describe("Export format: 'md' for markdown, 'json' for structured JSON"),
|
|
1069
1220
|
},
|
|
1070
1221
|
async ({ query, limit, scope, sessionId, allScopes, profile: profileName, format }) => {
|
|
1071
1222
|
const { retriever, profile } = getComponents(profileName || "writing");
|
|
@@ -1098,9 +1249,9 @@ registerTool(
|
|
|
1098
1249
|
|
|
1099
1250
|
registerTool(
|
|
1100
1251
|
"list_assets",
|
|
1101
|
-
"List recent structured memory assets
|
|
1252
|
+
"List recent structured memory assets (pinned memories and distilled briefs) sorted by creation date. Read-only. Use when you need an inventory of persisted knowledge artifacts — for example, before creating a new brief to avoid duplicates. Returns asset type, title, scope, creation date, and file path for each entry.",
|
|
1102
1253
|
{
|
|
1103
|
-
limit: z.number().min(1).max(50).default(12).describe("
|
|
1254
|
+
limit: z.number().min(1).max(50).default(12).describe("Maximum number of assets to return, sorted most-recent-first (default: 12, max: 50)"),
|
|
1104
1255
|
},
|
|
1105
1256
|
async ({ limit }) => {
|
|
1106
1257
|
const rows = listMemoryAssets(limit);
|
|
@@ -1118,7 +1269,7 @@ registerTool(
|
|
|
1118
1269
|
|
|
1119
1270
|
registerTool(
|
|
1120
1271
|
"list_dirty_briefs",
|
|
1121
|
-
"
|
|
1272
|
+
"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.",
|
|
1122
1273
|
{},
|
|
1123
1274
|
async () => {
|
|
1124
1275
|
const rows = listDirtyBriefAssets();
|
|
@@ -1136,9 +1287,9 @@ registerTool(
|
|
|
1136
1287
|
|
|
1137
1288
|
registerTool(
|
|
1138
1289
|
"clean_dirty_briefs",
|
|
1139
|
-
"Archive dirty briefs and remove their indexed asset
|
|
1290
|
+
"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.",
|
|
1140
1291
|
{
|
|
1141
|
-
apply: z.boolean().default(false).describe("When false, preview only. When true, archive and delete indexed rows."),
|
|
1292
|
+
apply: z.boolean().default(false).describe("When false, preview only (no writes). When true, archive briefs and delete indexed rows."),
|
|
1142
1293
|
},
|
|
1143
1294
|
async ({ apply }) => {
|
|
1144
1295
|
const rows = listDirtyBriefAssets();
|
|
@@ -1159,9 +1310,17 @@ registerTool(
|
|
|
1159
1310
|
let archived = 0;
|
|
1160
1311
|
let deleted = 0;
|
|
1161
1312
|
for (const row of rows) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1313
|
+
try {
|
|
1314
|
+
archiveDirtyBriefAsset(row);
|
|
1315
|
+
archived += 1;
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
console.error("[recallnest] Failed to archive dirty brief:", err instanceof Error ? err.message : String(err));
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
deleted += await store.bulkDelete([row.scope]);
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
console.error("[recallnest] Failed to delete dirty brief index rows:", err instanceof Error ? err.message : String(err));
|
|
1323
|
+
}
|
|
1165
1324
|
}
|
|
1166
1325
|
|
|
1167
1326
|
return {
|
|
@@ -1175,7 +1334,7 @@ registerTool(
|
|
|
1175
1334
|
|
|
1176
1335
|
registerTool(
|
|
1177
1336
|
"consolidate_memories",
|
|
1178
|
-
"Run semantic consolidation
|
|
1337
|
+
"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.",
|
|
1179
1338
|
{
|
|
1180
1339
|
scope: z.string().min(1).max(160).describe("Scope to consolidate (e.g. project:recallnest)"),
|
|
1181
1340
|
clusterThreshold: z.number().min(0.5).max(1.0).default(0.82).describe("Min similarity to form a cluster (default 0.82)"),
|
|
@@ -1215,11 +1374,59 @@ registerTool(
|
|
|
1215
1374
|
}
|
|
1216
1375
|
);
|
|
1217
1376
|
|
|
1377
|
+
// --- forget_memory tool (Ethics Layer) ---
|
|
1378
|
+
registerTool(
|
|
1379
|
+
"forget_memory",
|
|
1380
|
+
"Permanently forget a memory with full cascade: delete primary entry, remove KG triples, demote related memories, and log an audit trail. Requires confirm=true for durable-tier memories. Use when the user explicitly requests a memory be forgotten, or to clean up sensitive/incorrect data.",
|
|
1381
|
+
{
|
|
1382
|
+
memoryId: z.string().min(1).max(128).describe("Memory ID to forget (full UUID or 8+ hex prefix)"),
|
|
1383
|
+
confirm: z.boolean().default(false).describe("Required confirmation — must be true for durable-tier memories"),
|
|
1384
|
+
reason: z.string().min(1).max(200).optional().describe("Reason for forgetting (recorded in audit trail)"),
|
|
1385
|
+
scope: z.string().min(1).max(160).optional().describe("Optional scope filter for permission check"),
|
|
1386
|
+
},
|
|
1387
|
+
async ({ memoryId, confirm, reason, scope }) => {
|
|
1388
|
+
const { store } = getComponents();
|
|
1389
|
+
const auditLogger = createAuditLogger();
|
|
1390
|
+
const scopeFilter = scope ? [scope] : undefined;
|
|
1391
|
+
|
|
1392
|
+
const result = await forgetMemory(
|
|
1393
|
+
{ store, kgStore: kgStoreInstance, auditLogger },
|
|
1394
|
+
{ memoryId, confirm, reason, scopeFilter },
|
|
1395
|
+
);
|
|
1396
|
+
|
|
1397
|
+
if (!result.success) {
|
|
1398
|
+
return {
|
|
1399
|
+
content: [{
|
|
1400
|
+
type: "text" as const,
|
|
1401
|
+
text: `❌ Forget failed: ${result.error}`,
|
|
1402
|
+
}],
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const lines = [
|
|
1407
|
+
`✅ Memory ${result.memoryId.slice(0, 8)} forgotten.`,
|
|
1408
|
+
`Privacy tier: ${result.evidence?.privacyTier || "unknown"}`,
|
|
1409
|
+
`KG triples removed: ${result.kgTriplesRemoved ? "yes" : "no/N/A"}`,
|
|
1410
|
+
`Cascade demoted: ${result.cascadeResult.demotedCount} related memories`,
|
|
1411
|
+
];
|
|
1412
|
+
if (result.evidence?.reason) {
|
|
1413
|
+
lines.push(`Reason: ${result.evidence.reason}`);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return {
|
|
1417
|
+
content: [{
|
|
1418
|
+
type: "text" as const,
|
|
1419
|
+
text: lines.join("\n"),
|
|
1420
|
+
}],
|
|
1421
|
+
};
|
|
1422
|
+
},
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1218
1425
|
registerTool(
|
|
1219
1426
|
"list_pins",
|
|
1220
|
-
"List
|
|
1427
|
+
"List pinned memory assets sorted by creation date, showing title, scope, importance score, and file path. Read-only. Use when you need to review high-value memories that were explicitly pinned via pin_memory, or to check if a topic already has a pinned reference before creating a new one.",
|
|
1221
1428
|
{
|
|
1222
|
-
limit: z.number().min(1).max(50).default(10).describe("
|
|
1429
|
+
limit: z.number().min(1).max(50).default(10).describe("Maximum number of pinned assets to return, sorted most-recent-first (default: 10, max: 50)"),
|
|
1223
1430
|
},
|
|
1224
1431
|
async ({ limit }) => {
|
|
1225
1432
|
const rows = listPinAssets(limit);
|
|
@@ -1238,7 +1445,7 @@ registerTool(
|
|
|
1238
1445
|
// --- memory_stats tool ---
|
|
1239
1446
|
registerTool(
|
|
1240
1447
|
"memory_stats",
|
|
1241
|
-
"Show statistics of the
|
|
1448
|
+
"Show aggregate statistics of the memory database: total entries, counts by source and category. Read-only. Use when you need an overview of memory store health or size.",
|
|
1242
1449
|
{},
|
|
1243
1450
|
async () => {
|
|
1244
1451
|
const stats = await store.stats();
|
|
@@ -1270,15 +1477,97 @@ registerTool(
|
|
|
1270
1477
|
}
|
|
1271
1478
|
);
|
|
1272
1479
|
|
|
1480
|
+
// ============================================================================
|
|
1481
|
+
// LC-P4: Data Checkup Tool
|
|
1482
|
+
// ============================================================================
|
|
1483
|
+
|
|
1484
|
+
registerTool(
|
|
1485
|
+
"data_checkup",
|
|
1486
|
+
"Run health checks on the memory database: vector dimensions, orphans, tier distribution, and conflict backlog. Read-only. Use when diagnosing data quality issues or before a consolidation run.",
|
|
1487
|
+
{},
|
|
1488
|
+
async () => {
|
|
1489
|
+
const openConflicts = (await conflictStore.listRecent({ status: "open", limit: 200 })).length;
|
|
1490
|
+
const report = await runDataCheckup({ store, openConflictCount: openConflicts });
|
|
1491
|
+
return {
|
|
1492
|
+
content: [{ type: "text" as const, text: formatCheckupReport(report) }],
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
// ============================================================================
|
|
1498
|
+
// Memory Lint Tool
|
|
1499
|
+
// ============================================================================
|
|
1500
|
+
|
|
1501
|
+
registerTool(
|
|
1502
|
+
"memory_lint",
|
|
1503
|
+
"Run memory quality lint checks: contradictions, duplicates, stale entries, and orphans. Read-only. Returns a health score (0-100) and actionable findings. Use for periodic memory hygiene or before consolidation.",
|
|
1504
|
+
{
|
|
1505
|
+
scope: z.string().min(1).max(160).optional().describe("Optional scope filter, e.g. 'project:recallnest'. Omit to lint all scopes"),
|
|
1506
|
+
verbose: z.boolean().default(false).describe("Include all individual findings in output (default: summarized)"),
|
|
1507
|
+
},
|
|
1508
|
+
async ({ scope, verbose }) => {
|
|
1509
|
+
const report = await runMemoryLint({ store, scope, verbose });
|
|
1510
|
+
return {
|
|
1511
|
+
content: [{ type: "text" as const, text: formatMemoryLintReport(report) }],
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
);
|
|
1515
|
+
|
|
1516
|
+
// ============================================================================
|
|
1517
|
+
// Knowledge Graph Export Tool
|
|
1518
|
+
// ============================================================================
|
|
1519
|
+
|
|
1520
|
+
registerTool(
|
|
1521
|
+
"export_graph",
|
|
1522
|
+
"Export memories as an interactive HTML knowledge graph. Creates a self-contained HTML file with a force-directed visualization. Open in any browser. Use when the user wants to visualize their memory network.",
|
|
1523
|
+
{
|
|
1524
|
+
scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
|
|
1525
|
+
maxNodes: z.number().int().min(10).max(500).default(200).describe("Maximum nodes to include (default 200)"),
|
|
1526
|
+
},
|
|
1527
|
+
async ({ scope, maxNodes }) => {
|
|
1528
|
+
const { path, graph } = await exportMemoryGraph(store, { scope, maxNodes });
|
|
1529
|
+
return {
|
|
1530
|
+
content: [{ type: "text" as const, text: formatGraphExportResult(path, graph) }],
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
// ============================================================================
|
|
1536
|
+
// AD-1: Dream Pipeline Tool
|
|
1537
|
+
// ============================================================================
|
|
1538
|
+
|
|
1539
|
+
registerTool(
|
|
1540
|
+
"dream",
|
|
1541
|
+
"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.",
|
|
1542
|
+
{
|
|
1543
|
+
scope: z.string().min(1).max(160).optional().describe("Scope to consolidate, e.g. 'project:myapp'. Omit to consolidate across all scopes"),
|
|
1544
|
+
force: z.boolean().default(false).describe("Set to true to force consolidation even if recent write count is below the automatic threshold"),
|
|
1545
|
+
},
|
|
1546
|
+
async ({ scope, force }) => {
|
|
1547
|
+
const resolvedScope = scope || "project:default";
|
|
1548
|
+
const components = getComponents();
|
|
1549
|
+
const result = await runDream({
|
|
1550
|
+
store: components.store,
|
|
1551
|
+
llm: components.llm,
|
|
1552
|
+
embedder: components.embedder,
|
|
1553
|
+
scope: resolvedScope,
|
|
1554
|
+
force,
|
|
1555
|
+
});
|
|
1556
|
+
return {
|
|
1557
|
+
content: [{ type: "text" as const, text: formatDreamResult(result) }],
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1273
1562
|
// ============================================================================
|
|
1274
1563
|
// Memory Drill-Down Tool
|
|
1275
1564
|
// ============================================================================
|
|
1276
1565
|
|
|
1277
1566
|
registerTool(
|
|
1278
1567
|
"memory_drill_down",
|
|
1279
|
-
"
|
|
1568
|
+
"Retrieve the full or overview-level content of a single memory entry. Read-only. Use when search returned compact summaries and you need the complete text or L1 overview.",
|
|
1280
1569
|
{
|
|
1281
|
-
id: z.string().describe("Memory ID or prefix (at least 8 hex chars)"),
|
|
1570
|
+
id: z.string().describe("Memory ID or unique prefix (at least 8 hex chars), e.g. 'a1b2c3d4'"),
|
|
1282
1571
|
level: z.enum(["overview", "full"]).optional().default("full")
|
|
1283
1572
|
.describe("Content depth: 'overview' (L1) or 'full' (L2, default)"),
|
|
1284
1573
|
},
|
|
@@ -1329,9 +1618,347 @@ registerTool(
|
|
|
1329
1618
|
},
|
|
1330
1619
|
);
|
|
1331
1620
|
|
|
1621
|
+
// ============================================================================
|
|
1622
|
+
// Skill Memory Tools (D-1)
|
|
1623
|
+
// ============================================================================
|
|
1624
|
+
|
|
1625
|
+
registerTool(
|
|
1626
|
+
"store_skill",
|
|
1627
|
+
"Store an executable skill with trigger conditions, implementation, and verification steps. Side effect: persists a new skill entry and indexes it. Use when you identify a reusable procedure worth automating across sessions.",
|
|
1628
|
+
{
|
|
1629
|
+
name: z.string().min(1).max(120).describe("Unique skill identifier, e.g. 'deploy_production' or 'run_migrations'"),
|
|
1630
|
+
description: z.string().min(1).max(500).describe("Natural language description of what the skill does (used for semantic retrieval matching)"),
|
|
1631
|
+
triggerPattern: z.string().min(1).max(300).describe("Natural language pattern describing when to suggest this skill, e.g. 'user asks to deploy to production'"),
|
|
1632
|
+
implementationType: SkillImplementationTypeSchema.describe("Execution type: 'bash' for shell scripts, 'python' for Python code, 'mcp_tool_chain' for MCP sequences, 'instruction_sequence' for step-by-step instructions"),
|
|
1633
|
+
implementation: z.string().min(1).max(5000).describe("Executable content: the actual script, code, or instruction steps to run"),
|
|
1634
|
+
inputSchema: z.record(z.string(), z.unknown()).optional().describe("JSON Schema defining the skill's input parameters, e.g. {\"env\": {\"type\": \"string\"}}"),
|
|
1635
|
+
verification: z.string().max(500).optional().describe("Steps to verify the skill executed correctly, e.g. 'check deployment URL returns 200'"),
|
|
1636
|
+
scope: z.string().min(1).max(160).describe("Scope to store the skill under, e.g. 'project:recallnest'"),
|
|
1637
|
+
source: z.enum(["manual", "agent", "api"]).default("agent").describe("How this skill was captured: 'manual' by user, 'agent' by AI, or 'api' programmatically"),
|
|
1638
|
+
tags: z.array(z.string().max(60)).max(6).default([]).describe("Optional categorization tags, e.g. ['deployment', 'production']"),
|
|
1639
|
+
},
|
|
1640
|
+
async ({ name, description, triggerPattern, implementationType, implementation, inputSchema, verification, scope, source, tags }) => {
|
|
1641
|
+
const { store, embedder } = getComponents();
|
|
1642
|
+
const stored = await persistSkill(store, embedder, {
|
|
1643
|
+
name,
|
|
1644
|
+
description,
|
|
1645
|
+
triggerPattern,
|
|
1646
|
+
implementationType,
|
|
1647
|
+
implementation,
|
|
1648
|
+
inputSchema,
|
|
1649
|
+
verification,
|
|
1650
|
+
scope,
|
|
1651
|
+
source,
|
|
1652
|
+
tags,
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
return {
|
|
1656
|
+
content: [{
|
|
1657
|
+
type: "text" as const,
|
|
1658
|
+
text: [
|
|
1659
|
+
`Stored skill ${stored.id.slice(0, 8)}`,
|
|
1660
|
+
`Name: ${stored.name}`,
|
|
1661
|
+
`Type: ${stored.implementationType}`,
|
|
1662
|
+
`Scope: ${stored.scope}`,
|
|
1663
|
+
`Tags: ${stored.tags.join(", ") || "-"}`,
|
|
1664
|
+
`Stored at: ${stored.storedAt}`,
|
|
1665
|
+
].join("\n"),
|
|
1666
|
+
}],
|
|
1667
|
+
};
|
|
1668
|
+
},
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
registerTool(
|
|
1672
|
+
"retrieve_skill",
|
|
1673
|
+
"Retrieve executable skills matching a task description by semantic similarity. Read-only. Use when you need a stored procedure to act on, not just recall knowledge.",
|
|
1674
|
+
{
|
|
1675
|
+
query: z.string().min(1).max(300).describe("Natural language task description to match, e.g. 'deploy the app to production'"),
|
|
1676
|
+
scope: z.string().min(1).max(160).optional().describe("Restrict to skills in a specific scope, e.g. 'project:myapp'. Omit to search all scopes"),
|
|
1677
|
+
limit: z.number().min(1).max(10).default(3).describe("Maximum number of matching skills to return, sorted by relevance (default: 3)"),
|
|
1678
|
+
},
|
|
1679
|
+
async ({ query, scope, limit }) => {
|
|
1680
|
+
const { store, embedder } = getComponents();
|
|
1681
|
+
const results = await retrieveSkills(store, embedder, query, scope, limit);
|
|
1682
|
+
|
|
1683
|
+
if (results.length === 0) {
|
|
1684
|
+
return {
|
|
1685
|
+
content: [{
|
|
1686
|
+
type: "text" as const,
|
|
1687
|
+
text: "No matching skills found.",
|
|
1688
|
+
}],
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const formatted = results.map(({ skill, score }, index) => [
|
|
1693
|
+
`## ${index + 1}. ${skill.name} (score: ${score.toFixed(3)})`,
|
|
1694
|
+
`**Description**: ${skill.description}`,
|
|
1695
|
+
`**Trigger**: ${skill.triggerPattern}`,
|
|
1696
|
+
`**Type**: ${skill.implementationType}`,
|
|
1697
|
+
skill.verification ? `**Verification**: ${skill.verification}` : null,
|
|
1698
|
+
`**Tags**: ${skill.tags.join(", ") || "-"}`,
|
|
1699
|
+
"",
|
|
1700
|
+
"```",
|
|
1701
|
+
skill.implementation,
|
|
1702
|
+
"```",
|
|
1703
|
+
].filter((line): line is string => line !== null).join("\n")).join("\n\n---\n\n");
|
|
1704
|
+
|
|
1705
|
+
return {
|
|
1706
|
+
content: [{
|
|
1707
|
+
type: "text" as const,
|
|
1708
|
+
text: `Found ${results.length} skill(s):\n\n${formatted}`,
|
|
1709
|
+
}],
|
|
1710
|
+
};
|
|
1711
|
+
},
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
// --- D-2: scan_skill_promotions tool ---
|
|
1715
|
+
registerTool(
|
|
1716
|
+
"scan_skill_promotions",
|
|
1717
|
+
"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.",
|
|
1718
|
+
{
|
|
1719
|
+
scope: z.string().min(1).max(160).describe("Project scope to scan for promotion candidates"),
|
|
1720
|
+
minOccurrences: z.number().min(2).max(20).default(3).describe("Minimum similar cases to trigger a promotion suggestion"),
|
|
1721
|
+
},
|
|
1722
|
+
async ({ scope, minOccurrences }) => {
|
|
1723
|
+
const { store } = getComponents();
|
|
1724
|
+
const result = await scanForPromotions(store, scope, {
|
|
1725
|
+
minCaseOccurrences: minOccurrences,
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{
|
|
1730
|
+
type: "text" as const,
|
|
1731
|
+
text: formatPromotionResult(result),
|
|
1732
|
+
}],
|
|
1733
|
+
};
|
|
1734
|
+
},
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
// --- MCP-1: list_tools tool ---
|
|
1738
|
+
registerTool(
|
|
1739
|
+
"list_tools",
|
|
1740
|
+
"List available RecallNest tools with one-line descriptions, filtered by tier. Read-only. Use when you need to discover advanced or governance tools beyond the core set.",
|
|
1741
|
+
{
|
|
1742
|
+
tier: z.enum(["core", "advanced", "full"]).default("advanced").optional()
|
|
1743
|
+
.describe("Which tier of tools to list. Returns tools at this tier and below."),
|
|
1744
|
+
},
|
|
1745
|
+
async ({ tier }) => {
|
|
1746
|
+
const requestedTier = tier ?? "advanced";
|
|
1747
|
+
const tierOrder: Record<string, number> = { core: 0, advanced: 1, governance: 2 };
|
|
1748
|
+
const maxOrder = requestedTier === "full" ? 2 : tierOrder[requestedTier] ?? 1;
|
|
1749
|
+
|
|
1750
|
+
const lines: string[] = [`Available tools (tier: ${requestedTier}):`];
|
|
1751
|
+
for (const [toolName, toolTier] of Object.entries(TOOL_TIERS)) {
|
|
1752
|
+
if ((tierOrder[toolTier] ?? 999) > maxOrder) continue;
|
|
1753
|
+
const desc = TOOL_DESCRIPTIONS.get(toolName);
|
|
1754
|
+
const oneLiner = desc
|
|
1755
|
+
? desc.split(/[.!]\s/)[0]?.slice(0, 100) ?? desc.slice(0, 100)
|
|
1756
|
+
: "(no description)";
|
|
1757
|
+
lines.push(`- ${toolName}: ${oneLiner}`);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return {
|
|
1761
|
+
content: [{
|
|
1762
|
+
type: "text" as const,
|
|
1763
|
+
text: lines.join("\n"),
|
|
1764
|
+
}],
|
|
1765
|
+
};
|
|
1766
|
+
},
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
// --- MCP-3: batch_store tool ---
|
|
1770
|
+
registerTool(
|
|
1771
|
+
"batch_store",
|
|
1772
|
+
"Store multiple memories in a single call with deduplication. Side effect: persists up to 20 entries. Use when you have several facts to store at once, more efficient than repeated store_memory calls.",
|
|
1773
|
+
{
|
|
1774
|
+
memories: z.array(z.object({
|
|
1775
|
+
text: z.string().min(1),
|
|
1776
|
+
category: DurableMemoryCategorySchema.default("events"),
|
|
1777
|
+
importance: z.number().min(0).max(1).default(0.7),
|
|
1778
|
+
tags: z.array(z.string()).max(6).default([]),
|
|
1779
|
+
})).min(1).max(20),
|
|
1780
|
+
scope: z.string().min(1).max(160),
|
|
1781
|
+
source: z.enum(["manual", "agent", "api"]).default("agent"),
|
|
1782
|
+
},
|
|
1783
|
+
async ({ memories, scope, source }) => {
|
|
1784
|
+
const { store, embedder } = getComponents();
|
|
1785
|
+
const stored = await persistMemoryBatch({
|
|
1786
|
+
store,
|
|
1787
|
+
embedder,
|
|
1788
|
+
conflictStore,
|
|
1789
|
+
kgExtractor,
|
|
1790
|
+
}, {
|
|
1791
|
+
scope,
|
|
1792
|
+
source,
|
|
1793
|
+
defaultImportance: 0.7,
|
|
1794
|
+
memories: memories.map((m) => ({
|
|
1795
|
+
text: m.text,
|
|
1796
|
+
category: m.category,
|
|
1797
|
+
importance: m.importance,
|
|
1798
|
+
tags: m.tags,
|
|
1799
|
+
})),
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
const counts = { new: 0, deduped: 0, updated: 0 };
|
|
1803
|
+
for (const r of stored) {
|
|
1804
|
+
if (r.disposition === "deduped") counts.deduped++;
|
|
1805
|
+
else if (r.disposition === "updated") counts.updated++;
|
|
1806
|
+
else counts.new++;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return {
|
|
1810
|
+
content: [{
|
|
1811
|
+
type: "text" as const,
|
|
1812
|
+
text: `Stored ${stored.length} memories (${counts.new} new, ${counts.deduped} deduped, ${counts.updated} updated)`,
|
|
1813
|
+
}],
|
|
1814
|
+
};
|
|
1815
|
+
},
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1818
|
+
// --- MP-2: import_conversations tool ---
|
|
1819
|
+
|
|
1820
|
+
registerTool(
|
|
1821
|
+
"import_conversations",
|
|
1822
|
+
"Import a conversation file (Claude Code JSONL, Claude.ai JSON, ChatGPT JSON, Slack JSON, or plaintext) into memory. Auto-detects format or use explicit format parameter. Messages are normalized and stored via the standard persistMemory pipeline.",
|
|
1823
|
+
{
|
|
1824
|
+
content: z.string().min(1).max(500_000).describe("Raw file content to import"),
|
|
1825
|
+
scope: z.string().min(1).max(160).describe("Target scope for imported memories, e.g. 'project:myapp'"),
|
|
1826
|
+
format: z.enum(["auto", "claude-code", "claude-ai", "chatgpt", "slack", "plaintext"]).default("auto").describe("Conversation format. Use 'auto' to detect automatically."),
|
|
1827
|
+
},
|
|
1828
|
+
async ({ content, scope, format }) => {
|
|
1829
|
+
const { detectFormat, normalizeConversation, ingestNormalizedMessages } = await import("./conversation-importer.js");
|
|
1830
|
+
const { embedder } = getComponents();
|
|
1831
|
+
|
|
1832
|
+
const resolvedFormat = format === "auto" ? detectFormat(content) : format;
|
|
1833
|
+
const messages = normalizeConversation(content, resolvedFormat);
|
|
1834
|
+
|
|
1835
|
+
if (messages.length === 0) {
|
|
1836
|
+
return {
|
|
1837
|
+
content: [{
|
|
1838
|
+
type: "text" as const,
|
|
1839
|
+
text: `No messages found (detected format: ${resolvedFormat})`,
|
|
1840
|
+
}],
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
const result = await ingestNormalizedMessages(
|
|
1845
|
+
{ store, embedder, llm, conflictStore, kgExtractor },
|
|
1846
|
+
messages,
|
|
1847
|
+
scope,
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
return {
|
|
1851
|
+
content: [{
|
|
1852
|
+
type: "text" as const,
|
|
1853
|
+
text: [
|
|
1854
|
+
`Import complete (format: ${resolvedFormat})`,
|
|
1855
|
+
`Total: ${result.total}`,
|
|
1856
|
+
`Stored: ${result.stored}`,
|
|
1857
|
+
`Rejected: ${result.rejected}`,
|
|
1858
|
+
result.errors.length > 0 ? `Errors: ${result.errors.join("; ")}` : null,
|
|
1859
|
+
].filter(Boolean).join("\n"),
|
|
1860
|
+
}],
|
|
1861
|
+
};
|
|
1862
|
+
},
|
|
1863
|
+
);
|
|
1864
|
+
|
|
1865
|
+
// --- S15: distill_session tool ---
|
|
1866
|
+
registerTool(
|
|
1867
|
+
"distill_session",
|
|
1868
|
+
"Distill a conversation session into structured knowledge and persist to long-term memory. Three layers: (1) microcompact clears old tool results at zero cost, (2) LLM summarizes into 9 dimensions, (3) extracts durable knowledge into RecallNest. Use when a session is ending or context is getting large. Side effect: persists extracted memories.",
|
|
1869
|
+
{
|
|
1870
|
+
messages: z.array(z.object({
|
|
1871
|
+
role: z.enum(["user", "assistant", "system"]),
|
|
1872
|
+
content: z.union([
|
|
1873
|
+
z.string(),
|
|
1874
|
+
z.array(z.object({
|
|
1875
|
+
type: z.enum(["text", "tool_use", "tool_result"]),
|
|
1876
|
+
name: z.string().optional(),
|
|
1877
|
+
id: z.string().optional(),
|
|
1878
|
+
input: z.record(z.unknown()).optional(),
|
|
1879
|
+
content: z.string().optional(),
|
|
1880
|
+
text: z.string().optional(),
|
|
1881
|
+
tool_use_id: z.string().optional(),
|
|
1882
|
+
})),
|
|
1883
|
+
]),
|
|
1884
|
+
})).min(1).max(500).describe("Conversation messages to distill"),
|
|
1885
|
+
scope: z.string().min(1).max(160).describe("Memory scope for persisted knowledge, e.g. 'project:recallnest'"),
|
|
1886
|
+
preserveRecent: z.number().min(0).max(20).default(6).describe("Keep the N most recent messages verbatim (default: 6)"),
|
|
1887
|
+
keepRecentTools: z.number().min(0).max(20).default(5).describe("Keep the N most recent tool results during microcompact (default: 5)"),
|
|
1888
|
+
persist: z.boolean().default(true).describe("Whether to persist extracted knowledge to RecallNest (default: true)"),
|
|
1889
|
+
},
|
|
1890
|
+
async ({ messages, scope, preserveRecent, keepRecentTools, persist }) => {
|
|
1891
|
+
const { store, embedder } = getComponents();
|
|
1892
|
+
const { distillSession } = await import("./session-distiller.js");
|
|
1893
|
+
|
|
1894
|
+
const result = await distillSession(
|
|
1895
|
+
{ messages, scope, preserveRecent, keepRecentTools, persist },
|
|
1896
|
+
{
|
|
1897
|
+
llm: llmClient,
|
|
1898
|
+
persistMemory: async (input) => {
|
|
1899
|
+
const stored = await persistMemory({
|
|
1900
|
+
store, embedder, conflictStore, kgExtractor,
|
|
1901
|
+
}, input);
|
|
1902
|
+
return { disposition: stored.disposition, id: stored.id };
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
const lines = [
|
|
1908
|
+
`Microcompact: ${result.microcompact.toolsCleared} tool results cleared, ~${result.microcompact.tokensFreed} tokens freed`,
|
|
1909
|
+
];
|
|
1910
|
+
if (result.summary) {
|
|
1911
|
+
lines.push(`Summary: 9-dimension structured summary generated`);
|
|
1912
|
+
}
|
|
1913
|
+
if (result.persisted) {
|
|
1914
|
+
const p = result.persisted;
|
|
1915
|
+
lines.push(`Persisted: ${p.memoriesStored} stored, ${p.memoriesDeduped} deduped, ${p.memoriesConflicted} conflicted, ${p.memoriesRejected} rejected`);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
return {
|
|
1919
|
+
content: [{
|
|
1920
|
+
type: "text" as const,
|
|
1921
|
+
text: lines.join("\n"),
|
|
1922
|
+
}],
|
|
1923
|
+
};
|
|
1924
|
+
},
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
// ============================================================================
|
|
1928
|
+
// Global error handlers — prevent silent crashes from unhandled async errors
|
|
1929
|
+
// ============================================================================
|
|
1930
|
+
|
|
1931
|
+
process.on("unhandledRejection", (reason) => {
|
|
1932
|
+
console.error("[recallnest] Unhandled promise rejection:", reason instanceof Error ? reason.stack || reason.message : String(reason));
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
process.on("uncaughtException", (err) => {
|
|
1936
|
+
console.error("[recallnest] Uncaught exception:", err.stack || err.message);
|
|
1937
|
+
// Give stderr a chance to flush before exiting
|
|
1938
|
+
setTimeout(() => process.exit(1), 100);
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1332
1941
|
// ============================================================================
|
|
1333
1942
|
// Start
|
|
1334
1943
|
// ============================================================================
|
|
1335
1944
|
|
|
1945
|
+
// Auto-register babel-memory language processor if installed (non-blocking)
|
|
1946
|
+
autoRegisterBabelMemory().then((ok) => {
|
|
1947
|
+
if (ok) console.error("[recallnest] babel-memory registered");
|
|
1948
|
+
}).catch((err) => {
|
|
1949
|
+
console.error("[recallnest] babel-memory registration failed:", err instanceof Error ? err.message : String(err));
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1336
1952
|
const transport = new StdioServerTransport();
|
|
1337
|
-
|
|
1953
|
+
const CONNECT_TIMEOUT_MS = 30_000;
|
|
1954
|
+
try {
|
|
1955
|
+
await Promise.race([
|
|
1956
|
+
server.connect(transport),
|
|
1957
|
+
new Promise<never>((_, reject) =>
|
|
1958
|
+
setTimeout(() => reject(new Error("MCP server.connect() timed out after 30s")), CONNECT_TIMEOUT_MS)
|
|
1959
|
+
),
|
|
1960
|
+
]);
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
console.error("[recallnest] Fatal: MCP connection failed:", err instanceof Error ? err.message : String(err));
|
|
1963
|
+
process.exit(1);
|
|
1964
|
+
}
|