diff --git a/CHANGELOG.md b/CHANGELOG.md
index dde430663cf..069a532eec1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -170,6 +170,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Memory: close temp SQLite handles before failed atomic reindex cleanup and retry Windows EBUSY/EPERM/EACCES temp file removals, so `memory index --force` does not abort or leave temp sidecars on locked filesystems. Fixes #79708. Thanks @LobsterFarmerAmp and @hclsys.
+- Agents/CLI: add an explicit `reseedFromRawTranscriptWhenUncompacted` backend opt-in so safe invalidated CLI sessions can reseed from a bounded raw OpenClaw transcript tail before compaction while auth-boundary resets remain no-raw. Fixes #79713. (#79764) Thanks @hclsys.
- Agents/CLI: handle resumed CLI JSONL output and bound supervisor output buffering so resumed runs stay readable without letting noisy child output grow unbounded.
- Codex app-server: honor per-call `timeoutMs`, configured `image_generate` timeouts, and media image-understanding timeouts for dynamic tool calls, capped at 600000 ms, so slow image generation and image analysis no longer fail at the 30s bridge default. Fixes #79810. Thanks @omarshahine.
- Agents/sandbox: include the container workspace path hint in sandbox-root escape errors while preserving shortened host workspace roots. Fixes #79712. Thanks @haumanto and @hclsys.
diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256
index 973370aa321..ca2284baab5 100644
--- a/docs/.generated/config-baseline.sha256
+++ b/docs/.generated/config-baseline.sha256
@@ -1,4 +1,4 @@
-bb53a92a54a804d217baf466a4731924653d769db37122c38400cc3b97720c23 config-baseline.json
-3b632b0f038846722e2a5012a5eeec2a29048b6e385b591d7bd9122aa0981a20 config-baseline.core.json
+335083781741da50b280496b954794bdecba7c1150ce777d37534ccc1ec2c10a config-baseline.json
+b629f3b6ec6389eb0709e6f9149d7c3ab50431bb22124019541710873dc52cbb config-baseline.core.json
9edc62ae7dfedabc645470dd03102b813fc780b9108caf675fd661104714206f config-baseline.channel.json
1da42cb10427fb08510f29732493d24851ab915a424f91556569febdd450d9c3 config-baseline.plugin.json
diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md
index 9b7056a7afa..a927755f190 100644
--- a/docs/gateway/cli-backends.md
+++ b/docs/gateway/cli-backends.md
@@ -136,6 +136,9 @@ The provider id becomes the left side of your model ref:
systemPromptWhen: "first",
imageArg: "--image",
imageMode: "repeat",
+ // Opt in only if this backend may reseed safe invalidated sessions
+ // from bounded raw OpenClaw transcript history before compaction.
+ reseedFromRawTranscriptWhenUncompacted: true,
serialize: true,
},
},
@@ -231,6 +234,13 @@ binary is not already on `PATH`.
- Stored CLI sessions are provider-owned continuity. The implicit daily session
reset does not cut them; `/reset` and explicit `session.reset` policies still
do.
+- Fresh CLI sessions normally reseed only from OpenClaw's compaction summary
+ plus post-compaction tail. To recover short sessions that are invalidated
+ before compaction, a backend can opt in with
+ `reseedFromRawTranscriptWhenUncompacted: true`. OpenClaw still keeps raw
+ transcript reseed bounded and limits it to safe invalidations such as missing
+ CLI transcripts, system-prompt/MCP changes, or session-expired retry; auth
+ profile or credential-epoch changes never reseed raw transcript history.
Serialization notes:
diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md
index 6bc884f1382..5e31908319c 100644
--- a/docs/gateway/config-agents.md
+++ b/docs/gateway/config-agents.md
@@ -484,6 +484,10 @@ Optional CLI backends for text-only fallback runs (no tool calls). Useful as a b
- CLI backends are text-first; tools are always disabled.
- Sessions supported when `sessionArg` is set.
- Image pass-through supported when `imageArg` accepts file paths.
+- `reseedFromRawTranscriptWhenUncompacted: true` lets a backend recover safe
+ invalidated sessions from a bounded raw OpenClaw transcript tail before the
+ first compaction summary exists. Auth profile or credential-epoch changes
+ still never raw-reseed.
### `agents.defaults.systemPromptOverride`
diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts
index 5570819ef9e..58f5b2c2752 100644
--- a/src/agents/cli-runner.reliability.test.ts
+++ b/src/agents/cli-runner.reliability.test.ts
@@ -839,27 +839,32 @@ describe("runCliAgent reliability", () => {
);
try {
- await expect(
- runPreparedCliAgent({
+ const result = await runPreparedCliAgent({
+ ...buildPreparedContext({
+ sessionKey: "agent:main:main",
+ runId: "run-retry-success",
+ cliSessionId: "thread-123",
+ openClawHistoryPrompt:
+ "Continue this conversation using the OpenClaw transcript below.\n\nUser: recovered history\n\n\nhi\n",
+ }),
+ params: {
...buildPreparedContext({
sessionKey: "agent:main:main",
runId: "run-retry-success",
cliSessionId: "thread-123",
- }),
- params: {
- ...buildPreparedContext({
- sessionKey: "agent:main:main",
- runId: "run-retry-success",
- cliSessionId: "thread-123",
- }).params,
- agentId: "main",
- sessionFile,
- workspaceDir: dir,
- },
- }),
- ).resolves.toMatchObject({
+ openClawHistoryPrompt:
+ "Continue this conversation using the OpenClaw transcript below.\n\nUser: recovered history\n\n\nhi\n",
+ }).params,
+ agentId: "main",
+ sessionFile,
+ workspaceDir: dir,
+ },
+ });
+
+ expect(result).toMatchObject({
payloads: [{ text: "recovered output" }],
});
+ expect(result.meta.finalPromptText).toContain("User: recovered history");
await vi.waitFor(() => {
expect(hookRunner.runLlmInput).toHaveBeenCalledTimes(1);
diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts
index 19327663c6b..56d86447329 100644
--- a/src/agents/cli-runner/prepare.test.ts
+++ b/src/agents/cli-runner/prepare.test.ts
@@ -80,7 +80,11 @@ async function createTestMcpLoopbackServer(port = 0) {
}
function createCliBackendConfig(
- params: { systemPromptOverride?: string | null; bundleMcp?: boolean } = {},
+ params: {
+ systemPromptOverride?: string | null;
+ bundleMcp?: boolean;
+ reseedFromRawTranscriptWhenUncompacted?: boolean;
+ } = {},
): OpenClawConfig {
return {
agents: {
@@ -97,6 +101,9 @@ function createCliBackendConfig(
sessionMode: "existing",
output: "text",
input: "arg",
+ ...(params.reseedFromRawTranscriptWhenUncompacted
+ ? { reseedFromRawTranscriptWhenUncompacted: true }
+ : {}),
...(params.bundleMcp
? { bundleMcp: true, bundleMcpMode: "claude-config-file" as const }
: {}),
@@ -561,6 +568,89 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
}
});
+ it("prepares raw-tail history for safe invalidations only when the backend opts in", async () => {
+ const { dir, sessionFile } = createSessionFile();
+ appendTranscriptEntry(sessionFile, {
+ id: "msg-1",
+ parentId: null,
+ timestamp: new Date(1).toISOString(),
+ message: {
+ role: "user",
+ content: "prior no-compaction ask",
+ timestamp: 1,
+ },
+ });
+
+ try {
+ const context = await prepareCliRunContext({
+ sessionId: "session-test",
+ sessionFile,
+ workspaceDir: dir,
+ prompt: "latest ask",
+ provider: "test-cli",
+ model: "test-model",
+ timeoutMs: 1_000,
+ runId: "run-test-raw-reseed-opt-in",
+ extraSystemPrompt: "changed stable prompt",
+ extraSystemPromptStatic: "changed stable prompt",
+ cliSessionBinding: {
+ sessionId: "cli-session",
+ extraSystemPromptHash: hashCliSessionText("old stable prompt"),
+ },
+ config: createCliBackendConfig({
+ systemPromptOverride: null,
+ reseedFromRawTranscriptWhenUncompacted: true,
+ }),
+ });
+
+ expect(context.reusableCliSession).toEqual({ invalidatedReason: "system-prompt" });
+ expect(context.openClawHistoryPrompt).toContain("prior no-compaction ask");
+ expect(context.openClawHistoryPrompt).toContain("latest ask");
+ } finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ it("prepares opted-in raw-tail history for session-expired retry without disabling native resume", async () => {
+ const { dir, sessionFile } = createSessionFile();
+ appendTranscriptEntry(sessionFile, {
+ id: "msg-1",
+ parentId: null,
+ timestamp: new Date(1).toISOString(),
+ message: {
+ role: "user",
+ content: "prior resumable ask",
+ timestamp: 1,
+ },
+ });
+
+ try {
+ const context = await prepareCliRunContext({
+ sessionId: "session-test",
+ sessionFile,
+ workspaceDir: dir,
+ prompt: "latest ask",
+ provider: "test-cli",
+ model: "test-model",
+ timeoutMs: 1_000,
+ runId: "run-test-session-expired-reseed-opt-in",
+ cliSessionBinding: {
+ sessionId: "cli-session",
+ },
+ config: createCliBackendConfig({
+ systemPromptOverride: null,
+ reseedFromRawTranscriptWhenUncompacted: true,
+ }),
+ });
+
+ expect(context.reusableCliSession).toEqual({ sessionId: "cli-session" });
+ expect(context.openClawHistoryPrompt).toContain("prior resumable ask");
+ expect(context.openClawHistoryPrompt).toContain("latest ask");
+ } finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
it("applies direct-run prepend system context helpers on the CLI path", async () => {
const { dir, sessionFile } = createSessionFile();
try {
diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts
index 9a5671c13c3..f8a83a1a47f 100644
--- a/src/agents/cli-runner/prepare.ts
+++ b/src/agents/cli-runner/prepare.ts
@@ -406,18 +406,27 @@ export async function prepareCliRunContext(
cliBackendLog.warn(`cli prompt-build hook preparation failed: ${String(error)}`);
}
preparedPrompt = annotateInterSessionPromptText(preparedPrompt, params.inputProvenance);
- const openClawHistoryPrompt = reusableCliSession.sessionId
- ? undefined
- : buildCliSessionHistoryPrompt({
+ const allowRawTranscriptReseed =
+ backendResolved.config.reseedFromRawTranscriptWhenUncompacted === true;
+ const rawTranscriptReseedReason = reusableCliSession.sessionId
+ ? "session-expired"
+ : reusableCliSession.invalidatedReason;
+ const shouldPrepareOpenClawHistoryPrompt =
+ !reusableCliSession.sessionId || allowRawTranscriptReseed;
+ const openClawHistoryPrompt = shouldPrepareOpenClawHistoryPrompt
+ ? buildCliSessionHistoryPrompt({
messages: await loadCliSessionReseedMessages({
sessionId: params.sessionId,
sessionFile: params.sessionFile,
sessionKey: params.sessionKey,
agentId: params.agentId,
config: params.config,
+ allowRawTranscriptReseed,
+ rawTranscriptReseedReason,
}),
prompt: preparedPrompt,
- });
+ })
+ : undefined;
systemPrompt = appendModelIdentitySystemPrompt({
systemPrompt: applyPluginTextReplacements(systemPrompt, backendResolved.textTransforms?.input),
model: modelDisplay,
diff --git a/src/agents/cli-runner/session-history.test.ts b/src/agents/cli-runner/session-history.test.ts
index f72f018ed8a..416a970c5a5 100644
--- a/src/agents/cli-runner/session-history.test.ts
+++ b/src/agents/cli-runner/session-history.test.ts
@@ -249,6 +249,76 @@ describe("loadCliSessionReseedMessages", () => {
}
});
+ it("reseeds safe invalidated sessions from a bounded raw message tail when explicitly opted in", async () => {
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-state-"));
+ vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
+ const sessionFile = createSessionTranscript({
+ rootDir: stateDir,
+ sessionId: "session-opt-in-raw-tail",
+ messages: Array.from(
+ { length: MAX_CLI_SESSION_HISTORY_MESSAGES + 25 },
+ (_, index) => `raw-${index}`,
+ ),
+ });
+
+ try {
+ const reseed = await loadCliSessionReseedMessages({
+ sessionId: "session-opt-in-raw-tail",
+ sessionFile,
+ sessionKey: "agent:main:main",
+ agentId: "main",
+ allowRawTranscriptReseed: true,
+ rawTranscriptReseedReason: "missing-transcript",
+ });
+ expect(reseed).toHaveLength(MAX_CLI_SESSION_HISTORY_MESSAGES);
+ expect(reseed[0]).toMatchObject({ role: "user", content: "raw-25" });
+ expect(reseed.at(-1)).toMatchObject({
+ role: "user",
+ content: `raw-${MAX_CLI_SESSION_HISTORY_MESSAGES + 24}`,
+ });
+ expect(buildCliSessionHistoryPrompt({ messages: reseed, prompt: "next" })).toContain(
+ "raw-25",
+ );
+ } finally {
+ fs.rmSync(stateDir, { recursive: true, force: true });
+ }
+ });
+
+ it("does not raw-reseed auth-boundary invalidations even when opted in", async () => {
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-state-"));
+ vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
+ const sessionFile = createSessionTranscript({
+ rootDir: stateDir,
+ sessionId: "session-auth-boundary",
+ messages: ["previous account context"],
+ });
+
+ try {
+ await expect(
+ loadCliSessionReseedMessages({
+ sessionId: "session-auth-boundary",
+ sessionFile,
+ sessionKey: "agent:main:main",
+ agentId: "main",
+ allowRawTranscriptReseed: true,
+ rawTranscriptReseedReason: "auth-profile",
+ }),
+ ).resolves.toStrictEqual([]);
+ await expect(
+ loadCliSessionReseedMessages({
+ sessionId: "session-auth-boundary",
+ sessionFile,
+ sessionKey: "agent:main:main",
+ agentId: "main",
+ allowRawTranscriptReseed: true,
+ rawTranscriptReseedReason: "auth-epoch",
+ }),
+ ).resolves.toStrictEqual([]);
+ } finally {
+ fs.rmSync(stateDir, { recursive: true, force: true });
+ }
+ });
+
it("reseeds fresh CLI sessions from the latest compaction summary and post-compaction tail", async () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-state-"));
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
diff --git a/src/agents/cli-runner/session-history.ts b/src/agents/cli-runner/session-history.ts
index dca82e48b6b..402d67ebbc7 100644
--- a/src/agents/cli-runner/session-history.ts
+++ b/src/agents/cli-runner/session-history.ts
@@ -28,6 +28,21 @@ type HistoryEntry = {
summary?: unknown;
};
+type RawTranscriptReseedReason =
+ | "auth-profile"
+ | "auth-epoch"
+ | "system-prompt"
+ | "mcp"
+ | "missing-transcript"
+ | "session-expired";
+
+const RAW_TRANSCRIPT_RESEED_ALLOWED_REASONS = new Set([
+ "missing-transcript",
+ "system-prompt",
+ "mcp",
+ "session-expired",
+]);
+
function coerceHistoryText(content: unknown): string {
if (typeof content === "string") {
return content.trim();
@@ -190,20 +205,36 @@ export async function loadCliSessionReseedMessages(params: {
sessionKey?: string;
agentId?: string;
config?: OpenClawConfig;
+ allowRawTranscriptReseed?: boolean;
+ rawTranscriptReseedReason?: RawTranscriptReseedReason;
}): Promise {
const entries = await loadCliSessionEntries(params);
+ const loadRawTail = () => {
+ if (
+ params.allowRawTranscriptReseed !== true ||
+ !params.rawTranscriptReseedReason ||
+ !RAW_TRANSCRIPT_RESEED_ALLOWED_REASONS.has(params.rawTranscriptReseedReason)
+ ) {
+ return [];
+ }
+ const rawTail = entries.flatMap((entry) => {
+ const candidate = entry as HistoryEntry;
+ return candidate.type === "message" ? [candidate.message] : [];
+ });
+ return limitAgentHookHistoryMessages(rawTail, MAX_CLI_SESSION_HISTORY_MESSAGES);
+ };
const latestCompactionIndex = entries.findLastIndex((entry) => {
const candidate = entry as HistoryEntry;
return candidate.type === "compaction" && typeof candidate.summary === "string";
});
if (latestCompactionIndex < 0) {
- return [];
+ return loadRawTail();
}
const compaction = entries[latestCompactionIndex] as HistoryEntry;
const summary = typeof compaction.summary === "string" ? compaction.summary.trim() : "";
if (!summary) {
- return [];
+ return loadRawTail();
}
const tailMessages = entries.slice(latestCompactionIndex + 1).flatMap((entry) => {
diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts
index c45e49f9438..f6f7a51c725 100644
--- a/src/config/types.agent-defaults.ts
+++ b/src/config/types.agent-defaults.ts
@@ -150,6 +150,8 @@ export type CliBackendConfig = {
imagePathScope?: "temp" | "workspace";
/** Serialize runs for this CLI. */
serialize?: boolean;
+ /** Opt in to bounded raw transcript reseed before compaction for safe session resets. */
+ reseedFromRawTranscriptWhenUncompacted?: boolean;
/** Runtime reliability tuning for this backend's process lifecycle. */
reliability?: {
/** Live-session output caps for CLIs that stream JSONL through a long-lived process. */
diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts
index ca651f15735..06e86dd56ef 100644
--- a/src/config/zod-schema.core.ts
+++ b/src/config/zod-schema.core.ts
@@ -642,6 +642,7 @@ export const CliBackendSchema = z
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
imagePathScope: z.union([z.literal("temp"), z.literal("workspace")]).optional(),
serialize: z.boolean().optional(),
+ reseedFromRawTranscriptWhenUncompacted: z.boolean().optional(),
reliability: z
.object({
outputLimits: CliBackendOutputLimitsSchema,