Make compaction visible and resume final replies

When an automatic compaction happens mid-turn, chat users currently see a long stall and the run can finish without a final visible answer.

This adds an optional bundled compaction notifier hook and a one-shot compacted-transcript continuation retry when a compaction produced no user-visible final payload.
This commit is contained in:
simplyclever914
2026-05-03 14:51:03 +03:00
committed by Peter Steinberger
parent d0f0fe97a6
commit e84ceb47f6
4 changed files with 82 additions and 4 deletions

View File

@@ -162,10 +162,11 @@ Npm specs are registry-only (package name + optional exact version or dist-tag).
| Hook | Events | What it does |
| --------------------- | ------------------------------ | ----------------------------------------------------- |
| session-memory | `command:new`, `command:reset` | Saves session context to `<workspace>/memory/` |
| bootstrap-extra-files | `agent:bootstrap` | Injects additional bootstrap files from glob patterns |
| command-logger | `command` | Logs all commands to `~/.openclaw/logs/commands.log` |
| boot-md | `gateway:startup` | Runs `BOOT.md` when the gateway starts |
| session-memory | `command:new`, `command:reset` | Saves session context to `<workspace>/memory/` |
| bootstrap-extra-files | `agent:bootstrap` | Injects additional bootstrap files from glob patterns |
| command-logger | `command` | Logs all commands to `~/.openclaw/logs/commands.log` |
| compaction-notifier | `session:compact:before`, `session:compact:after` | Sends visible chat notices when session compaction starts/ends |
| boot-md | `gateway:startup` | Runs `BOOT.md` when the gateway starts |
Enable any bundled hook:
@@ -206,6 +207,12 @@ Paths resolve relative to workspace. Only recognized bootstrap basenames are loa
Logs every slash command to `~/.openclaw/logs/commands.log`.
<a id="compaction-notifier"></a>
### compaction-notifier details
Sends short status messages into the current conversation when OpenClaw starts and finishes compacting the session transcript. This makes long turns less confusing on chat surfaces because the user can see that the assistant is summarizing context and will continue after compaction.
<a id="boot-md"></a>
### boot-md details

View File

@@ -171,6 +171,8 @@ const MAX_SAME_MODEL_IDLE_TIMEOUT_RETRIES = 1;
const EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS = 30_000;
const MID_TURN_PRECHECK_CONTINUATION_PROMPT =
"Continue from the current transcript after the latest tool result. Do not repeat the original user request, and do not rerun completed tools unless the transcript shows they are still needed.";
const COMPACTION_CONTINUATION_RETRY_INSTRUCTION =
"The previous attempt compacted the conversation context before producing a final user-visible answer. Continue from the compacted transcript and produce the final answer now. Do not restart from scratch, do not repeat completed work, and do not rerun tools unless the transcript clearly lacks required evidence.";
type EmbeddedRunAttemptForRunner = Awaited<ReturnType<typeof runEmbeddedAttemptWithBackend>>;
function resolveEmbeddedRunLaneTimeoutMs(timeoutMs: number): number | undefined {
@@ -769,6 +771,7 @@ export async function runEmbeddedPiAgent(
let planningOnlyRetryAttempts = 0;
let reasoningOnlyRetryAttempts = 0;
let emptyResponseRetryAttempts = 0;
let compactionContinuationRetryAttempts = 0;
let sameModelIdleTimeoutRetries = 0;
// Cost-runaway breaker for #76293. State lives at the run-loop level
// on purpose so it survives across attempt boundaries and across
@@ -781,6 +784,7 @@ export async function runEmbeddedPiAgent(
let planningOnlyRetryInstruction: string | null = null;
let reasoningOnlyRetryInstruction: string | null = null;
let emptyResponseRetryInstruction: string | null = null;
let compactionContinuationRetryInstruction: string | null = null;
let nextAttemptPromptOverride: string | null = null;
const ackExecutionFastPathInstruction = resolveAckExecutionFastPathInstruction({
provider,
@@ -996,6 +1000,7 @@ export async function runEmbeddedPiAgent(
planningOnlyRetryInstruction,
reasoningOnlyRetryInstruction,
emptyResponseRetryInstruction,
compactionContinuationRetryInstruction,
].filter(
(value): value is string => typeof value === "string" && value.trim().length > 0,
);
@@ -2347,6 +2352,29 @@ export async function runEmbeddedPiAgent(
timedOut,
attempt,
});
if (
!emptyAssistantReplyIsSilent &&
attemptCompactionCount > 0 &&
payloadCount === 0 &&
!aborted &&
!promptError &&
!timedOut &&
!attempt.clientToolCalls &&
!attempt.yieldDetected &&
!attempt.didSendDeterministicApprovalPrompt &&
!attempt.lastToolError &&
!hasMessagingToolDeliveryEvidence(attempt) &&
compactionContinuationRetryAttempts < 1
) {
compactionContinuationRetryAttempts += 1;
compactionContinuationRetryInstruction = COMPACTION_CONTINUATION_RETRY_INSTRUCTION;
log.warn(
`compaction interrupted visible final answer: runId=${params.runId} sessionId=${params.sessionId} ` +
`compactions=${attemptCompactionCount} — retrying ${compactionContinuationRetryAttempts}/1 with compacted-transcript continuation`,
);
continue;
}
compactionContinuationRetryInstruction = null;
if (reasoningOnlyRetriesExhausted && !finalAssistantVisibleText) {
log.warn(
`reasoning-only retries exhausted: runId=${params.runId} sessionId=${params.sessionId} ` +

View File

@@ -0,0 +1,16 @@
---
name: compaction-notifier
description: "Send visible chat notices when session compaction starts and finishes."
metadata:
{ "openclaw": { "emoji": "🧹", "events": ["session:compact:before", "session:compact:after"], "always": true } }
---
# Compaction Notifier
Sends short user-visible status messages when OpenClaw compacts a session transcript. Enable with:
```bash
openclaw hooks enable compaction-notifier
```
This is useful on chat surfaces where a long turn can otherwise look stalled while context is being summarized.

View File

@@ -0,0 +1,27 @@
const handler = async (event: any) => {
try {
if (!event || !Array.isArray(event.messages)) return;
const context = event.context ?? {};
if (event.type === "session" && event.action === "compact:before") {
const messageCount = typeof context.messageCount === "number" && context.messageCount >= 0
? ` (${context.messageCount} messages)`
: "";
event.messages.push(`🧹 Compacting context${messageCount} so I can continue without losing history…`);
return;
}
if (event.type === "session" && event.action === "compact:after") {
const before = typeof context.tokensBefore === "number" ? context.tokensBefore : undefined;
const after = typeof context.tokensAfter === "number" ? context.tokensAfter : undefined;
const tokenDelta = before !== undefined && after !== undefined
? ` (${before.toLocaleString()}${after.toLocaleString()} tokens)`
: "";
event.messages.push(`✅ Context compacted${tokenDelta}. Continuing from where I left off.`);
}
} catch (error) {
console.warn(`[compaction-notifier] failed: ${error instanceof Error ? error.message : String(error)}`);
}
};
export default handler;