


























@@ -3,6 +3,7 @@ import os from "node:os";
33import path from "node:path";
44import type { AgentMessage } from "@earendil-works/pi-agent-core";
55import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6+import { HEARTBEAT_TRANSCRIPT_PROMPT } from "../../../auto-reply/heartbeat.js";
67import type { OpenClawConfig } from "../../../config/types.js";
78import { buildMemorySystemPromptAddition } from "../../../context-engine/delegate.js";
89import {
@@ -392,6 +393,294 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
392393}
393394});
394395396+it("filters heartbeat response-tool transcript artifacts before normal prompt snapshots", async () => {
397+const contextEngine = createContextEngineBootstrapAndAssemble();
398+const sessionMessages = [
399+{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT, timestamp: 1 },
400+{
401+role: "assistant",
402+content: [
403+{
404+type: "toolCall",
405+id: "call_bash",
406+name: "bash",
407+arguments: { command: "cat HEARTBEAT.md" },
408+},
409+],
410+timestamp: 2,
411+},
412+{
413+role: "toolResult",
414+toolCallId: "call_bash",
415+content: [{ type: "text", text: "HEARTBEAT.md says stay quiet" }],
416+timestamp: 3,
417+},
418+{
419+role: "assistant",
420+content: [
421+{
422+type: "toolCall",
423+id: "call_heartbeat",
424+name: "heartbeat_respond",
425+arguments: {
426+outcome: "no_change",
427+notify: false,
428+summary: "No visible update.",
429+},
430+},
431+],
432+timestamp: 4,
433+},
434+{
435+role: "toolResult",
436+toolCallId: "call_heartbeat",
437+content: [{ type: "text", text: '{"notify":false}' }],
438+timestamp: 5,
439+},
440+{ role: "assistant", content: "No visible update. notify=false", timestamp: 6 },
441+] as AgentMessage[];
442+443+const result = await createContextEngineAttemptRunner({
444+ contextEngine,
445+ sessionKey,
446+ tempPaths,
447+ sessionMessages,
448+attemptOverrides: {
449+prompt: "what model are you",
450+transcriptPrompt: "what model are you",
451+},
452+sessionPrompt: async (session) => {
453+session.messages = [
454+ ...session.messages,
455+{ role: "assistant", content: "gpt-test", timestamp: 7 },
456+];
457+},
458+});
459+460+const assembleInput = contextEngine.assemble.mock.calls.at(0)?.[0];
461+const assembledMessagesJson = JSON.stringify(assembleInput?.messages ?? []);
462+const snapshotJson = JSON.stringify(result.messagesSnapshot);
463+for (const artifact of [
464+"HEARTBEAT.md",
465+"heartbeat_respond",
466+"notify=false",
467+'"notify":false',
468+HEARTBEAT_TRANSCRIPT_PROMPT,
469+]) {
470+expect(assembledMessagesJson).not.toContain(artifact);
471+expect(snapshotJson).not.toContain(artifact);
472+}
473+expect(result.finalPromptText).toBe("what model are you");
474+});
475+476+it("filters interrupted prompt-only heartbeat artifacts before normal prompt snapshots", async () => {
477+const contextEngine = createContextEngineBootstrapAndAssemble();
478+const sessionMessages = [
479+{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT, timestamp: 1 },
480+] as AgentMessage[];
481+482+const result = await createContextEngineAttemptRunner({
483+ contextEngine,
484+ sessionKey,
485+ tempPaths,
486+ sessionMessages,
487+attemptOverrides: {
488+prompt: "what model are you",
489+transcriptPrompt: "what model are you",
490+},
491+sessionPrompt: async (session) => {
492+session.messages = [
493+ ...session.messages,
494+{ role: "assistant", content: "gpt-test", timestamp: 2 },
495+];
496+},
497+});
498+499+const assembleInput = contextEngine.assemble.mock.calls.at(0)?.[0];
500+const assembledMessagesJson = JSON.stringify(assembleInput?.messages ?? []);
501+const snapshotJson = JSON.stringify(result.messagesSnapshot);
502+expect(assembledMessagesJson).not.toContain(HEARTBEAT_TRANSCRIPT_PROMPT);
503+expect(snapshotJson).not.toContain(HEARTBEAT_TRANSCRIPT_PROMPT);
504+expect(result.finalPromptText).toBe("what model are you");
505+});
506+507+it("filters pending notify=true heartbeat response-tool calls before normal prompt snapshots", async () => {
508+const contextEngine = createContextEngineBootstrapAndAssemble();
509+const sessionMessages = [
510+{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT, timestamp: 1 },
511+{
512+role: "assistant",
513+content: [
514+{
515+type: "toolCall",
516+id: "call_heartbeat",
517+name: "heartbeat_respond",
518+arguments: {
519+outcome: "needs_attention",
520+notify: true,
521+summary: "Build is blocked.",
522+notificationText: "Build is blocked on missing credentials.",
523+},
524+},
525+],
526+timestamp: 2,
527+},
528+] as AgentMessage[];
529+530+const result = await createContextEngineAttemptRunner({
531+ contextEngine,
532+ sessionKey,
533+ tempPaths,
534+ sessionMessages,
535+attemptOverrides: {
536+prompt: "what model are you",
537+transcriptPrompt: "what model are you",
538+},
539+sessionPrompt: async (session) => {
540+session.messages = [
541+ ...session.messages,
542+{ role: "assistant", content: "gpt-test", timestamp: 3 },
543+];
544+},
545+});
546+547+const assembleInput = contextEngine.assemble.mock.calls.at(0)?.[0];
548+const assembledMessagesJson = JSON.stringify(assembleInput?.messages ?? []);
549+const snapshotJson = JSON.stringify(result.messagesSnapshot);
550+for (const artifact of [
551+HEARTBEAT_TRANSCRIPT_PROMPT,
552+"heartbeat_respond",
553+'"notify":true',
554+"Build is blocked on missing credentials.",
555+]) {
556+expect(assembledMessagesJson).not.toContain(artifact);
557+expect(snapshotJson).not.toContain(artifact);
558+}
559+expect(result.finalPromptText).toBe("what model are you");
560+});
561+562+it("preserves visible heartbeat alerts in normal prompt snapshots", async () => {
563+const contextEngine = createContextEngineBootstrapAndAssemble();
564+const sessionMessages = [
565+{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT, timestamp: 1 },
566+{
567+role: "assistant",
568+content: [
569+{
570+type: "toolCall",
571+id: "call_bash",
572+name: "bash",
573+arguments: { command: "cat HEARTBEAT.md" },
574+},
575+],
576+timestamp: 2,
577+},
578+{
579+role: "toolResult",
580+toolCallId: "call_bash",
581+content: [{ type: "text", text: "HEARTBEAT.md says check deployment" }],
582+timestamp: 3,
583+},
584+{
585+role: "assistant",
586+content: "Build is blocked on a failing release check.",
587+timestamp: 4,
588+},
589+] as AgentMessage[];
590+591+const result = await createContextEngineAttemptRunner({
592+ contextEngine,
593+ sessionKey,
594+ tempPaths,
595+ sessionMessages,
596+attemptOverrides: {
597+prompt: "what changed while I was away?",
598+transcriptPrompt: "what changed while I was away?",
599+},
600+sessionPrompt: async (session) => {
601+session.messages = [
602+ ...session.messages,
603+{ role: "assistant", content: "gpt-test", timestamp: 5 },
604+];
605+},
606+});
607+608+const assembleInput = contextEngine.assemble.mock.calls.at(0)?.[0];
609+const assembledMessagesJson = JSON.stringify(assembleInput?.messages ?? []);
610+const snapshotJson = JSON.stringify(result.messagesSnapshot);
611+for (const visibleContext of [
612+HEARTBEAT_TRANSCRIPT_PROMPT,
613+"HEARTBEAT.md says check deployment",
614+"Build is blocked on a failing release check.",
615+]) {
616+expect(assembledMessagesJson).toContain(visibleContext);
617+expect(snapshotJson).toContain(visibleContext);
618+}
619+expect(result.finalPromptText).toBe("what changed while I was away?");
620+});
621+622+it("preserves visible heartbeat response-tool notifications in normal prompt snapshots", async () => {
623+const contextEngine = createContextEngineBootstrapAndAssemble();
624+const sessionMessages = [
625+{ role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT, timestamp: 1 },
626+{
627+role: "assistant",
628+content: [
629+{
630+type: "toolCall",
631+id: "call_heartbeat",
632+name: "heartbeat_respond",
633+arguments: {
634+outcome: "needs_attention",
635+notify: true,
636+summary: "Build is blocked.",
637+notificationText: "Build is blocked on missing credentials.",
638+},
639+},
640+],
641+timestamp: 2,
642+},
643+{
644+role: "toolResult",
645+toolCallId: "call_heartbeat",
646+content: [{ type: "text", text: '{"notify":true}' }],
647+timestamp: 3,
648+},
649+{ role: "assistant", content: "HEARTBEAT_OK", timestamp: 4 },
650+] as AgentMessage[];
651+652+const result = await createContextEngineAttemptRunner({
653+ contextEngine,
654+ sessionKey,
655+ tempPaths,
656+ sessionMessages,
657+attemptOverrides: {
658+prompt: "what changed while I was away?",
659+transcriptPrompt: "what changed while I was away?",
660+},
661+sessionPrompt: async (session) => {
662+session.messages = [
663+ ...session.messages,
664+{ role: "assistant", content: "gpt-test", timestamp: 5 },
665+];
666+},
667+});
668+669+const assembleInput = contextEngine.assemble.mock.calls.at(0)?.[0];
670+const assembledMessagesJson = JSON.stringify(assembleInput?.messages ?? []);
671+const snapshotJson = JSON.stringify(result.messagesSnapshot);
672+for (const visibleContext of [
673+"heartbeat_respond",
674+'"notify":true',
675+"Build is blocked on missing credentials.",
676+"HEARTBEAT_OK",
677+]) {
678+expect(assembledMessagesJson).toContain(visibleContext);
679+expect(snapshotJson).toContain(visibleContext);
680+}
681+expect(result.finalPromptText).toBe("what changed while I was away?");
682+});
683+395684it("rebuilds skill prompt inputs from the sandbox workspace for non-rw sandbox runs", async () => {
396685const sandboxWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sandbox-skills-"));
397686tempPaths.push(sandboxWorkspace);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。