

























@@ -1,6 +1,9 @@
11import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
22import type { OpenClawConfig } from "../../config/config.js";
3-import { hasCommittedMessagingToolDeliveryEvidence } from "./delivery-evidence.js";
3+import {
4+hasCommittedMessagingToolDeliveryEvidence,
5+hasOutboundDeliveryEvidence,
6+} from "./delivery-evidence.js";
47import { makeAttemptResult } from "./run.overflow-compaction.fixture.js";
58import {
69loadRunOverflowCompactionHarness,
@@ -1484,6 +1487,34 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
14841487expect(retryInstruction).toBe(EMPTY_RESPONSE_RETRY_INSTRUCTION);
14851488});
148614891490+it("does not retry empty turns after an accepted sessions_spawn delivery", () => {
1491+const retryInstruction = resolveEmptyResponseRetryInstruction({
1492+provider: "ollama",
1493+modelId: "gemma4:31b",
1494+payloadCount: 0,
1495+aborted: false,
1496+timedOut: false,
1497+attempt: makeAttemptResult({
1498+assistantTexts: [],
1499+acceptedSessionSpawns: [
1500+{
1501+runId: "run-child",
1502+childSessionKey: "agent:claude:subagent:child",
1503+},
1504+],
1505+lastAssistant: {
1506+role: "assistant",
1507+stopReason: "end_turn",
1508+provider: "ollama",
1509+model: "gemma4:31b",
1510+content: [{ type: "text", text: "" }],
1511+} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
1512+}),
1513+});
1514+1515+expect(retryInstruction).toBeNull();
1516+});
1517+14871518it("retries generic empty OpenAI-compatible turns from custom endpoints", () => {
14881519const retryInstruction = resolveEmptyResponseRetryInstruction({
14891520provider: "llama-cpp-local",
@@ -1654,6 +1685,100 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
16541685expect(incompleteTurnText).toBeNull();
16551686});
165616871688+it("suppresses the incomplete-turn warning after an accepted sessions_spawn terminal success", () => {
1689+const attemptWithAcceptedSpawn: Partial<EmbeddedRunAttemptResult> & {
1690+acceptedSessionSpawns: Array<{ runId: string; childSessionKey: string }>;
1691+} = {
1692+assistantTexts: [],
1693+acceptedSessionSpawns: [
1694+{
1695+runId: "run-child",
1696+childSessionKey: "agent:claude:subagent:child",
1697+},
1698+],
1699+lastAssistant: {
1700+role: "assistant",
1701+stopReason: "stop",
1702+provider: "anthropic",
1703+model: "sonnet-4.6",
1704+content: [],
1705+} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
1706+};
1707+1708+const incompleteTurnText = resolveIncompleteTurnPayloadText({
1709+payloadCount: 0,
1710+aborted: false,
1711+timedOut: false,
1712+attempt: makeAttemptResult(attemptWithAcceptedSpawn),
1713+});
1714+1715+expect(incompleteTurnText).toBeNull();
1716+});
1717+1718+it("still returns a timeout payload when the parent prompt times out after an accepted sessions_spawn", async () => {
1719+const acceptedSessionSpawns = [
1720+{
1721+runId: "run-child",
1722+childSessionKey: "agent:claude:subagent:child",
1723+},
1724+];
1725+mockedClassifyFailoverReason.mockReturnValue(null);
1726+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
1727+makeAttemptResult({
1728+assistantTexts: [],
1729+ acceptedSessionSpawns,
1730+timedOut: true,
1731+lastAssistant: {
1732+role: "assistant",
1733+stopReason: "toolUse",
1734+provider: "openai",
1735+model: "gpt-5.4",
1736+content: [],
1737+} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
1738+}),
1739+);
1740+1741+const result = await runEmbeddedPiAgent({
1742+ ...overflowBaseRunParams,
1743+provider: "openai",
1744+model: "gpt-5.4",
1745+runId: "run-timeout-after-accepted-spawn",
1746+});
1747+1748+expect(result.payloads).toEqual([
1749+{
1750+text: "Request timed out before a response was generated. Please try again, or increase `agents.defaults.timeoutSeconds` in your config.",
1751+isError: true,
1752+},
1753+]);
1754+expect(result.acceptedSessionSpawns).toEqual(acceptedSessionSpawns);
1755+});
1756+1757+it("still surfaces the incomplete-turn warning without an accepted sessions_spawn success", () => {
1758+const attemptWithMalformedSpawn: Partial<EmbeddedRunAttemptResult> & {
1759+acceptedSessionSpawns: Array<{ runId: string; childSessionKey: string }>;
1760+} = {
1761+assistantTexts: [],
1762+acceptedSessionSpawns: [],
1763+lastAssistant: {
1764+role: "assistant",
1765+stopReason: "stop",
1766+provider: "anthropic",
1767+model: "sonnet-4.6",
1768+content: [],
1769+} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
1770+};
1771+1772+const incompleteTurnText = resolveIncompleteTurnPayloadText({
1773+payloadCount: 0,
1774+aborted: false,
1775+timedOut: false,
1776+attempt: makeAttemptResult(attemptWithMalformedSpawn),
1777+});
1778+1779+expect(incompleteTurnText).toContain("couldn't generate a response");
1780+});
1781+16571782it("still surfaces the incomplete-turn warning when no messaging delivery was committed", () => {
16581783const incompleteTurnText = resolveIncompleteTurnPayloadText({
16591784payloadCount: 0,
@@ -1738,6 +1863,40 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
17381863).toEqual({ hadPotentialSideEffects: true, replaySafe: false });
17391864});
174018651866+it("treats accepted sessions_spawn as replay-invalid outbound delivery", () => {
1867+const acceptedSessionSpawns = [
1868+{
1869+runId: "run-child",
1870+childSessionKey: "agent:claude:subagent:child",
1871+},
1872+];
1873+1874+expect(
1875+buildAttemptReplayMetadata({
1876+toolMetas: [],
1877+didSendViaMessagingTool: false,
1878+messagingToolSentTexts: [],
1879+messagingToolSentMediaUrls: [],
1880+ acceptedSessionSpawns,
1881+}),
1882+).toEqual({ hadPotentialSideEffects: true, replaySafe: false });
1883+expect(hasOutboundDeliveryEvidence({ acceptedSessionSpawns })).toBe(true);
1884+});
1885+1886+it("ignores malformed accepted sessions_spawn delivery evidence", () => {
1887+expect(
1888+hasOutboundDeliveryEvidence({
1889+acceptedSessionSpawns: [
1890+null,
1891+{
1892+runId: "run-child",
1893+childSessionKey: " ",
1894+},
1895+],
1896+}),
1897+).toBe(false);
1898+});
1899+17411900it("leaves committed delivery plus tool errors to the tool-error payload path", () => {
17421901const incompleteTurnText = resolveIncompleteTurnPayloadText({
17431902payloadCount: 0,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。