Skip to content
Closed
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
7 changes: 7 additions & 0 deletions Memory/src/algorithm/plugin-algorithms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4426,6 +4426,13 @@ export function signatureFromTraceParts(
return signatureFromTraceLike(tags, toolCalls, reflection);
}

export function isBucketableSignature(signature: string): boolean {
return signature
.split("|")
.map((part) => part.trim())
.some((part) => part.length > 0 && part !== "_");
}

export function l2CandidateSignatureHash(signature: string): string {
let hash = 5381;
for (let index = 0; index < signature.length; index += 1) {
Expand Down
13 changes: 12 additions & 1 deletion Memory/src/service/evolution/policy-induction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
L2_INDUCTION_PROMPT,
buildPolicyDraft,
detectDominantLanguage,
isBucketableSignature,
l2CandidateIdFor,
languageSteeringLine,
packL2InductionTraces,
Expand Down Expand Up @@ -192,6 +193,14 @@ export class PolicyInductionEngine {
);

for (const signature of signatures) {
if (!isBucketableSignature(signature)) {
logEvolutionDecision(job, "l2_induction", "gate_not_met", {
sourceMemoryId: source.id,
reason: "non_discriminative_signature",
signature
});
continue;
}
const bucket = uniq(
pendingCandidates
.filter((candidate) => candidate.candidateKey === signature)
Expand Down Expand Up @@ -796,6 +805,7 @@ export class PolicyInductionEngine {
signature: string,
at: string
): void {
if (!isBucketableSignature(signature)) return;
const id = l2CandidateIdFor(signature, trace.id);
this.deps.repos.runtime.upsertCandidatePoolTrace({
id,
Expand Down Expand Up @@ -823,7 +833,8 @@ export class PolicyInductionEngine {
trace.memory.properties.internal_info.evidence_status !== "provisional" &&
trace.memory.properties.internal_info.evidence_status !== "disputed" &&
trace.value >= this.deps.config.algorithm.l2Induction.minTraceValue &&
Boolean(trace.vecSummary ?? trace.vecAction);
Boolean(trace.vecSummary ?? trace.vecAction) &&
isBucketableSignature(signatureFromTrace(trace));
}

private markCandidatePoolPromoted(
Expand Down
13 changes: 13 additions & 0 deletions Memory/tests/algorithm/plugin-algorithms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
retrievePluginMemories,
retrievalLayersForMode,
retrievalLayersForProfile,
isBucketableSignature,
signatureFromTraceParts,
traceMetaFromMemory,
type PolicyMemoryMeta,
Expand Down Expand Up @@ -1037,6 +1038,18 @@ describe("plugin algorithm parity helpers", () => {
}], "reflection saw EXIT_2")).toBe("shell|_|shell|EXIT_2");
});

it("keeps empty no-tool signatures unbucketable and leaves tagged no-tool signatures bucketable", () => {
expect(signatureFromTraceParts([], [], "")).toBe("_|_|_|_");
expect(signatureFromTraceParts([], [], "你好,今天天气怎么样")).toBe("_|_|_|_");
expect(isBucketableSignature("_|_|_|_")).toBe(false);
expect(isBucketableSignature("")).toBe(false);
expect(isBucketableSignature("_|_|_")).toBe(false);
expect(isBucketableSignature("error|python|_|_")).toBe(true);
expect(isBucketableSignature("compact|summary|_|_")).toBe(true);
expect(isBucketableSignature("python|pytest|pytest|EXIT_1")).toBe(true);
expect(signatureFromTraceParts(["error", "python"], [], "")).toBe("error|python|_|_");
});

it("uses the plugin skill lifecycle prior when resolving trials", () => {
expect(skillEtaAfterTrial({
currentEta: 0.4,
Expand Down
106 changes: 106 additions & 0 deletions Memory/tests/service/evolution/policy-induction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,112 @@ describe("MemoryService / evolution / policy induction", () => {

db.close();
});

it("does not bucket fully wildcard L1 signatures into the L2 candidate pool", async () => {
const { db, service } = createTestService({
skillLlm: createCapturingL2Llm([]),
config: {
...DEFAULT_MEMMY_CONFIG,
algorithm: {
...DEFAULT_MEMMY_CONFIG.algorithm,
l2Induction: {
...DEFAULT_MEMMY_CONFIG.algorithm.l2Induction,
minEpisodesForInduction: 1,
minGain: -1
}
}
}
});
const session = service.openSession({
namespace: {
source: "codex",
profileId: "jiang",
userId: "wildcard-l1-user"
},
workspaceId: "wildcard-l1-workspace"
});
const first = service.completeTurn("turn-wildcard-email", {
sessionId: session.sessionId,
episodeId: "episode-wildcard-email",
query: "帮我写一封感谢客户的邮件",
answer: "感谢您一直以来的支持,后续我们会继续跟进。"
});
const second = service.completeTurn("turn-wildcard-report", {
sessionId: session.sessionId,
episodeId: "episode-wildcard-report",
query: "把这三点整理成一段给领导的汇报",
answer: "本周已完成客户回访、方案修订和交付排期。"
});
const preference = service.completeTurn("turn-wildcard-preference", {
sessionId: session.sessionId,
episodeId: "episode-wildcard-preference",
query: "我喜欢吃苹果",
answer: "记下了,你喜欢吃苹果。"
});
expect(first.l1MemoryId).toBeTruthy();
expect(second.l1MemoryId).toBeTruthy();

for (const turn of [first, second]) {
makeTraceEligibleForL2(db, turn.l1MemoryId);
setTraceSignatureAndVectorForTest(db, turn.l1MemoryId, "_|_|_|_", [1, 0, 0]);
await addPositiveFeedbackForTurn(service, session.sessionId, turn);
}
service.closeSession(session.sessionId);
await runWorkerRounds(service, 8, 50);

expect(traceSignatureForTest(db, first.l1MemoryId)).toBe("_|_|_|_");
expect(traceSignatureForTest(db, second.l1MemoryId)).toBe("_|_|_|_");
expect(db.db.prepare(`SELECT status FROM memories WHERE id = ?`).get(first.l1MemoryId))
.toEqual({ status: "activated" });
const userMemories = db.db.prepare(
`SELECT content FROM user_memories WHERE status = 'active' ORDER BY created_at`
).all() as Array<{ content: string }>;
expect(userMemories.map((memory) => memory.content)).toEqual(["我喜欢吃苹果"]);
expect(preference.userMemoryIds.length).toBeGreaterThan(0);
expect(db.db.prepare(
`SELECT COUNT(*) AS count FROM l2_candidate_pool WHERE candidate_key = '_|_|_|_'`
).get()).toEqual({ count: 0 });
expect(db.db.prepare(
`SELECT COUNT(*) AS count FROM memories WHERE memory_layer = 'L2'`
).get()).toEqual({ count: 0 });

const at = new Date().toISOString();
db.db.prepare(
`INSERT INTO l2_candidate_pool (
id, user_id, session_id, source_memory_id, candidate_key,
candidate_value, score, status, evidence_json, created_at, updated_at, expires_at
) VALUES (?, ?, ?, ?, '_|_|_|_', 'stale wildcard', 1, 'pending', '[]', ?, ?, ?)`
).run(
l2CandidateIdFor("_|_|_|_", first.l1MemoryId),
"wildcard-l1-user",
session.sessionId,
first.l1MemoryId,
at,
at,
new Date(Date.parse(at) + 86_400_000).toISOString()
);
db.db.prepare(
`INSERT INTO evolution_jobs (
id, job_type, status, user_id, session_id, episode_id, target_memory_id,
payload_json, attempts, max_attempts, created_at, updated_at
) VALUES (?, 'l2_induction', 'queued', ?, ?, ?, ?, ?, 0, 3, ?, ?)`
).run(
"job_wildcard_l2",
"wildcard-l1-user",
session.sessionId,
first.episodeId,
first.l1MemoryId,
JSON.stringify({ sourceMemoryId: first.l1MemoryId }),
at,
at
);
await runWorkerRounds(service, 4, 20);

expect(db.db.prepare(
`SELECT COUNT(*) AS count FROM memories WHERE memory_layer = 'L2'`
).get()).toEqual({ count: 0 });
db.close();
});
});

function createBc08SummaryLlm(): LlmClient {
Expand Down