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
28 changes: 28 additions & 0 deletions apps/memos-local-plugin/core/memory/l3/l3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export interface RunL3Deps {

const KV_COOLDOWN_PREFIX = "l3.lastRun.";

/**
* Catch-all cluster key emitted by `cluster.ts::domainKeyOf` when a
* policy matches none of the TAG / TOOL regexes. Clusters with this key
* have no shared organising principle; L3 pre-filters them before
* calling the abstractor to avoid the reliably-empty-title failure mode
* documented in issue #2374.
*/
const UNTAGGED_CLUSTER_KEY = "_|_";

// ─── Public entry ──────────────────────────────────────────────────────────

export async function runL3(
Expand Down Expand Up @@ -142,6 +151,25 @@ export async function runL3(
continue;
}

// Untagged clusters — no TAG_REGEX or TOOL_REGEX matched any member,
// so `domainKeyOf` returned the catch-all `"_|_"` bucket. These have
// no shared organising principle; the abstractor's `DOMAIN_TAGS: -`
// prompt reliably yields empty titles and 100% `llm_failed` in the
// field (issue #2374). Pre-filter before we consult cooldown or spend
// an LLM round-trip.
if (cluster.key === UNTAGGED_CLUSTER_KEY) {
abstractLog.info("untagged.skipped", {
clusterKey: cluster.key,
clusterPolicyCount: cluster.policies.length,
});
abstractions.push(
skipped(cluster, "untagged_cluster", {
policyIds: cluster.policies.map((p) => p.id),
}),
);
continue;
}

if (isInCooldown(cluster, repos.kv, config.cooldownDays, now)) {
abstractLog.info("cooldown.skipped", {
clusterKey: cluster.key,
Expand Down
1 change: 1 addition & 0 deletions apps/memos-local-plugin/core/memory/l3/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export interface AbstractionResult {
| "draft_invalid"
| "cooldown"
| "no_centroid"
| "untagged_cluster"
| "duplicate_of";
/** When `skippedReason === "duplicate_of"`, the existing WM id. */
duplicateOfWorldId?: WorldModelId | null;
Expand Down
175 changes: 175 additions & 0 deletions apps/memos-local-plugin/tests/unit/memory/l3/l3.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,181 @@ describe("memory/l3/integration", () => {
expect(handle.repos.worldModel.list().length).toBe(0);
});

it("skips the '_|_' untagged cluster before calling the LLM (issue #2374)", async () => {
// Seed three policies whose text matches none of the domain regexes,
// so they all land in the "_|_" bucket. Historically this cluster
// was passed to `abstractDraft` and the LLM's empty `title` tripped
// the validator, producing 100% `llm_failed`. The fix pre-filters
// `_|_` clusters in `runL3` so no LLM call happens.
seedPolicy(handle, {
id: "po_u1" as PolicyId,
title: "abstract planning heuristic",
trigger: "when tasks become complex",
procedure: "decompose into subgoals then evaluate",
verification: "outcome satisfies goal",
boundary: "general reasoning tasks",
sourceEpisodeIds: ["ep_u1" as EpisodeId],
vec: vec([1, 0, 0]),
});
seedPolicy(handle, {
id: "po_u2" as PolicyId,
title: "reflect on past decisions",
trigger: "at episode boundary",
procedure: "summarise and store lessons",
verification: "reflection recorded",
boundary: "reflection stage",
sourceEpisodeIds: ["ep_u2" as EpisodeId],
vec: vec([0.95, 0.05, 0]),
});
seedPolicy(handle, {
id: "po_u3" as PolicyId,
title: "prioritise information intake",
trigger: "before starting a session",
procedure: "review recent context and open questions",
verification: "context reviewed",
boundary: "session prelude",
sourceEpisodeIds: ["ep_u3" as EpisodeId],
vec: vec([0.9, 0.1, 0]),
});

const bus = createL3EventBus();
const events: L3Event[] = [];
bus.onAny((e) => events.push(e));

let llmCalls = 0;
const llm = fakeLlm({
completeJson: {
[OP]: () => {
llmCalls += 1;
// If this ever fires, `runL3` failed to pre-filter the bucket.
return {
title: "should not be called",
domain_tags: [],
environment: [],
inference: [],
constraints: [],
body: "",
confidence: 0.5,
supersedes_world_ids: [],
};
},
},
});

const result = await runL3(
{ trigger: "l2.policy.induced" },
{
repos: {
policies: handle.repos.policies,
traces: handle.repos.traces,
worldModel: handle.repos.worldModel,
kv: handle.repos.kv,
},
llm,
log,
bus,
config: cfg(),
},
);

expect(result.abstractions.length).toBe(1);
expect(result.abstractions[0]!.clusterKey).toBe("_|_");
expect(result.abstractions[0]!.skippedReason).toBe("untagged_cluster");
expect(result.abstractions[0]!.worldModelId).toBeNull();
expect(result.abstractions[0]!.policyCount).toBe(3);

// No LLM call — the pre-filter runs before `abstractDraft`.
expect(llmCalls).toBe(0);

// No WM row was created.
expect(handle.repos.worldModel.list().length).toBe(0);

// No `l3.failed` event — the skip is not a failure.
expect(events.some((e) => e.kind === "l3.failed")).toBe(false);
});

it("still processes tagged clusters when mixed with an untagged bucket (issue #2374)", async () => {
// Tagged docker+alpine+pip triplet — should still produce a WM.
seedTriplet();

// Additional three untagged policies that would otherwise form a
// second (broken) cluster. Only the tagged one should survive.
seedPolicy(handle, {
id: "po_u1" as PolicyId,
title: "abstract planning heuristic",
trigger: "when tasks become complex",
procedure: "decompose into subgoals then evaluate",
verification: "outcome satisfies goal",
boundary: "general reasoning tasks",
sourceEpisodeIds: ["ep_u1" as EpisodeId],
vec: vec([1, 0, 0]),
});
seedPolicy(handle, {
id: "po_u2" as PolicyId,
title: "reflect on past decisions",
trigger: "at episode boundary",
procedure: "summarise and store lessons",
verification: "reflection recorded",
boundary: "reflection stage",
sourceEpisodeIds: ["ep_u2" as EpisodeId],
vec: vec([0.95, 0.05, 0]),
});
seedPolicy(handle, {
id: "po_u3" as PolicyId,
title: "prioritise information intake",
trigger: "before starting a session",
procedure: "review recent context and open questions",
verification: "context reviewed",
boundary: "session prelude",
sourceEpisodeIds: ["ep_u3" as EpisodeId],
vec: vec([0.9, 0.1, 0]),
});

const llm = fakeLlm({
completeJson: {
[OP]: {
title: "Alpine python dependency model",
domain_tags: ["docker", "alpine", "pip"],
environment: [{ label: "musl", description: "no glibc" }],
inference: [{ label: "wheels fail", description: "must build from source" }],
constraints: [{ label: "no prebuilt", description: "avoid binary" }],
body: "# summary",
confidence: 0.75,
supersedes_world_ids: [],
},
},
});

const result = await runL3(
{ trigger: "l2.policy.induced" },
{
repos: {
policies: handle.repos.policies,
traces: handle.repos.traces,
worldModel: handle.repos.worldModel,
kv: handle.repos.kv,
},
llm,
log,
config: cfg(),
},
);

// Two clusters seen: one untagged (skipped), one tagged (created).
const byKey = new Map(result.abstractions.map((a) => [a.clusterKey, a]));
expect(byKey.get("_|_")?.skippedReason).toBe("untagged_cluster");
const taggedEntry = Array.from(byKey.values()).find(
(a) => a.clusterKey !== "_|_",
);
expect(taggedEntry?.skippedReason).toBeNull();
expect(taggedEntry?.createdNew).toBe(true);

// Only the tagged WM was inserted.
const rows = handle.repos.worldModel.list();
expect(rows.length).toBe(1);
expect(rows[0]!.title).toBe("Alpine python dependency model");
});

it("adjustConfidence clamps in [0,1] and emits an event", async () => {
const wm = seedWorldModel(handle, { id: "wm_adj" as WorldModelId, confidence: 0.9 });
const bus = createL3EventBus();
Expand Down
Loading