mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix: force preflight compaction before oversized agent turns
Force required preflight context compaction before oversized turns can enter the agent runtime. Treat required preflight compaction as a hard gate: compact, skip only explicit harmless no-op reasons, or surface a visible recovery message when compaction cannot recover. Fixes #87234. Co-authored-by: ArthurNie <264332276+ArthurNie@users.noreply.github.com>
This commit is contained in:
@@ -26,7 +26,7 @@ export function classifyCompactionReason(reason?: string): string {
|
||||
if (!text) {
|
||||
return "unknown";
|
||||
}
|
||||
if (text.includes("nothing to compact")) {
|
||||
if (text.includes("nothing to compact") || text.includes("no real conversation messages")) {
|
||||
return "no_compactable_entries";
|
||||
}
|
||||
// Backends use both phrases for the same harmless state: the transcript is
|
||||
|
||||
@@ -2164,6 +2164,46 @@ describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => {
|
||||
expect(hookRunner.runAfterCompaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forces engine-owned compaction for preflight-required budget compaction", async () => {
|
||||
const result = await compactEmbeddedAgentSession(
|
||||
wrappedCompactionArgs({
|
||||
trigger: "budget",
|
||||
forcePreflight: true,
|
||||
preflightRequired: true,
|
||||
preflightCompactionTrigger: "transcript_bytes",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const compactArg = mockCallArg(contextEngineCompactMock) as {
|
||||
runtimeContext?: Record<string, unknown>;
|
||||
};
|
||||
expectRecordFields(compactArg, {
|
||||
compactionTarget: "budget",
|
||||
force: true,
|
||||
});
|
||||
expectRecordFields(compactArg.runtimeContext, {
|
||||
forceReason: "preflight_required",
|
||||
preflightCompactionTrigger: "transcript_bytes",
|
||||
});
|
||||
});
|
||||
|
||||
it("continues forcing engine-owned manual compaction with manual force reason", async () => {
|
||||
const result = await compactEmbeddedAgentSession(wrappedCompactionArgs({ trigger: "manual" }));
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const compactArg = mockCallArg(contextEngineCompactMock) as {
|
||||
runtimeContext?: Record<string, unknown>;
|
||||
};
|
||||
expectRecordFields(compactArg, {
|
||||
compactionTarget: "threshold",
|
||||
force: true,
|
||||
});
|
||||
expectRecordFields(compactArg.runtimeContext, {
|
||||
forceReason: "manual",
|
||||
});
|
||||
});
|
||||
|
||||
it("threads the caller abort signal into the engine compact() call", async () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
|
||||
@@ -363,8 +363,21 @@ export async function compactEmbeddedAgentSession(
|
||||
currentTokenCount: params.currentTokenCount,
|
||||
compactionTarget: params.trigger === "manual" ? "threshold" : "budget",
|
||||
customInstructions: params.customInstructions,
|
||||
force: params.trigger === "manual",
|
||||
runtimeContext,
|
||||
force:
|
||||
params.force === true ||
|
||||
params.forcePreflight === true ||
|
||||
params.preflightRequired === true ||
|
||||
params.trigger === "manual",
|
||||
runtimeContext: {
|
||||
...runtimeContext,
|
||||
forceReason:
|
||||
params.forcePreflight === true || params.preflightRequired === true
|
||||
? "preflight_required"
|
||||
: params.trigger === "manual"
|
||||
? "manual"
|
||||
: undefined,
|
||||
preflightCompactionTrigger: params.preflightCompactionTrigger,
|
||||
},
|
||||
},
|
||||
resolveCompactionTimeoutMs(params.config),
|
||||
params.abortSignal,
|
||||
|
||||
@@ -66,6 +66,12 @@ export type CompactEmbeddedAgentSessionParams = {
|
||||
customInstructions?: string;
|
||||
tokenBudget?: number;
|
||||
force?: boolean;
|
||||
/** Force compaction because the caller already determined this turn must compact before prompt submission. */
|
||||
forcePreflight?: boolean;
|
||||
/** Alias for forcePreflight used by preflight budget gates. */
|
||||
preflightRequired?: boolean;
|
||||
/** Diagnostic trigger that made preflight compaction mandatory. */
|
||||
preflightCompactionTrigger?: "tokens" | "transcript_bytes";
|
||||
trigger?: "budget" | "overflow" | "manual";
|
||||
/**
|
||||
* Preflight callers can allow native/current-session harness compaction but
|
||||
|
||||
@@ -306,6 +306,29 @@ describe("runReplyAgent runtime config", () => {
|
||||
expect(metadata?.deliverDespiteSourceReplySuppression).toBe(true);
|
||||
});
|
||||
|
||||
it("surfaces preflight compaction failures before the agent starts", async () => {
|
||||
const { replyParams } = createDirectRuntimeReplyParams({
|
||||
shouldFollowup: false,
|
||||
isActive: false,
|
||||
});
|
||||
runPreflightCompactionIfNeededMock.mockRejectedValue(
|
||||
new Error("Preflight compaction required but failed: auth profile mismatch"),
|
||||
);
|
||||
runMemoryFlushIfNeededMock.mockResolvedValue(undefined);
|
||||
|
||||
const result = await runReplyAgent(replyParams);
|
||||
|
||||
if (!result || Array.isArray(result)) {
|
||||
throw new Error("expected a single preflight compaction failure reply payload");
|
||||
}
|
||||
expect(result.text).toContain("Context is too large");
|
||||
expect(result.text).toContain("auto-compaction could not recover");
|
||||
expect(result.text).toContain("/compact");
|
||||
expect(result.text).toContain("/new");
|
||||
const metadata = getReplyPayloadMetadata(result);
|
||||
expect(metadata?.deliverDespiteSourceReplySuppression).toBe(true);
|
||||
});
|
||||
|
||||
it("does not resolve secrets before the enqueue-followup queue path", async () => {
|
||||
const { followupRun, resolvedQueue, replyParams } = createDirectRuntimeReplyParams({
|
||||
shouldFollowup: true,
|
||||
|
||||
@@ -625,6 +625,7 @@ function collapseRepeatedFailureDetail(message: string): string {
|
||||
const SAFE_MISSING_API_KEY_PROVIDERS = new Set(["anthropic", "google", "openai"]);
|
||||
const EXTERNAL_RUN_FAILURE_DETAIL_MAX_CHARS = 900;
|
||||
const AGENT_FAILED_BEFORE_REPLY_TEXT = "Agent failed before reply:";
|
||||
const PREFLIGHT_COMPACTION_FAILURE_PREFIX = "Preflight compaction required but failed:";
|
||||
|
||||
type ExternalRunFailureReply = {
|
||||
text: string;
|
||||
@@ -692,6 +693,27 @@ function buildCodexAppServerFailureText(message: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildPreflightCompactionFailureText(
|
||||
message: string,
|
||||
options?: { includeDetails?: boolean },
|
||||
): string | null {
|
||||
const normalizedMessage = collapseRepeatedFailureDetail(message);
|
||||
if (!normalizedMessage.startsWith(PREFLIGHT_COMPACTION_FAILURE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
const reason = sanitizeUserFacingText(
|
||||
normalizedMessage.slice(PREFLIGHT_COMPACTION_FAILURE_PREFIX.length),
|
||||
{ errorContext: true },
|
||||
)
|
||||
.trim()
|
||||
.replace(/\s+/gu, " ");
|
||||
const reasonSuffix = options?.includeDetails && reason ? ` Reason: ${reason}.` : "";
|
||||
return (
|
||||
"⚠️ Context is too large and auto-compaction could not recover this turn." +
|
||||
`${reasonSuffix} Try again, use /compact, or use /new to start a fresh session.`
|
||||
);
|
||||
}
|
||||
|
||||
function buildCliBackendTimeoutFailureText(message: string): string | null {
|
||||
const normalizedMessage = collapseRepeatedFailureDetail(message);
|
||||
const stall = normalizedMessage.match(CLI_BACKEND_NO_OUTPUT_STALL_RE);
|
||||
@@ -820,6 +842,20 @@ export function buildKnownAgentRunFailureReplyPayload(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const preflightCompactionFailureText = buildPreflightCompactionFailureText(message, {
|
||||
includeDetails: isVerboseFailureDetailEnabled(params.resolvedVerboseLevel),
|
||||
});
|
||||
if (preflightCompactionFailureText) {
|
||||
return markAgentRunFailureReplyPayload({
|
||||
text: resolveExternalRunFailureTextForConversation({
|
||||
text: preflightCompactionFailureText,
|
||||
sessionCtx: params.sessionCtx,
|
||||
isGenericRunnerFailure: false,
|
||||
cfg: params.cfg,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const isPureTransientSummary = isFallbackSummary
|
||||
? isPureTransientRateLimitSummary(params.err)
|
||||
: false;
|
||||
|
||||
@@ -106,6 +106,10 @@ type CompactEmbeddedAgentSessionParams = {
|
||||
sandboxSessionKey?: string;
|
||||
currentTokenCount?: number;
|
||||
cwd?: string;
|
||||
force?: boolean;
|
||||
forcePreflight?: boolean;
|
||||
preflightRequired?: boolean;
|
||||
preflightCompactionTrigger?: string;
|
||||
sessionFile?: string;
|
||||
sessionId?: string;
|
||||
trigger?: string;
|
||||
@@ -999,6 +1003,10 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({
|
||||
trigger: "budget",
|
||||
force: true,
|
||||
forcePreflight: true,
|
||||
preflightRequired: true,
|
||||
preflightCompactionTrigger: "tokens",
|
||||
deferOwningContextEngineCompaction: false,
|
||||
contextTokenBudget: 100,
|
||||
});
|
||||
@@ -1112,7 +1120,7 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
["stale_thread_binding", "thread not found: <codex-thread-id>"],
|
||||
["missing_thread_binding", "no thread binding for session"],
|
||||
])(
|
||||
"continues after recoverable native harness %s failure during preflight compaction",
|
||||
"fails required preflight compaction after native harness %s failure",
|
||||
async (failureReason, reason) => {
|
||||
const sessionFile = path.join(rootDir, "session.jsonl");
|
||||
await fs.writeFile(
|
||||
@@ -1143,30 +1151,31 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
};
|
||||
const sessionStore = { "agent:main:telegram:group:redacted": sessionEntry };
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
await expect(
|
||||
runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
).rejects.toThrow(`Preflight compaction required but failed: ${reason}`);
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("continues after an unstructured thread-not-found preflight compaction failure", async () => {
|
||||
it("fails required preflight compaction after an unstructured thread-not-found failure", async () => {
|
||||
const sessionFile = path.join(rootDir, "session.jsonl");
|
||||
await fs.writeFile(
|
||||
sessionFile,
|
||||
@@ -1195,24 +1204,27 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
};
|
||||
const sessionStore = { "agent:main:telegram:group:redacted": sessionEntry };
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
await expect(
|
||||
runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:telegram:group:redacted",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
).rejects.toThrow(
|
||||
"Preflight compaction required but failed: thread not found: <codex-thread-id>",
|
||||
);
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(incrementCompactionCountMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1469,7 +1481,7 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("continues when preflight compaction returns a successful no-op", async () => {
|
||||
it("fails when required preflight compaction returns an unknown successful no-op", async () => {
|
||||
compactEmbeddedAgentSessionMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
compacted: false,
|
||||
@@ -1485,23 +1497,24 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
const sessionStore = { main: sessionEntry };
|
||||
const replyOperation = createReplyOperation();
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
await expect(
|
||||
runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 200_000,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "main",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation,
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 200_000,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "main",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation,
|
||||
});
|
||||
).rejects.toThrow("Preflight compaction required but failed: plugin already stored this turn");
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||
const compactCall = requireCompactEmbeddedAgentSessionCall();
|
||||
expect(compactCall.contextTokenBudget).toBe(200_000);
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||
import { estimateMessagesTokens } from "../../agents/compaction.js";
|
||||
import {
|
||||
classifyCompactionReason,
|
||||
DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON,
|
||||
} from "../../agents/embedded-agent-runner/compact-reasons.js";
|
||||
import { isRecoverableNativeHarnessBindingFailure } from "../../agents/harness/compaction-recovery.js";
|
||||
import { classifyCompactionReason } from "../../agents/embedded-agent-runner/compact-reasons.js";
|
||||
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
|
||||
import { ensureSelectedAgentHarnessPlugin } from "../../agents/harness/runtime-plugin.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
@@ -189,10 +185,6 @@ function isPreflightCompactionSkipReason(reason?: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isDeferredPreflightCompactionReason(reason?: string): boolean {
|
||||
return normalizeOptionalString(reason) === DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON;
|
||||
}
|
||||
|
||||
function resolveMemoryFlushModelFallbackOptions(
|
||||
run: FollowupRun["run"],
|
||||
model?: string,
|
||||
@@ -893,6 +885,10 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
thinkLevel: params.followupRun.run.thinkLevel,
|
||||
bashElevated: params.followupRun.run.bashElevated,
|
||||
trigger: "budget",
|
||||
force: true,
|
||||
forcePreflight: true,
|
||||
preflightRequired: true,
|
||||
preflightCompactionTrigger: compactionTrigger,
|
||||
deferOwningContextEngineCompaction: false,
|
||||
contextTokenBudget: contextWindowTokens,
|
||||
currentTokenCount: tokenCountForCompaction ?? freshPersistedTokens,
|
||||
@@ -907,25 +903,17 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
logVerbose(`preflightCompaction failed: sessionKey=${params.sessionKey} reason=${reason}`);
|
||||
if (isRecoverableNativeHarnessBindingFailure(result)) {
|
||||
logVerbose(
|
||||
`preflightCompaction continuing after recoverable native harness binding failure: sessionKey=${params.sessionKey} reason=${reason}`,
|
||||
);
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
throw new Error(`Preflight compaction required but failed: ${reason}`);
|
||||
}
|
||||
|
||||
if (!result.compacted) {
|
||||
const reason = normalizeOptionalString(result.reason);
|
||||
if (isDeferredPreflightCompactionReason(reason)) {
|
||||
logVerbose(`preflightCompaction failed: sessionKey=${params.sessionKey} reason=${reason}`);
|
||||
throw new Error(`Preflight compaction required but failed: ${reason}`);
|
||||
const reason = normalizeOptionalString(result.reason) ?? "not_compacted";
|
||||
if (isPreflightCompactionSkipReason(reason)) {
|
||||
logVerbose(`preflightCompaction skipped: sessionKey=${params.sessionKey} reason=${reason}`);
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
logVerbose(
|
||||
`preflightCompaction skipped: sessionKey=${params.sessionKey} reason=${reason ?? "not_compacted"}`,
|
||||
);
|
||||
return entry ?? params.sessionEntry;
|
||||
logVerbose(`preflightCompaction failed: sessionKey=${params.sessionKey} reason=${reason}`);
|
||||
throw new Error(`Preflight compaction required but failed: ${reason}`);
|
||||
}
|
||||
|
||||
await deps.incrementCompactionCount({
|
||||
|
||||
@@ -744,6 +744,77 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
);
|
||||
realAgentEvents.resetAgentRunContextForTest();
|
||||
});
|
||||
|
||||
it("routes preflight compaction failures before starting queued followup runs", async () => {
|
||||
runPreflightCompactionIfNeededMock.mockRejectedValueOnce(
|
||||
new Error("Preflight compaction required but failed: auth profile mismatch"),
|
||||
);
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
sessionKey: "main",
|
||||
defaultModel: "anthropic/claude",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
originatingAccountId: "acct-1",
|
||||
originatingThreadId: "thread-1",
|
||||
originatingChatType: "group",
|
||||
run: {
|
||||
messageProvider: "discord",
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
verboseLevel: "off",
|
||||
sessionKey: "main",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
||||
expect(routeReplyMock).toHaveBeenCalledOnce();
|
||||
expect(routeReplyMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "discord",
|
||||
to: "channel:C1",
|
||||
accountId: "acct-1",
|
||||
threadId: "thread-1",
|
||||
payload: expect.objectContaining({
|
||||
text: expect.stringContaining("auto-compaction could not recover"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves non-compaction preflight failures for queued followup runs", async () => {
|
||||
runPreflightCompactionIfNeededMock.mockRejectedValueOnce(new Error("session load failed"));
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
sessionKey: "main",
|
||||
defaultModel: "anthropic/claude",
|
||||
});
|
||||
|
||||
await expect(
|
||||
runner(
|
||||
createQueuedRun({
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
run: {
|
||||
messageProvider: "discord",
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
sessionKey: "main",
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("session load failed");
|
||||
|
||||
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
||||
expect(routeReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
async function normalizeComparablePath(filePath: string): Promise<string> {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { shouldPreserveUserFacingSessionStateForInputProvenance } from "../../sessions/input-provenance.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import { markReplyPayloadForSourceSuppressionDelivery } from "../reply-payload.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
clearDroppedCliSessionBinding,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
runCliAgentWithLifecycle,
|
||||
} from "./agent-runner-cli-dispatch.js";
|
||||
import {
|
||||
buildPreflightCompactionFailureText,
|
||||
resolveRunAfterAutoFallbackPrimaryProbeRecheck,
|
||||
resolveSessionRuntimeOverrideForProvider,
|
||||
} from "./agent-runner-execution.js";
|
||||
@@ -546,19 +548,40 @@ export function createFollowupRunner(params: {
|
||||
let runResult: Awaited<ReturnType<typeof runEmbeddedAgent>>;
|
||||
let fallbackProvider = run.provider;
|
||||
let fallbackModel = run.model;
|
||||
activeSessionEntry = await runPreflightCompactionIfNeeded({
|
||||
cfg: runtimeConfig,
|
||||
followupRun: effectiveQueued,
|
||||
promptForEstimate: queued.prompt,
|
||||
defaultModel,
|
||||
agentCfgContextTokens,
|
||||
sessionEntry: activeSessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: replySessionKey,
|
||||
storePath,
|
||||
isHeartbeat: opts?.isHeartbeat === true,
|
||||
replyOperation,
|
||||
});
|
||||
try {
|
||||
activeSessionEntry = await runPreflightCompactionIfNeeded({
|
||||
cfg: runtimeConfig,
|
||||
followupRun: effectiveQueued,
|
||||
promptForEstimate: queued.prompt,
|
||||
defaultModel,
|
||||
agentCfgContextTokens,
|
||||
sessionEntry: activeSessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: replySessionKey,
|
||||
storePath,
|
||||
isHeartbeat: opts?.isHeartbeat === true,
|
||||
replyOperation,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = formatErrorMessage(err);
|
||||
replyOperation.fail("run_failed", err);
|
||||
const preflightCompactionFailureText = buildPreflightCompactionFailureText(message, {
|
||||
includeDetails: run.verboseLevel === "on" || run.verboseLevel === "full",
|
||||
});
|
||||
if (preflightCompactionFailureText) {
|
||||
await sendFollowupPayloads(
|
||||
[
|
||||
markReplyPayloadForSourceSuppressionDelivery({
|
||||
text: preflightCompactionFailureText,
|
||||
}),
|
||||
],
|
||||
effectiveQueued,
|
||||
{ provider: fallbackProvider, modelId: fallbackModel },
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (run.sessionKey) {
|
||||
const owningSessionId =
|
||||
activeSessionEntry?.sessionId === run.sessionId
|
||||
|
||||
Reference in New Issue
Block a user