Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,11 @@ describe("stored memory -> panel/detail -> rendered trace title", () => {
expect(detailFromMemory(stored).body).toContain(input);
});

it("refreshes existing log summaries to empty without replacing missing records or span goals", () => {
it("refreshes existing log summaries to empty without replacing missing records or span goals", async () => {
const { db, service } = createTestService();
const repos = new Repositories(db.db);
const session = service.openSession({ namespace: { source: "codex", profileId: "default", userId: "review-log" } });
const completed = service.completeTurn("preview-log-turn", { sessionId: session.sessionId, query: request, answer: "完成。" });
const completed = await service.completeTurn("preview-log-turn", { sessionId: session.sessionId, query: request, answer: "完成。" });
const storedLog = db.db.prepare("SELECT output_json FROM api_logs WHERE tool_name = 'memory_add'").get() as { output_json: string };
expect(JSON.parse(storedLog.output_json).details[0].summary).toBe("");
const row = repos.memories.get(completed.l1MemoryId)!;
Expand Down
5 changes: 5 additions & 0 deletions Memory/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export interface AgentAccessConfig {
export interface AlgorithmConfig {
enableMemoryAdd: boolean;
enableMemorySearch: boolean;
/** Controls agent-facing User Memory recall; capture and management remain available. */
enableUserMemoryRecall: boolean;
enableQueryRewrite: boolean;
capture: {
maxTextChars: number;
Expand Down Expand Up @@ -384,6 +386,7 @@ export const DEFAULT_MEMMY_CONFIG: MemmyConfig = {
algorithm: {
enableMemoryAdd: true,
enableMemorySearch: true,
enableUserMemoryRecall: false,
enableQueryRewrite: false,
capture: {
maxTextChars: 4_000,
Expand Down Expand Up @@ -645,6 +648,7 @@ function configFromEnv(): Record<string, unknown> {
algorithm: compactRecord({
enableMemoryAdd: booleanEnv("MEMMY_ENABLE_MEMORY_ADD"),
enableMemorySearch: booleanEnv("MEMMY_ENABLE_MEMORY_SEARCH"),
enableUserMemoryRecall: booleanEnv("MEMMY_ENABLE_USER_MEMORY_RECALL"),
enableQueryRewrite: booleanEnv("MEMMY_ENABLE_QUERY_REWRITE"),
retrieval: compactRecord({
readOnlyInjectionProfile:
Expand Down Expand Up @@ -1161,6 +1165,7 @@ function normalizeAlgorithm(input: Record<string, unknown>): AlgorithmConfig {
return {
enableMemoryAdd: booleanValue(input.enableMemoryAdd, DEFAULT_MEMMY_CONFIG.algorithm.enableMemoryAdd),
enableMemorySearch: booleanValue(input.enableMemorySearch, DEFAULT_MEMMY_CONFIG.algorithm.enableMemorySearch),
enableUserMemoryRecall: booleanValue(input.enableUserMemoryRecall, DEFAULT_MEMMY_CONFIG.algorithm.enableUserMemoryRecall),
enableQueryRewrite: booleanValue(input.enableQueryRewrite, DEFAULT_MEMMY_CONFIG.algorithm.enableQueryRewrite),
capture: {
maxTextChars: numberValue(capture.maxTextChars, DEFAULT_MEMMY_CONFIG.algorithm.capture.maxTextChars),
Expand Down
8 changes: 7 additions & 1 deletion Memory/src/service/memory-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,8 +1297,13 @@ export class MemoryService {
if (!this.memoryAddEnabled()) {
return scopedRun();
}
// Recall responses depend on this gate. Do not replay a pre-gate response
// or a response produced while the opposite setting was active.
const recallPolicyKey = operation === "turn.start" || operation === "memory.search"
? `:user-memory-recall:${this.config.algorithm.enableUserMemoryRecall === true}`
: "";
const idempotencyKey = request.adapterId && request.requestId
? `${operation}:${request.adapterId}:${request.requestId}`
? `${operation}:${request.adapterId}:${request.requestId}${recallPolicyKey}`
: undefined;
if (!idempotencyKey) {
return scopedRun();
Expand Down Expand Up @@ -4152,6 +4157,7 @@ function memoryConfigLogFields(config: MemmyConfig): Record<string, unknown> {
embeddingMode: config.embedding.mode,
memoryAddEnabled: config.algorithm.enableMemoryAdd,
memorySearchEnabled: config.algorithm.enableMemorySearch,
userMemoryRecallEnabled: config.algorithm.enableUserMemoryRecall,
summaryModel: {
provider: config.summary.provider,
vendor: config.summary.vendor,
Expand Down
12 changes: 9 additions & 3 deletions Memory/src/service/retrieval/retrieval-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1932,6 +1932,9 @@ export class RetrievalService {
? requestedSemanticLayers.filter((layer) => layer !== "L1")
: requestedSemanticLayers;
const searchAt = Date.now();
const userMemoryRecallEnabled = this.deps.config.algorithm.enableUserMemoryRecall === true;
// Capture uses these candidates to confirm or correct existing User Memory,
// even when it must not be returned to an agent.
const includeUserMemory = !onboardingFirstReportHit && semanticLayers.includes("L1");
const userMemoryCount = includeUserMemory
? this.deps.repos.userMemories.listActive(context.userId).length
Expand Down Expand Up @@ -2022,7 +2025,9 @@ export class RetrievalService {
const agentHits = onboardingFirstReportHit || timeFilter
? retrieval.hits
: filterL1TraceSpanRecallHits(retrieval.hits, memories);
const merged = mergeSameTurnRecallHits(agentHits, memories, userMemoryOutput.hits);
const merged = mergeSameTurnRecallHits(
agentHits, memories, userMemoryRecallEnabled ? userMemoryOutput.hits : []
);
const rerankAt = Date.now();
const filteredHits = onboardingFirstReportHit
? { hits: retrieval.hits, status: ["first_report_handoff:latest_only"] }
Expand Down Expand Up @@ -2060,7 +2065,7 @@ export class RetrievalService {
.map((memory) => memory.id);
const candidateMemoryIds = uniq([
...memories.map((memory) => memory.id),
...userMemoryCandidateIds
...(userMemoryRecallEnabled ? userMemoryCandidateIds : [])
]);
const sourceMemoryIds = contextPacket.sourceMemoryIds;
const hitIds = new Set(hits.flatMap((hit) => hit.memberMemoryIds ?? [hit.id]));
Expand All @@ -2075,7 +2080,7 @@ export class RetrievalService {
memoryLayer: memory.memoryLayer,
reason: "rank_threshold" as const
})),
...userMemoryOutput.memories
...(userMemoryRecallEnabled ? userMemoryOutput.memories : [])
.filter((memory) => !hitIds.has(memory.id))
.slice(0, 50)
.map((memory) => ({
Expand Down Expand Up @@ -2145,6 +2150,7 @@ export class RetrievalService {
},
status: uniq([
...filteredHits.status,
...(!userMemoryRecallEnabled ? ["user_memory_recall:disabled"] : []),
...(dynamicCurrentQuery ? ["dynamic_current:refresh_required"] : []),
...(stalePolicyIds.size > 0 ? ["policy:revalidation_required"] : []),
...(!this.deps.memoryAddEnabled() ? ["memory_add:disabled:no_recall_log"] : [])
Expand Down
19 changes: 17 additions & 2 deletions Memory/src/storage/queue-scheduling-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export interface PolicyRefreshResult {

/** A metadata projection and read-only candidate adapter; never claims or completes jobs. */
export class QueueSchedulingStore {
// Cache statements, never eligibility results; each store owns one connection.
private readonly eligibilityStatements = new Map<string, Database.Statement>();

constructor(private readonly db: Database.Database) {}

getDeadlineRevision(): { revision: number; rescanRevision: number } {
Expand All @@ -68,8 +71,20 @@ export class QueueSchedulingStore {
const condition = candidate.queue === "main" ? mainQueueEligibility(at, options)
: retryQueueEligibility(Date.parse(at), options.targetMemoryIds);
const table = candidate.queue === "main" ? "evolution_jobs" : "embedding_retry_queue q";
return !!this.db.prepare(`SELECT 1 FROM ${table} WHERE ${condition.sql} AND id = ? AND enqueue_seq = ?`)
.get(...condition.params, candidate.id, candidate.enqueueSeq);
const sql = `SELECT 1 FROM ${table} WHERE ${condition.sql} AND id = ? AND enqueue_seq = ?`;
let statement = this.eligibilityStatements.get(sql);
if (statement) {
this.eligibilityStatements.delete(sql);
} else {
statement = this.db.prepare(sql);
// Filters can produce different placeholder counts. Keep the cache bounded.
if (this.eligibilityStatements.size >= 32) {
const oldest = this.eligibilityStatements.keys().next().value;
if (oldest !== undefined) this.eligibilityStatements.delete(oldest);
}
}
this.eligibilityStatements.set(sql, statement);
return !!statement.get(...condition.params, candidate.id, candidate.enqueueSeq);
}

readSnapshot<T>(read: () => T): T {
Expand Down
23 changes: 23 additions & 0 deletions Memory/tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("memmy memory config", () => {

expect(loadMemmyConfig(configPath).config.algorithm.enableMemoryAdd).toBe(true);
expect(loadMemmyConfig(configPath).config.algorithm.enableMemorySearch).toBe(true);
expect(loadMemmyConfig(configPath).config.algorithm.enableUserMemoryRecall).toBe(false);
expect(loadMemmyConfig(configPath).config.algorithm.enableQueryRewrite).toBe(false);
expect(loadMemmyConfig(configPath).config.algorithm).not.toHaveProperty("lightweightMemory");
expect(loadMemmyConfig(configPath).config).not.toHaveProperty("logging");
Expand Down Expand Up @@ -105,6 +106,28 @@ describe("memmy memory config", () => {
});
});

it.each([undefined, null, "true", 1, {}, false, true])(
"only enables User Memory recall for an explicit boolean true (%j)", (enableUserMemoryRecall) => {
const configPath = join(tempRoot(), "config.yaml");
writeFileSync(configPath, YAML.stringify({
memmyMemory: { algorithm: { enableUserMemoryRecall } }
}));
expect(loadMemmyConfig(configPath).config.algorithm.enableUserMemoryRecall)
.toBe(enableUserMemoryRecall === true);
}
);

it("allows the environment to disable and re-enable User Memory recall", () => {
const configPath = join(tempRoot(), "config.yaml");
writeFileSync(configPath, YAML.stringify({
memmyMemory: { algorithm: { enableUserMemoryRecall: true } }
}));
setEnv("MEMMY_ENABLE_USER_MEMORY_RECALL", "false");
expect(loadMemmyConfig(configPath).config.algorithm.enableUserMemoryRecall).toBe(false);
setEnv("MEMMY_ENABLE_USER_MEMORY_RECALL", "true");
expect(loadMemmyConfig(configPath).config.algorithm.enableUserMemoryRecall).toBe(true);
});

it("reads query extract history limits from retrieval config and defaults them", () => {
const root = tempRoot();
const configPath = join(root, "config.yaml");
Expand Down
1 change: 1 addition & 0 deletions Memory/tests/fixtures/memory-service-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export function addAgentSourceImport(
export function configWithMemoryGates(gates: {
enableMemoryAdd?: boolean;
enableMemorySearch?: boolean;
enableUserMemoryRecall?: boolean;
enableQueryRewrite?: boolean;
}): typeof DEFAULT_MEMMY_CONFIG {
return {
Expand Down
Loading
Loading