Skip to content

Commit eda0316

Browse files
steipeteAndy Ye
andcommitted
fix: classify active memory no-relevant status (#80015)
Recreated locally from PR #80015 because the contributor branch could not be updated by maintainers (maintainerCanModify=false). Fixes #79812. Co-authored-by: Andy Ye <[email protected]>
1 parent 64b5b97 commit eda0316

4 files changed

Lines changed: 70 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
5757
- CLI: make parser, startup, config, guardrail, channel, agent, task, session, and MCP failures explain what happened and point to the next recovery command.
5858
- GitHub Copilot: refresh the model catalog from `${baseUrl}/models` so per-account entitlement and accurate context windows surface at runtime; static manifest catalog (now including `gpt-5.5`) remains the fallback when discovery is disabled or the API is unreachable.
5959
- Active Memory: support concrete `plugins.entries.active-memory.config.toolsAllow` recall tool names for custom memory plugins while keeping the built-in memory-core default on `memory_search`/`memory_get` and preserving `memory_recall` automatically for `plugins.slots.memory: "memory-lancedb"`.
60+
- Active Memory: report normal `NONE` recall decisions as `status=no_relevant_memory`, keep unavailable and failed recall paths distinct, and avoid caching no-summary recall results so ordinary no-context turns no longer look like broken `status=empty` memory. Fixes #79812. (#80015) Thanks @TurboTheTurtle.
6061
- Telegram: share the grammY API throttler across polling and ad hoc send clients for the same bot token, so visible draft previews and CLI sends use one quota gate. Thanks @anagnorisis2peripeteia.
6162
- Feishu: resolve group policy/tool context from the trusted chat target for group turns while keeping the speaker in `From`, so @mention replies do not drop the configured group id. Fixes #79457. Thanks @greyxiong.
6263
- Telegram/Feishu: honor configured per-agent and global `reasoningDefault` values when deciding whether channel reasoning previews should stream or stay hidden, addressing the preview-default part of #73182. Thanks @anagnorisis2peripeteia.

docs/concepts/active-memory.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ The runtime shape is:
327327
flowchart LR
328328
U["User Message"] --> Q["Build Memory Query"]
329329
Q --> R["Active Memory Blocking Memory Sub-Agent"]
330-
R -->|NONE or empty| M["Main Reply"]
330+
R -->|NONE / no relevant memory| M["Main Reply"]
331331
R -->|relevant summary| I["Append Hidden active_memory_plugin System Context"]
332332
I --> M["Main Reply"]
333333
```

extensions/active-memory/index.test.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,7 +2031,7 @@ describe("active-memory plugin", () => {
20312031
{ pluginId: "other-plugin", lines: ["Other Plugin: keep me"] },
20322032
{
20332033
pluginId: "active-memory",
2034-
lines: [expect.stringContaining("🧩 Active Memory: status=empty")],
2034+
lines: [expect.stringContaining("🧩 Active Memory: status=no_relevant_memory")],
20352035
},
20362036
]);
20372037
});
@@ -2073,8 +2073,7 @@ describe("active-memory plugin", () => {
20732073
expect(hasDebugLine("no configured memory tools available")).toBe(true);
20742074
expect(hasWarnLine("No callable tools remain")).toBe(false);
20752075
const lines = getActiveMemoryLines(sessionKey);
2076-
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
2077-
expect(lines.join("\n")).not.toContain("status=unavailable");
2076+
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=unavailable")]);
20782077
});
20792078

20802079
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
@@ -2099,7 +2098,7 @@ describe("active-memory plugin", () => {
20992098
expect(hasDebugLine("no configured memory tools available")).toBe(true);
21002099
expect(hasWarnLine("No callable tools remain")).toBe(false);
21012100
expect(getActiveMemoryLines(sessionKey)).toEqual([
2102-
expect.stringContaining("🧩 Active Memory: status=empty"),
2101+
expect.stringContaining("🧩 Active Memory: status=unavailable"),
21032102
]);
21042103
});
21052104

@@ -2131,7 +2130,7 @@ describe("active-memory plugin", () => {
21312130
expect(result).toBeUndefined();
21322131
expect(hasDebugLine("no configured memory tools available")).toBe(true);
21332132
expect(getActiveMemoryLines(sessionKey)).toEqual([
2134-
expect.stringContaining("🧩 Active Memory: status=empty"),
2133+
expect.stringContaining("🧩 Active Memory: status=unavailable"),
21352134
]);
21362135
});
21372136

@@ -2157,7 +2156,7 @@ describe("active-memory plugin", () => {
21572156
expect(hasDebugLine("no configured memory tools available")).toBe(true);
21582157
expect(hasWarnLine("No callable tools remain")).toBe(false);
21592158
expect(getActiveMemoryLines(sessionKey)).toEqual([
2160-
expect.stringContaining("🧩 Active Memory: status=empty"),
2159+
expect.stringContaining("🧩 Active Memory: status=unavailable"),
21612160
]);
21622161
});
21632162

@@ -2185,7 +2184,7 @@ describe("active-memory plugin", () => {
21852184
expect(hasDebugLine("no configured memory tools available")).toBe(false);
21862185
expect(hasWarnLine(reason)).toBe(true);
21872186
expect(getActiveMemoryLines(sessionKey)).toEqual([
2188-
expect.stringContaining("🧩 Active Memory: status=empty"),
2187+
expect.stringContaining("🧩 Active Memory: status=failed"),
21892188
]);
21902189
},
21912190
);
@@ -2521,7 +2520,7 @@ describe("active-memory plugin", () => {
25212520

25222521
expect(result).toBeUndefined();
25232522
expect(getActiveMemoryLines(sessionKey)).toEqual([
2524-
expect.stringContaining("🧩 Active Memory: status=empty"),
2523+
expect.stringContaining("🧩 Active Memory: status=failed"),
25252524
]);
25262525
expect(getActiveMemoryLines(sessionKey).join("\n")).not.toContain(
25272526
"must not be surfaced from generic errors",
@@ -2625,7 +2624,7 @@ describe("active-memory plugin", () => {
26252624
).resolves.toMatchObject({ backend: "qmd", hits: 1 });
26262625
});
26272626

2628-
it("caches ok and empty results but not timeout_partial results", () => {
2627+
it("caches ok summaries but not empty, no-relevant, or timeout_partial results", () => {
26292628
expect(
26302629
__testing.shouldCacheResult({
26312630
status: "timeout_partial",
@@ -2647,10 +2646,17 @@ describe("active-memory plugin", () => {
26472646
elapsedMs: 1,
26482647
summary: null,
26492648
}),
2650-
).toBe(true);
2649+
).toBe(false);
2650+
expect(
2651+
__testing.shouldCacheResult({
2652+
status: "no_relevant_memory",
2653+
elapsedMs: 1,
2654+
summary: null,
2655+
}),
2656+
).toBe(false);
26512657
});
26522658

2653-
it("caches empty recall results", async () => {
2659+
it("does not cache no-relevant-memory recall results", async () => {
26542660
api.pluginConfig = {
26552661
agents: ["main"],
26562662
logging: true,
@@ -2679,16 +2685,11 @@ describe("active-memory plugin", () => {
26792685
},
26802686
);
26812687

2682-
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
2688+
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2);
26832689
const infoLines = vi
26842690
.mocked(api.logger.info)
26852691
.mock.calls.map((call: unknown[]) => String(call[0]));
2686-
expect(
2687-
infoLines.some(
2688-
(line: string) =>
2689-
line.includes(" cached status=empty ") || line.includes(" cached status=empty"),
2690-
),
2691-
).toBe(true);
2692+
expect(infoLines.some((line: string) => line.includes("cached status="))).toBe(false);
26922693
});
26932694

26942695
it("surfaces timeout_partial summaries in status lines, metadata, and prompt prefixes", () => {
@@ -2911,7 +2912,7 @@ describe("active-memory plugin", () => {
29112912
expect(wallClockMs).toBeLessThan(CONFIGURED_TIMEOUT_MS + HARD_DEADLINE_MARGIN_MS);
29122913
});
29132914

2914-
it("fast-fails terminal zero-hit memory_search results without waiting for recall timeout", async () => {
2915+
it("does not fast-fail terminal zero-hit memory_search results as empty", async () => {
29152916
const CONFIGURED_TIMEOUT_MS = 1_000;
29162917
__testing.setMinimumTimeoutMsForTests(1);
29172918
__testing.setSetupGraceTimeoutMsForTests(0);
@@ -2947,10 +2948,10 @@ describe("active-memory plugin", () => {
29472948
const infoLines = vi
29482949
.mocked(api.logger.info)
29492950
.mock.calls.map((call: unknown[]) => String(call[0]));
2950-
expectLinesToContain(infoLines, "done status=empty");
2951-
expectLinesNotToContain(infoLines, "done status=timeout");
2951+
expectLinesToContain(infoLines, "done status=timeout");
2952+
expectLinesNotToContain(infoLines, "done status=empty");
29522953
expect(getActiveMemoryLines(sessionKey)).toEqual([
2953-
expect.stringContaining("🧩 Active Memory: status=empty"),
2954+
expect.stringContaining("🧩 Active Memory: status=timeout"),
29542955
expect.stringContaining("🔎 Active Memory Debug: backend=qmd searchMs=8 hits=0"),
29552956
]);
29562957
});
@@ -3039,10 +3040,10 @@ describe("active-memory plugin", () => {
30393040
const infoLines = vi
30403041
.mocked(api.logger.info)
30413042
.mock.calls.map((call: unknown[]) => String(call[0]));
3042-
expectLinesToContain(infoLines, "done status=empty");
3043+
expectLinesToContain(infoLines, "done status=unavailable");
30433044
expectLinesNotToContain(infoLines, "done status=timeout");
30443045
expect(getActiveMemoryLines(sessionKey)).toEqual([
3045-
expect.stringContaining("🧩 Active Memory: status=empty"),
3046+
expect.stringContaining("🧩 Active Memory: status=unavailable"),
30463047
expect.stringContaining(
30473048
"🔎 Active Memory Debug: Memory search is unavailable due to an embedding/provider error. Check the embedding provider configuration, then retry memory_search.",
30483049
),
@@ -3301,7 +3302,7 @@ describe("active-memory plugin", () => {
33013302
{
33023303
pluginId: "active-memory",
33033304
lines: [
3304-
expect.stringContaining("🧩 Active Memory: status=empty"),
3305+
expect.stringContaining("🧩 Active Memory: status=unavailable"),
33053306
expect.stringContaining(
33063307
"🔎 Active Memory Debug: Memory search is unavailable because the embedding provider quota is exhausted. Top up or switch embedding provider, then retry memory_search.",
33073308
),

extensions/active-memory/index.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ type ActiveMemorySearchDebug = {
224224

225225
type ActiveRecallResult =
226226
| {
227-
status: "empty" | "timeout" | "unavailable";
227+
status: "empty" | "failed" | "no_relevant_memory" | "timeout" | "unavailable";
228228
elapsedMs: number;
229229
summary: string | null;
230230
searchDebug?: ActiveMemorySearchDebug;
@@ -256,12 +256,13 @@ type TranscriptReadLimits = {
256256

257257
type RecallSubagentResult = {
258258
rawReply: string;
259+
resultStatus?: "failed" | "unavailable";
259260
transcriptPath?: string;
260261
searchDebug?: ActiveMemorySearchDebug;
261262
};
262263

263264
type TerminalMemorySearchResult = {
264-
status: "empty";
265+
status: "unavailable";
265266
searchDebug?: ActiveMemorySearchDebug;
266267
};
267268

@@ -1400,7 +1401,11 @@ function toSingleLineLogValue(value: unknown): string {
14001401
}
14011402

14021403
function shouldCacheResult(result: ActiveRecallResult): boolean {
1403-
return result.status === "ok" || result.status === "empty";
1404+
return result.status === "ok" && result.summary.length > 0;
1405+
}
1406+
1407+
function isUnavailableMemorySearchDebug(debug?: ActiveMemorySearchDebug): boolean {
1408+
return Boolean(debug?.error);
14041409
}
14051410

14061411
function resolveStatusUpdateAgentId(ctx: { agentId?: string; sessionKey?: string }): string {
@@ -1741,15 +1746,10 @@ function extractTerminalMemorySearchResultFromSessionRecord(
17411746
}
17421747
const details = asRecord(message.details);
17431748
const debug = extractActiveMemorySearchDebugFromSessionRecord(value);
1744-
const results = Array.isArray(details?.results) ? details.results : undefined;
17451749
const disabled = details?.disabled === true;
1746-
const unavailable =
1747-
disabled || Boolean(debug?.warning) || Boolean(debug?.error) || Boolean(details?.error);
1748-
const debugHits =
1749-
typeof debug?.hits === "number" && Number.isFinite(debug.hits) ? debug.hits : undefined;
1750-
const zeroHitSearch = results !== undefined ? results.length === 0 : debugHits === 0;
1751-
if (unavailable || zeroHitSearch) {
1752-
return { status: "empty", searchDebug: debug };
1750+
const unavailable = disabled || Boolean(debug?.error) || Boolean(details?.error);
1751+
if (unavailable) {
1752+
return { status: "unavailable", searchDebug: debug };
17531753
}
17541754
return undefined;
17551755
}
@@ -2038,21 +2038,23 @@ async function buildTimeoutRecallResult(params: {
20382038
normalizeActiveSummary(rawReply ?? "") ?? "",
20392039
params.maxSummaryChars,
20402040
);
2041+
const searchDebug =
2042+
params.searchDebug ??
2043+
subagentPartialData.searchDebug ??
2044+
(params.sessionFile ? await readActiveMemorySearchDebug(params.sessionFile) : undefined);
20412045
if (summary.length === 0) {
20422046
return {
20432047
status: "timeout",
20442048
elapsedMs: params.elapsedMs,
20452049
summary: null,
2050+
searchDebug,
20462051
};
20472052
}
20482053
return {
20492054
status: "timeout_partial",
20502055
elapsedMs: params.elapsedMs,
20512056
summary,
2052-
searchDebug:
2053-
params.searchDebug ??
2054-
subagentPartialData.searchDebug ??
2055-
(params.sessionFile ? await readActiveMemorySearchDebug(params.sessionFile) : undefined),
2057+
searchDebug,
20562058
};
20572059
}
20582060

@@ -2564,7 +2566,7 @@ async function runRecallSubagent(params: {
25642566
} catch (error) {
25652567
if (params.abortSignal?.aborted) {
25662568
const partialReply = await readPartialAssistantText(sessionFile);
2567-
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
2569+
const searchDebug = await readActiveMemorySearchDebug(sessionFile);
25682570
attachPartialTimeoutData(error, partialReply, searchDebug);
25692571
}
25702572
if (
@@ -2574,14 +2576,14 @@ async function runRecallSubagent(params: {
25742576
params.api.logger.debug?.(
25752577
`active-memory: no configured memory tools available; skipping sub-agent`,
25762578
);
2577-
return { rawReply: "NONE" };
2579+
return { rawReply: "NONE", resultStatus: "unavailable" };
25782580
}
25792581
if (!params.abortSignal?.aborted) {
25802582
const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error));
25812583
params.api.logger.warn?.(
25822584
`active-memory: memory sub-agent failed, skipping recall: ${message}`,
25832585
);
2584-
return { rawReply: "NONE" };
2586+
return { rawReply: "NONE", resultStatus: "failed" };
25852587
}
25862588
throw error;
25872589
} finally {
@@ -2777,7 +2779,7 @@ async function maybeResolveActiveRecall(params: {
27772779
return result;
27782780
}
27792781

2780-
const { rawReply, transcriptPath, searchDebug } = raceResult;
2782+
const { rawReply, resultStatus, transcriptPath, searchDebug } = raceResult;
27812783
const summary = truncateSummary(
27822784
normalizeActiveSummary(rawReply) ?? "",
27832785
params.config.maxSummaryChars,
@@ -2794,12 +2796,26 @@ async function maybeResolveActiveRecall(params: {
27942796
summary,
27952797
searchDebug,
27962798
}
2797-
: {
2798-
status: "empty",
2799-
elapsedMs: Date.now() - startedAt,
2800-
summary: null,
2801-
searchDebug,
2802-
};
2799+
: resultStatus === "failed"
2800+
? {
2801+
status: "failed",
2802+
elapsedMs: Date.now() - startedAt,
2803+
summary: null,
2804+
searchDebug,
2805+
}
2806+
: resultStatus === "unavailable" || isUnavailableMemorySearchDebug(searchDebug)
2807+
? {
2808+
status: "unavailable",
2809+
elapsedMs: Date.now() - startedAt,
2810+
summary: null,
2811+
searchDebug,
2812+
}
2813+
: {
2814+
status: "no_relevant_memory",
2815+
elapsedMs: Date.now() - startedAt,
2816+
summary: null,
2817+
searchDebug,
2818+
};
28032819
if (params.config.logging) {
28042820
params.api.logger.info?.(
28052821
`${logPrefix} done status=${result.status} elapsedMs=${String(result.elapsedMs)} summaryChars=${String(result.summary?.length ?? 0)}`,
@@ -2849,7 +2865,7 @@ async function maybeResolveActiveRecall(params: {
28492865
params.api.logger.warn?.(`${logPrefix} failed error=${message}; skipping recall`);
28502866
}
28512867
const result: ActiveRecallResult = {
2852-
status: "empty",
2868+
status: "failed",
28532869
elapsedMs: Date.now() - startedAt,
28542870
summary: null,
28552871
};

0 commit comments

Comments
 (0)