Skip to content

Commit 8eca78b

Browse files
AliceLJYclaude
andcommitted
feat: add recallMode config (full/summary/off) for resume_context
Allow users to control how much context resume_context returns: - full: existing compose behavior (stable + patterns + cases + guidance) - summary: checkpoint-only, lightweight (suitable for dev scenarios) - off: no recall, guides agent to use search_memory on demand Priority resolution: per-call override > RECALLNEST_RECALL_MODE env > config.recallMode > default ("summary") Motivation: auto-recall conflicts with compact in complex dev sessions, wastes attention budget, and reduces signal-to-noise ratio per Anthropic/OpenAI context engineering recommendations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea5154f commit 8eca78b

4 files changed

Lines changed: 132 additions & 2 deletions

File tree

‎config.json.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"dbPath": "~/.recallnest/data/lancedb",
3+
"recallMode": "summary",
34
"embedding": {
45
"provider": "jina",
56
"apiKey": "${JINA_API_KEY}",

‎src/__tests__/recall-mode.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2+
import { resolveRecallMode, type LocalMemoryConfig } from "../runtime-config.js";
3+
4+
const baseConfig: LocalMemoryConfig = {
5+
dbPath: "./data/lancedb",
6+
embedding: {
7+
provider: "jina",
8+
apiKey: "test",
9+
model: "test-model",
10+
},
11+
sources: {},
12+
};
13+
14+
describe("resolveRecallMode", () => {
15+
const originalEnv = process.env.RECALLNEST_RECALL_MODE;
16+
17+
beforeEach(() => {
18+
delete process.env.RECALLNEST_RECALL_MODE;
19+
});
20+
21+
afterEach(() => {
22+
if (originalEnv === undefined) {
23+
delete process.env.RECALLNEST_RECALL_MODE;
24+
} else {
25+
process.env.RECALLNEST_RECALL_MODE = originalEnv;
26+
}
27+
});
28+
29+
it("defaults to 'summary' when no config, no env, no override", () => {
30+
delete process.env.RECALLNEST_RECALL_MODE;
31+
expect(resolveRecallMode(baseConfig)).toBe("summary");
32+
});
33+
34+
it("reads from config.recallMode", () => {
35+
delete process.env.RECALLNEST_RECALL_MODE;
36+
expect(resolveRecallMode({ ...baseConfig, recallMode: "full" })).toBe("full");
37+
expect(resolveRecallMode({ ...baseConfig, recallMode: "off" })).toBe("off");
38+
});
39+
40+
it("env var overrides config", () => {
41+
process.env.RECALLNEST_RECALL_MODE = "off";
42+
expect(resolveRecallMode({ ...baseConfig, recallMode: "full" })).toBe("off");
43+
});
44+
45+
it("per-call override takes highest priority", () => {
46+
process.env.RECALLNEST_RECALL_MODE = "off";
47+
expect(resolveRecallMode({ ...baseConfig, recallMode: "full" }, "summary")).toBe("summary");
48+
});
49+
50+
it("ignores invalid per-call override, falls back to env", () => {
51+
process.env.RECALLNEST_RECALL_MODE = "full";
52+
expect(resolveRecallMode(baseConfig, "invalid")).toBe("full");
53+
});
54+
55+
it("ignores invalid env var, falls back to config", () => {
56+
process.env.RECALLNEST_RECALL_MODE = "garbage";
57+
expect(resolveRecallMode({ ...baseConfig, recallMode: "off" })).toBe("off");
58+
});
59+
60+
it("ignores invalid env var and no config, falls back to default", () => {
61+
process.env.RECALLNEST_RECALL_MODE = "garbage";
62+
expect(resolveRecallMode(baseConfig)).toBe("summary");
63+
});
64+
});

‎src/mcp-server.ts‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ import type { MemoryStore } from "./store.js";
7272
import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
7373
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, listDirtyBriefAssets, listMemoryAssets, listPinAssets, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
7474
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
75-
import { createComponentResolver, loadConfig, loadDotEnv } from "./runtime-config.js";
75+
import { createComponentResolver, loadConfig, loadDotEnv, resolveRecallMode } from "./runtime-config.js";
7676
import { DurableMemoryCategorySchema, StoreMemorySourceSchema } from "./memory-schema.js";
7777
import { persistCaseMemory, persistMemory, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
7878
import { buildSessionCheckpointResult } from "./session-engine.js";
@@ -613,8 +613,54 @@ registerTool(
613613
limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
614614
includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
615615
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
616+
mode: z.enum(["full", "summary", "off"]).optional().describe("Override recall mode (default: from config.recallMode)"),
616617
},
617-
async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName }) => {
618+
async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
619+
const effectiveMode = resolveRecallMode(config, modeOverride);
620+
621+
// --- off mode: no recall, guide agent to use search_memory ---
622+
if (effectiveMode === "off") {
623+
return {
624+
content: [{
625+
type: "text" as const,
626+
text: "Recall mode is off. Use search_memory to retrieve specific memories on demand.",
627+
}],
628+
};
629+
}
630+
631+
// --- summary mode: checkpoint only, lightweight ---
632+
if (effectiveMode === "summary") {
633+
const scopeSelection = resolveScopeSelection({
634+
scope,
635+
sessionId,
636+
operation: "resume_context",
637+
allowUnscoped: true,
638+
});
639+
const latest = await checkpointStore.getLatest({
640+
sessionId,
641+
scope: scopeSelection.resolvedScope,
642+
});
643+
const summaryText = formatCheckpointSummary(latest) +
644+
"\n\nFor detailed recall, use search_memory with specific queries.";
645+
await saveManagedObservation({
646+
workflowId: "resume_context",
647+
outcome: "success",
648+
summary: `Managed resume_context returned summary-mode checkpoint${latest ? "" : " (none found)"}.`,
649+
scope: scopeSelection.resolvedScope || scope || "global",
650+
source: "managed:recallnest",
651+
signal: "managed-resume-summary",
652+
task,
653+
tags: ["managed", "recallnest", "summary-mode"],
654+
});
655+
return {
656+
content: [{
657+
type: "text" as const,
658+
text: summaryText,
659+
}],
660+
};
661+
}
662+
663+
// --- full mode: existing compose behavior ---
618664
const { retriever, profile } = getComponents(profileName);
619665
const scopeSelection = resolveScopeSelection({
620666
scope,

‎src/runtime-config.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import { AccessTracker } from "./access-tracker.js";
1010
import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
1111
import { logInfo } from "./stderr-log.js";
1212

13+
export type RecallMode = "full" | "summary" | "off";
14+
1315
export interface LocalMemoryConfig {
1416
dbPath: string;
17+
recallMode?: RecallMode;
1518
embedding: {
1619
provider: string;
1720
apiKey: string;
@@ -143,3 +146,19 @@ export function createComponentResolver(config: LocalMemoryConfig) {
143146
return created;
144147
};
145148
}
149+
150+
const VALID_RECALL_MODES: RecallMode[] = ["full", "summary", "off"];
151+
152+
/**
153+
* Resolve effective recall mode: per-call override > env var > config > default ("summary").
154+
*/
155+
export function resolveRecallMode(config: LocalMemoryConfig, perCallOverride?: string): RecallMode {
156+
if (perCallOverride && VALID_RECALL_MODES.includes(perCallOverride as RecallMode)) {
157+
return perCallOverride as RecallMode;
158+
}
159+
const envMode = process.env.RECALLNEST_RECALL_MODE;
160+
if (envMode && VALID_RECALL_MODES.includes(envMode as RecallMode)) {
161+
return envMode as RecallMode;
162+
}
163+
return config.recallMode ?? "summary";
164+
}

0 commit comments

Comments
 (0)