



























@@ -3,6 +3,7 @@ import os from "node:os";
33import path from "node:path";
44import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55import type { SessionEntry } from "../../config/sessions.js";
6+import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js";
67import type { OpenClawConfig } from "../../config/types.openclaw.js";
78import { FailoverError } from "../failover-error.js";
89import { runEmbeddedPiAgent, type EmbeddedPiRunResult } from "../pi-embedded.js";
@@ -418,6 +419,130 @@ describe("CLI attempt execution", () => {
418419});
419420});
420421422+it("embedded assistant gap-fill skips user mirror and dedupes identical assistant tails", async () => {
423+const sessionKey = "agent:main:subagent:embedded-gap-fill";
424+const sessionEntry: SessionEntry = {
425+sessionId: "session-embedded-gap-fill",
426+updatedAt: Date.now(),
427+};
428+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
429+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
430+431+const result = makeCliResult("already mirrored");
432+result.meta.executionTrace = {
433+winnerProvider: "anthropic",
434+winnerModel: "claude-opus-4-6",
435+fallbackUsed: false,
436+runner: "embedded",
437+};
438+439+const updatedFirst = await persistCliTurnTranscript({
440+body: "ignored for gap fill",
441+transcriptBody: "also ignored",
442+ result,
443+sessionId: sessionEntry.sessionId,
444+ sessionKey,
445+ sessionEntry,
446+ sessionStore,
447+ storePath,
448+sessionAgentId: "main",
449+sessionCwd: tmpDir,
450+config: {},
451+embeddedAssistantGapFill: true,
452+});
453+454+let messages = await readSessionMessages(updatedFirst?.sessionFile ?? "");
455+expect(messages).toHaveLength(1);
456+expect(messages[0]).toMatchObject({
457+role: "assistant",
458+content: [{ type: "text", text: "already mirrored" }],
459+});
460+461+await persistCliTurnTranscript({
462+body: "still ignored",
463+ result,
464+sessionId: sessionEntry.sessionId,
465+ sessionKey,
466+sessionEntry: updatedFirst,
467+ sessionStore,
468+ storePath,
469+sessionAgentId: "main",
470+sessionCwd: tmpDir,
471+config: {},
472+embeddedAssistantGapFill: true,
473+});
474+475+messages = await readSessionMessages(updatedFirst?.sessionFile ?? "");
476+expect(messages).toHaveLength(1);
477+});
478+479+it("embedded assistant gap-fill appends repeated replies after a user tail", async () => {
480+const sessionKey = "agent:main:subagent:embedded-repeated-reply";
481+const sessionEntry: SessionEntry = {
482+sessionId: "session-embedded-repeated-reply",
483+updatedAt: Date.now(),
484+};
485+const sessionStore: Record<string, SessionEntry> = { [sessionKey]: sessionEntry };
486+await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8");
487+488+const result = makeCliResult("same answer");
489+result.meta.executionTrace = {
490+winnerProvider: "anthropic",
491+winnerModel: "claude-opus-4-6",
492+fallbackUsed: false,
493+runner: "embedded",
494+};
495+496+const updatedFirst = await persistCliTurnTranscript({
497+body: "ignored for gap fill",
498+ result,
499+sessionId: sessionEntry.sessionId,
500+ sessionKey,
501+ sessionEntry,
502+ sessionStore,
503+ storePath,
504+sessionAgentId: "main",
505+sessionCwd: tmpDir,
506+config: {},
507+embeddedAssistantGapFill: true,
508+});
509+const sessionFile = updatedFirst?.sessionFile;
510+expect(sessionFile).toBeTruthy();
511+512+await appendSessionTranscriptMessage({
513+transcriptPath: sessionFile!,
514+sessionId: sessionEntry.sessionId,
515+cwd: tmpDir,
516+config: {},
517+message: {
518+role: "user",
519+content: "next prompt",
520+timestamp: Date.now(),
521+},
522+});
523+524+await persistCliTurnTranscript({
525+body: "still ignored",
526+ result,
527+sessionId: sessionEntry.sessionId,
528+ sessionKey,
529+sessionEntry: updatedFirst,
530+ sessionStore,
531+ storePath,
532+sessionAgentId: "main",
533+sessionCwd: tmpDir,
534+config: {},
535+embeddedAssistantGapFill: true,
536+});
537+538+const messages = await readSessionMessages(sessionFile!);
539+expect(messages).toHaveLength(3);
540+expect(messages.map((message) => message.role)).toEqual(["assistant", "user", "assistant"]);
541+expect(messages[2]).toMatchObject({
542+content: [{ type: "text", text: "same answer" }],
543+});
544+});
545+421546it("persists the transcript body instead of runtime-only CLI prompt context", async () => {
422547const sessionKey = "agent:main:subagent:cli-transcript-clean";
423548const sessionEntry: SessionEntry = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。