mirror of https://github.com/openclaw/openclaw.git
fix(guardian): detect system triggers from historyMessages, not just currentPrompt
Heartbeat prompts may arrive via historyMessages (as the last user message) rather than via currentPrompt, depending on the agent loop stage. Check both sources for system trigger detection so heartbeat tool calls are consistently skipped regardless of how the prompt is delivered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13b4a0bbeb
commit
8a2c15f9bc
|
|
@ -556,12 +556,12 @@ describe("message-cache", () => {
|
|||
expect(isSystemTrigger("nonexistent")).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves isSystemTrigger when subsequent llm_input has no prompt", () => {
|
||||
it("stays true when heartbeat is in historyMessages on subsequent llm_input", () => {
|
||||
// Heartbeat fires with prompt → isSystemTrigger=true
|
||||
updateCache("s1", [], "heartbeat", 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
|
||||
// Agent loop continues without prompt (tool result processed) → should preserve true
|
||||
// Agent loop continues — heartbeat is now in historyMessages
|
||||
updateCache("s1", [{ role: "user", content: "heartbeat" }], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
});
|
||||
|
|
@ -570,11 +570,15 @@ describe("message-cache", () => {
|
|||
updateCache("s1", [], "heartbeat", 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
|
||||
// Real user message arrives → should reset to false
|
||||
// Real user message arrives — now the last user message in history is the real one
|
||||
updateCache(
|
||||
"s1",
|
||||
[{ role: "user", content: "heartbeat" }],
|
||||
"Deploy my project",
|
||||
[
|
||||
{ role: "user", content: "heartbeat" },
|
||||
{ role: "assistant", content: "HEARTBEAT_OK" },
|
||||
{ role: "user", content: "Deploy my project" },
|
||||
],
|
||||
undefined,
|
||||
3,
|
||||
NO_FILTER,
|
||||
);
|
||||
|
|
@ -586,6 +590,27 @@ describe("message-cache", () => {
|
|||
updateCache("s1", [], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(false);
|
||||
});
|
||||
|
||||
it("detects heartbeat from last user message in historyMessages when currentPrompt is undefined", () => {
|
||||
// Heartbeat prompt arrives via historyMessages, not currentPrompt
|
||||
const heartbeatPrompt =
|
||||
"Read HEARTBEAT.md if it exists (workspace context). If nothing needs attention, reply HEARTBEAT_OK.";
|
||||
updateCache("s1", [{ role: "user", content: heartbeatPrompt }], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects heartbeat from historyMessages even on first llm_input (no existing entry)", () => {
|
||||
updateCache("s1", [{ role: "user", content: "heartbeat" }], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
});
|
||||
|
||||
it("resets when historyMessages last user message is not a system trigger", () => {
|
||||
updateCache("s1", [{ role: "user", content: "heartbeat" }], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(true);
|
||||
|
||||
updateCache("s1", [{ role: "user", content: "Deploy my project" }], undefined, 3, NO_FILTER);
|
||||
expect(isSystemTrigger("s1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRecentTurns filters system turns", () => {
|
||||
|
|
|
|||
|
|
@ -49,14 +49,13 @@ export function updateCache(
|
|||
contextTools,
|
||||
totalTurnsProcessed: totalTurns,
|
||||
lastSummarizedTurnCount: existing?.lastSummarizedTurnCount ?? 0,
|
||||
// Preserve isSystemTrigger when currentPrompt is empty (agent loop continuation).
|
||||
// During a heartbeat cycle, llm_input fires multiple times: first with the
|
||||
// heartbeat prompt (isSystemTrigger=true), then without a prompt as the agent
|
||||
// loop continues after tool results. Without preservation, the flag resets to
|
||||
// false and heartbeat tool calls reach the guardian unnecessarily.
|
||||
isSystemTrigger: currentPrompt
|
||||
? isSystemTriggerPrompt(currentPrompt)
|
||||
: (existing?.isSystemTrigger ?? false),
|
||||
// Detect system triggers from both currentPrompt AND the last user message
|
||||
// in historyMessages. Heartbeats may arrive via either path depending on
|
||||
// the agent loop stage (currentPrompt on first llm_input, historyMessages
|
||||
// on subsequent continuations after tool results).
|
||||
isSystemTrigger:
|
||||
isSystemTriggerPrompt(currentPrompt) ||
|
||||
isSystemTriggerPrompt(getLastUserMessageText(historyMessages)),
|
||||
agentSystemPrompt: existing?.agentSystemPrompt,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
|
@ -279,6 +278,17 @@ function filterSystemTurns(turns: ConversationTurn[]): ConversationTurn[] {
|
|||
});
|
||||
}
|
||||
|
||||
/** Extract text from the last user message in the history array. */
|
||||
function getLastUserMessageText(historyMessages: unknown[]): string | undefined {
|
||||
for (let i = historyMessages.length - 1; i >= 0; i--) {
|
||||
const msg = historyMessages[i];
|
||||
if (isMessageLike(msg) && msg.role === "user") {
|
||||
return extractTextContent(msg.content) || undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Count user messages in the history array. */
|
||||
function countUserMessages(historyMessages: unknown[]): number {
|
||||
let count = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue