← Back to Plugins
Tools

Plugin Deferred Compaction

sakilabo By sakilabo 👁 24 views ▲ 0 votes

Let OpenClaw agents compact their own Codex-backed context after the current turn.

GitHub

Install

npm install --legacy-peer-deps

Configuration Example

const result = await request_context_compaction({
  agentId: "ops",
  sessionKey: "agent:ops:main",
  message: "Save anything that must survive, then call compact_context_after_turn.",
  idempotencyKey: `idle-${trigger.state?.idleSince}`,
});

README

# Deferred Compaction

**This plugin lets an OpenClaw agent compact its own context.**

It adds a `compact_context_after_turn` tool to OpenClaw agents running on the Codex harness. When an agent calls the tool, the plugin compacts the context of the same session after the current turn ends. It also adds `request_context_compaction`, which lets automation such as a cron trigger ask a session to compact its context.

The central problem this plugin solves is not when or how often compaction runs. **By default, an agent cannot execute context compaction itself.** Giving the agent that ability enables workflows such as:

- Compacting its own context automatically once a day
- Compacting when it decides that its context has grown too large
- Compacting when instructed to do so by the user

"Once a day" is only an example. It does not mean that this plugin runs on a fixed schedule. This plugin is not a scheduler; it gives OpenClaw agents the context-compaction capability they otherwise lack.

## Supported versions and requirements

- Tested with OpenClaw `2026.10.3`
- Tested with `@openclaw/codex@2026.10.3`
- Supports sessions where `session_windows.agent_harness_id = 'codex'`
- Does not support sessions using other harnesses
- Other versions have not been tested

This plugin depends on internal OpenClaw and Codex plugin implementation details. Compatibility must therefore be checked after updating OpenClaw or `@openclaw/codex`. In particular, it uses the disposer symbol on `globalThis`, the `session_windows` and `cron_run_receipts` schemas, the agent and state directory layouts, the `sessions.compact` and `chat.send` Gateway RPC methods, and the `openclaw message send` CLI command.

## Why this plugin is needed

### Agents cannot execute compaction

OpenClaw provides `/codex compact` for users to enter. However, it is a chat command, not a tool that an agent can call. The `openclaw sessions compact` CLI command is likewise not exposed as a context-compaction tool for agents.

Consequently, a standard agent may determine that it has not compacted today or that its context has grown too large, but it still cannot perform the compaction itself. Writing an automatic-execution rule or instruction is not enough; the agent needs an actual compaction tool that it can call.

This plugin fills that gap by adding `compact_context_after_turn`.

## How it works

When the agent calls `compact_context_after_turn`, the plugin performs the following steps. An agent cannot replace its own context in the middle of a turn, so compaction is deferred until the turn ends. Reserving compaction is the implementation mechanism; the purpose is to let the agent compact its own context.

1. While the tool is running within the normal turn, wait up to `idleWaitMs` (10 seconds by default) for other Codex sessions to finish. If the wait expires, return a tool error without reserving compaction.
2. Read the pre-compaction token count through the Plugin Runtime API and reserve compaction.
3. After `agent_end`, wait for `delayMs` (1.5 seconds by default), then check the session state again. If another session conflicts, wait up to `postTurnIdleWaitMs` (15 seconds by default). If it is still running, treat the compaction as failed.
4. Close the Codex app-server connections for all agents and release the thread writer locks. The connections are recreated on the next turn.
5. Run `sessions compact --json` through the OpenClaw CLI.
6. Log the compaction result and the token counts before and after compaction.
7. Whether compaction succeeds or fails, send a message to the original session through `chat.send`.

All waiting is designed to occur within the turn. Once `agent_end` has fired, the turn is already over and the plugin cannot wait indefinitely; a conflict at that point is treated as a failed attempt.

Reservation state is shared in process memory through `globalThis`. Reservations are shared even if OpenClaw loads the plugin as multiple registration instances, and they are discarded automatically when the Gateway restarts. They are not persisted to a file or database.

## Requesting compaction from automation

`request_context_compaction` sends a message to another session as a new turn through `chat.send`. The message should tell that session to call `compact_context_after_turn`. The tool returns as soon as the request is accepted, and the plugin delivers it in the Gateway process afterwards. Cron trigger scripts call tools as global functions, so a trigger can request compaction within its evaluation budget without starting a process of its own.

| Parameter | Required | Description |
| --- | --- | --- |
| `agentId` | Yes | Agent that owns the target session |
| `sessionKey` | Yes | Session to compact |
| `message` | Yes | Message delivered to the session |
| `idempotencyKey` | No | Key that deduplicates repeated requests; a random key is used when omitted |
| `failureChannel`, `failureTarget` | No | Destination of a failure notice sent through `openclaw message send`; give both or neither |
| `failureMessage` | No | Text placed before the failure reason in the notice |

The tool returns `{ accepted: true, agentId, sessionKey, idempotencyKey }`. A delivery failure is logged as `compaction_request_failed` with the CLI exit code, signal, standard output, and standard error, and is sent as the failure notice when a destination is given.

The tool is registered as optional, so it is available only to agents whose tool policy allows it explicitly, for example through `tools.alsoAllow`.

Example trigger script fragment:

```js
const result = await request_context_compaction({
  agentId: "ops",
  sessionKey: "agent:ops:main",
  message: "Save anything that must survive, then call compact_context_after_turn.",
  idempotencyKey: `idle-${trigger.state?.idleSince}`,
});
```

## Session notifications

Because compaction runs after the turn ends, the agent cannot continue without learning the result. The reservation and completion notifications therefore form a pair.

| Event | Recipient | Message |
| --- | --- | --- |
| Reserved (tool result) | Agent | Session compaction is reserved. Tell the user to wait: this session will be unavailable for a few minutes, until compaction finishes. |
| Completed | Session | Session compaction finished.<br>Context tokens: 58352 -> 28436 of 258400. |
| Failed | Session | Session compaction failed. Reason: …<br>The context is left uncompacted.<br>Do not run compaction again. |

The completion message includes the token counts before and after compaction and the session's context window. This prevents the agent from having to look up the result itself. If neither token count can be obtained, the line is omitted. If only one is available, only that value is reported, as in `Context tokens after compaction: …`.

### Scheduled jobs that overlap compaction

Compaction releases the Codex app-server connection and occupies the session. A scheduled job starting at almost the same time can therefore be affected. Because that run occurs outside the agent's own turn, the session would not know about it without a notification.

The plugin reads `cron_run_receipts` in the state database to find runs for the same agent that overlap the compaction window, from the beginning of deferred execution until immediately before notification. Only receipts with the `skipped` status are excluded. That status means the trigger condition was not met and no run took place, so excluding it does not omit an actual execution.

The plugin does not filter by any other status because a receipt records only the job's own outcome, not whether it overlapped compaction. A run affected by compaction may still finish successfully and be recorded as `ok`. Filtering by status would hide exactly the cases that need review.

When overlapping runs exist, the following lines are appended to the completion or failure message:

```
1 scheduled job ran while compaction was running:
- job 01234567-89ab-cdef-0123-456789abcdef: ok
Confirm that compaction caused no problem for it before resuming your work.
```

The final line identifies both what to inspect—the listed job runs—and why—to determine whether compaction caused a problem. Without it, the receiving agent would have to infer why the list was included.

If no runs overlap, nothing is appended and the message reports only the compaction result.

The state database path is resolved with the plugin SDK's `resolveStateDir`, and `state/openclaw.sqlite` is opened read-only. A read failure does not prevent the notification from being sent, because failing to announce that compaction has ended would be more harmful. Such failures are recorded as `compaction_window_jobs_failed`.

Completion and failure messages are sent through the Gateway RPC method `chat.send` with `deliver: true`. The in-process `dispatchGatewayMethod` is available only within the scope of authenticated plugin HTTP routes, so this plugin invokes it through the OpenClaw CLI (`gateway call chat.send`), as it does for compaction. The message becomes session input, starts one agent turn, and delivers the resulting reply to the channel.

The plugin uses `chat.send` rather than `sessions.send` because `sessions.send` cannot carry a delivery option. It delegates a fixed set of parameters to `chat.send`; without `deliver`, the reply is routed to the internal message channel. The turn still runs, but the response reaches only Gateway subscribers such as Control UI and is not delivered to an external channel. With `deliver: true`, OpenClaw resolves the destination from the session's own delivery context, keeping the plugin independent of any particular channel type. Sessions without an external route, including Control UI and WebChat sessions, continue to record the response only in the transcript. Because `chat.send` requires an `idempotencyKey`, the plugin generates a UUID for every send.

A notification is always sent after failure as well. Otherwise, an agent in

... (truncated)
tools

Comments

Sign in to leave a comment

Loading comments...