mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
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 <andy@Andys-MacBook-Pro-2.local>
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI: make parser, startup, config, guardrail, channel, agent, task, session, and MCP failures explain what happened and point to the next recovery command.
|
||||
- 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.
|
||||
- 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"`.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -327,7 +327,7 @@ The runtime shape is:
|
||||
flowchart LR
|
||||
U["User Message"] --> Q["Build Memory Query"]
|
||||
Q --> R["Active Memory Blocking Memory Sub-Agent"]
|
||||
R -->|NONE or empty| M["Main Reply"]
|
||||
R -->|NONE / no relevant memory| M["Main Reply"]
|
||||
R -->|relevant summary| I["Append Hidden active_memory_plugin System Context"]
|
||||
I --> M["Main Reply"]
|
||||
```
|
||||
|
||||
@@ -2031,7 +2031,7 @@ describe("active-memory plugin", () => {
|
||||
{ pluginId: "other-plugin", lines: ["Other Plugin: keep me"] },
|
||||
{
|
||||
pluginId: "active-memory",
|
||||
lines: [expect.stringContaining("🧩 Active Memory: status=empty")],
|
||||
lines: [expect.stringContaining("🧩 Active Memory: status=no_relevant_memory")],
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -2073,8 +2073,7 @@ describe("active-memory plugin", () => {
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
const lines = getActiveMemoryLines(sessionKey);
|
||||
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
|
||||
expect(lines.join("\n")).not.toContain("status=unavailable");
|
||||
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=unavailable")]);
|
||||
});
|
||||
|
||||
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
|
||||
@@ -2099,7 +2098,7 @@ describe("active-memory plugin", () => {
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2131,7 +2130,7 @@ describe("active-memory plugin", () => {
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2157,7 +2156,7 @@ describe("active-memory plugin", () => {
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2185,7 +2184,7 @@ describe("active-memory plugin", () => {
|
||||
expect(hasDebugLine("no configured memory tools available")).toBe(false);
|
||||
expect(hasWarnLine(reason)).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=failed"),
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -2521,7 +2520,7 @@ describe("active-memory plugin", () => {
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=failed"),
|
||||
]);
|
||||
expect(getActiveMemoryLines(sessionKey).join("\n")).not.toContain(
|
||||
"must not be surfaced from generic errors",
|
||||
@@ -2625,7 +2624,7 @@ describe("active-memory plugin", () => {
|
||||
).resolves.toMatchObject({ backend: "qmd", hits: 1 });
|
||||
});
|
||||
|
||||
it("caches ok and empty results but not timeout_partial results", () => {
|
||||
it("caches ok summaries but not empty, no-relevant, or timeout_partial results", () => {
|
||||
expect(
|
||||
__testing.shouldCacheResult({
|
||||
status: "timeout_partial",
|
||||
@@ -2647,10 +2646,17 @@ describe("active-memory plugin", () => {
|
||||
elapsedMs: 1,
|
||||
summary: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(
|
||||
__testing.shouldCacheResult({
|
||||
status: "no_relevant_memory",
|
||||
elapsedMs: 1,
|
||||
summary: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("caches empty recall results", async () => {
|
||||
it("does not cache no-relevant-memory recall results", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
logging: true,
|
||||
@@ -2679,16 +2685,11 @@ describe("active-memory plugin", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1);
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2);
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expect(
|
||||
infoLines.some(
|
||||
(line: string) =>
|
||||
line.includes(" cached status=empty ") || line.includes(" cached status=empty"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(infoLines.some((line: string) => line.includes("cached status="))).toBe(false);
|
||||
});
|
||||
|
||||
it("surfaces timeout_partial summaries in status lines, metadata, and prompt prefixes", () => {
|
||||
@@ -2911,7 +2912,7 @@ describe("active-memory plugin", () => {
|
||||
expect(wallClockMs).toBeLessThan(CONFIGURED_TIMEOUT_MS + HARD_DEADLINE_MARGIN_MS);
|
||||
});
|
||||
|
||||
it("fast-fails terminal zero-hit memory_search results without waiting for recall timeout", async () => {
|
||||
it("does not fast-fail terminal zero-hit memory_search results as empty", async () => {
|
||||
const CONFIGURED_TIMEOUT_MS = 1_000;
|
||||
__testing.setMinimumTimeoutMsForTests(1);
|
||||
__testing.setSetupGraceTimeoutMsForTests(0);
|
||||
@@ -2947,10 +2948,10 @@ describe("active-memory plugin", () => {
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expectLinesToContain(infoLines, "done status=empty");
|
||||
expectLinesNotToContain(infoLines, "done status=timeout");
|
||||
expectLinesToContain(infoLines, "done status=timeout");
|
||||
expectLinesNotToContain(infoLines, "done status=empty");
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=timeout"),
|
||||
expect.stringContaining("🔎 Active Memory Debug: backend=qmd searchMs=8 hits=0"),
|
||||
]);
|
||||
});
|
||||
@@ -3039,10 +3040,10 @@ describe("active-memory plugin", () => {
|
||||
const infoLines = vi
|
||||
.mocked(api.logger.info)
|
||||
.mock.calls.map((call: unknown[]) => String(call[0]));
|
||||
expectLinesToContain(infoLines, "done status=empty");
|
||||
expectLinesToContain(infoLines, "done status=unavailable");
|
||||
expectLinesNotToContain(infoLines, "done status=timeout");
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
expect.stringContaining(
|
||||
"🔎 Active Memory Debug: Memory search is unavailable due to an embedding/provider error. Check the embedding provider configuration, then retry memory_search.",
|
||||
),
|
||||
@@ -3301,7 +3302,7 @@ describe("active-memory plugin", () => {
|
||||
{
|
||||
pluginId: "active-memory",
|
||||
lines: [
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
expect.stringContaining(
|
||||
"🔎 Active Memory Debug: Memory search is unavailable because the embedding provider quota is exhausted. Top up or switch embedding provider, then retry memory_search.",
|
||||
),
|
||||
|
||||
@@ -224,7 +224,7 @@ type ActiveMemorySearchDebug = {
|
||||
|
||||
type ActiveRecallResult =
|
||||
| {
|
||||
status: "empty" | "timeout" | "unavailable";
|
||||
status: "empty" | "failed" | "no_relevant_memory" | "timeout" | "unavailable";
|
||||
elapsedMs: number;
|
||||
summary: string | null;
|
||||
searchDebug?: ActiveMemorySearchDebug;
|
||||
@@ -256,12 +256,13 @@ type TranscriptReadLimits = {
|
||||
|
||||
type RecallSubagentResult = {
|
||||
rawReply: string;
|
||||
resultStatus?: "failed" | "unavailable";
|
||||
transcriptPath?: string;
|
||||
searchDebug?: ActiveMemorySearchDebug;
|
||||
};
|
||||
|
||||
type TerminalMemorySearchResult = {
|
||||
status: "empty";
|
||||
status: "unavailable";
|
||||
searchDebug?: ActiveMemorySearchDebug;
|
||||
};
|
||||
|
||||
@@ -1400,7 +1401,11 @@ function toSingleLineLogValue(value: unknown): string {
|
||||
}
|
||||
|
||||
function shouldCacheResult(result: ActiveRecallResult): boolean {
|
||||
return result.status === "ok" || result.status === "empty";
|
||||
return result.status === "ok" && result.summary.length > 0;
|
||||
}
|
||||
|
||||
function isUnavailableMemorySearchDebug(debug?: ActiveMemorySearchDebug): boolean {
|
||||
return Boolean(debug?.error);
|
||||
}
|
||||
|
||||
function resolveStatusUpdateAgentId(ctx: { agentId?: string; sessionKey?: string }): string {
|
||||
@@ -1741,15 +1746,10 @@ function extractTerminalMemorySearchResultFromSessionRecord(
|
||||
}
|
||||
const details = asRecord(message.details);
|
||||
const debug = extractActiveMemorySearchDebugFromSessionRecord(value);
|
||||
const results = Array.isArray(details?.results) ? details.results : undefined;
|
||||
const disabled = details?.disabled === true;
|
||||
const unavailable =
|
||||
disabled || Boolean(debug?.warning) || Boolean(debug?.error) || Boolean(details?.error);
|
||||
const debugHits =
|
||||
typeof debug?.hits === "number" && Number.isFinite(debug.hits) ? debug.hits : undefined;
|
||||
const zeroHitSearch = results !== undefined ? results.length === 0 : debugHits === 0;
|
||||
if (unavailable || zeroHitSearch) {
|
||||
return { status: "empty", searchDebug: debug };
|
||||
const unavailable = disabled || Boolean(debug?.error) || Boolean(details?.error);
|
||||
if (unavailable) {
|
||||
return { status: "unavailable", searchDebug: debug };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -2038,21 +2038,23 @@ async function buildTimeoutRecallResult(params: {
|
||||
normalizeActiveSummary(rawReply ?? "") ?? "",
|
||||
params.maxSummaryChars,
|
||||
);
|
||||
const searchDebug =
|
||||
params.searchDebug ??
|
||||
subagentPartialData.searchDebug ??
|
||||
(params.sessionFile ? await readActiveMemorySearchDebug(params.sessionFile) : undefined);
|
||||
if (summary.length === 0) {
|
||||
return {
|
||||
status: "timeout",
|
||||
elapsedMs: params.elapsedMs,
|
||||
summary: null,
|
||||
searchDebug,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "timeout_partial",
|
||||
elapsedMs: params.elapsedMs,
|
||||
summary,
|
||||
searchDebug:
|
||||
params.searchDebug ??
|
||||
subagentPartialData.searchDebug ??
|
||||
(params.sessionFile ? await readActiveMemorySearchDebug(params.sessionFile) : undefined),
|
||||
searchDebug,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2564,7 +2566,7 @@ async function runRecallSubagent(params: {
|
||||
} catch (error) {
|
||||
if (params.abortSignal?.aborted) {
|
||||
const partialReply = await readPartialAssistantText(sessionFile);
|
||||
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
|
||||
const searchDebug = await readActiveMemorySearchDebug(sessionFile);
|
||||
attachPartialTimeoutData(error, partialReply, searchDebug);
|
||||
}
|
||||
if (
|
||||
@@ -2574,14 +2576,14 @@ async function runRecallSubagent(params: {
|
||||
params.api.logger.debug?.(
|
||||
`active-memory: no configured memory tools available; skipping sub-agent`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
return { rawReply: "NONE", resultStatus: "unavailable" };
|
||||
}
|
||||
if (!params.abortSignal?.aborted) {
|
||||
const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error));
|
||||
params.api.logger.warn?.(
|
||||
`active-memory: memory sub-agent failed, skipping recall: ${message}`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
return { rawReply: "NONE", resultStatus: "failed" };
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -2777,7 +2779,7 @@ async function maybeResolveActiveRecall(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { rawReply, transcriptPath, searchDebug } = raceResult;
|
||||
const { rawReply, resultStatus, transcriptPath, searchDebug } = raceResult;
|
||||
const summary = truncateSummary(
|
||||
normalizeActiveSummary(rawReply) ?? "",
|
||||
params.config.maxSummaryChars,
|
||||
@@ -2794,12 +2796,26 @@ async function maybeResolveActiveRecall(params: {
|
||||
summary,
|
||||
searchDebug,
|
||||
}
|
||||
: {
|
||||
status: "empty",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
searchDebug,
|
||||
};
|
||||
: resultStatus === "failed"
|
||||
? {
|
||||
status: "failed",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
searchDebug,
|
||||
}
|
||||
: resultStatus === "unavailable" || isUnavailableMemorySearchDebug(searchDebug)
|
||||
? {
|
||||
status: "unavailable",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
searchDebug,
|
||||
}
|
||||
: {
|
||||
status: "no_relevant_memory",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
searchDebug,
|
||||
};
|
||||
if (params.config.logging) {
|
||||
params.api.logger.info?.(
|
||||
`${logPrefix} done status=${result.status} elapsedMs=${String(result.elapsedMs)} summaryChars=${String(result.summary?.length ?? 0)}`,
|
||||
@@ -2849,7 +2865,7 @@ async function maybeResolveActiveRecall(params: {
|
||||
params.api.logger.warn?.(`${logPrefix} failed error=${message}; skipping recall`);
|
||||
}
|
||||
const result: ActiveRecallResult = {
|
||||
status: "empty",
|
||||
status: "failed",
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
summary: null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user