← Back to Plugins
Tools

Agent Postmortem

branpurn By branpurn 👁 36 views ▲ 0 votes

OpenClaw plugin: when an agent turn returns nothing, detect it, ask the agent to check what actually landed, and complete what's missing

GitHub

Install

npm install &&

README

# agent-postmortem

An OpenClaw plugin for the failure where your agent goes quiet.

A run does some work, then returns nothing. You see *"Agent couldn't generate a
response. Note: some tool actions may have already been executed — please verify
before retrying."* Files may be half-written. Nothing tells you which half.

This plugin notices, waits until the provider can answer, and asks the agent to
check the actual state and report back — completing whatever is genuinely
missing. By the time you look, the session says what landed and what didn't.

## Why

Born from running OpenClaw on modest self-hosted hardware. A smaller local model
working a long task will occasionally choke, and you return to a session parked
in a vague error state. The path of least resistance is to type "what happened?"
and let the model explain itself.

The insight: a smaller model is usually perfectly capable of diagnosing its own
failure once prompted, and often lands on the cause. This just does the prompting
for you.

## Behavior

```
run produces no reply  →  wait until the provider is ready  →  probe  →  diagnosis
```

- **Arms on any failure.** No error-string enumeration. It arms unless the run
  ended in a clean reply or a deliberate abort, so timeouts, cooldowns, empty
  turns, and future error variants are caught by construction.
- **The probe is the readiness check.** While the provider is cooled, OpenClaw
  rejects the attempt in ~20ms without contacting it, so retrying is cheap and
  can't extend the cooldown. When ready, the same attempt succeeds and delivers.
  One act, no separate health check.
- **Your return cancels it.** If you come back and run anything successfully
  first, the pending diagnosis is dropped silently. It only fires into a session
  you walked away from.
- **It survives its own flakiness.** On a model prone to empty turns, the probe
  itself can come back empty. The retry loop rides that out — observed in
  testing: probe attempt 1 returned nothing, attempt 2 delivered.
- **Bounded.** One pending per session; gives up after `freshnessMinutes` or
  `maxAttempts`.

## What a delivered post-mortem looks like

> Nothing completed because the previous turn failed before any writes occurred.
> I have now added both tasks to `todo/tasks.md`, regenerated the Eisenhower
> matrix, and added coffee and sugar to your priority shopping list.

Note it both **diagnosed and recovered**. That is deliberate — see the prompt
note under Config.

## Classifying failure: what actually works

This is the part worth reading if you're writing anything against `agent_end`.

The obvious approach is `event.success`, which the hook type advertises:

```ts
type PluginHookAgentEndEvent = {
  runId?: string; messages: unknown[];
  success: boolean; error?: string; durationMs?: number;
};
```

**It does not work.** Measured on OpenClaw 2026.7.1-2:

| Case | msgs | assistant textLen | `event.success` | `event.error` | `stopReason` |
|---|---|---|---|---|---|
| Clean reply | 2 | 5 | `true` | `""` | `"stop"` |
| Incomplete turn | 8 | 0 | `true` | `""` | `"stop"` |
| Died mid-work | 6 | 0 | `true` | `""` | `"stop"` |

The runtime reports `success: true` for runs the user watched fail, and leaves
`error` empty. The only field that tracks reality is **whether the final
assistant message carried any text.**

So `classify()` uses:

1. `abort` if the terminal mentions abort/takeover/cancel — genuine interrupts
   surface as `stopReason: "aborted"`.
2. `success` if the assistant message has text and no error.
3. `success` if the reply went out through the messaging tool (`deliveredViaTool`),
   which legitimately leaves an empty assistant turn.
4. `failure` otherwise — no text, nothing delivered.

An earlier revision mapped `stopReason === "stop"` with no text to `abort`, on
the theory it meant an escape or `/stop`. That is exactly the incomplete-turn
signature, so the plugin silently ignored the single most common real failure.
Removed.

## Install

```bash
./setup.sh
```

Builds, installs, enables, grants the hook permission, and restarts the gateway.
Needs `openclaw` and `npm` on `PATH`.

<details>
<summary>Manual install</summary>

```bash
npm install && npm run build            # emits dist/index.js
openclaw plugins install ./agent-postmortem --force
openclaw config set plugins.entries.agent-postmortem.enabled true
openclaw config set plugins.entries.agent-postmortem.hooks.allowConversationAccess true
openclaw config validate
openclaw gateway restart
```
</details>

## Config

`plugins.entries.agent-postmortem.config`:

| Key | Default | Meaning |
|-----|---------|---------|
| `prompt` | see below | The message sent into the session. |
| `appendReason` | `true` | Append the captured reason to the prompt. |
| `freshnessMinutes` | `15` | Give up if not delivered within this window. |
| `pollSeconds` | `15` | Retry cadence. Flat — OpenClaw's cooldown gating handles backoff. |
| `maxAttempts` | `60` | Hard cap (freshness usually ends it first). |
| `debug` | `false` | Log per-run classification. Turn on when calibrating a new model. |

**On the default prompt.** It used to be `"What happened?"`. The model reads that
as *"report on the task"* and will cheerfully redo the work — harmless when
nothing landed, a duplicate-write risk when a run died halfway. The default now
asks it to check current file state first and complete only what is genuinely
missing:

> Your previous turn ended without sending a reply. Check the current state of
> any files you were working on, then tell me in 2-3 sentences what actually
> completed, what did not, and anything left inconsistent. Complete only what is
> genuinely missing — do not redo work that already landed.

**On `appendReason`.** The raw terminal is useless to paste at a model — the
dominant failure reports `stopReason: "stop"` with no message, which produced
*"(An earlier run failed: stop.)"*. `describeReason()` now renders that signature
as *"the turn ended without producing a reply (tool calls may have partly run)"*.

## How it works

Listens on `agent_end`. Classifies each run, and on failure arms a per-session
timer that invokes `openclaw agent --session-key <key> --message <prompt>`.
Delivery is confirmed from the probe's own `agent_end` — identified by the last
user message matching the configured prompt — which both proves the provider
recovered and clears pending state.

## Known limitations

- **In-memory state.** A pending post-mortem is lost if the gateway restarts
  between failure and recovery. A real cooldown doesn't restart the gateway, so
  this mostly affects manual stop/start testing.
- **In-flight race.** If your successful run races an already-executing probe,
  that probe may still deliver right after you. Cancel stops future probes, not
  in-flight ones. Rare and harmless.
- **It may act, not just report.** Because the prompt asks it to complete what's
  missing, the agent can perform work *after* reporting the failure to you. That
  is usually what you want, but it means a session can change state while you are
  away. Set `prompt` to something purely diagnostic if you'd rather it only
  looked.
- **Delivery via CLI.** The probe shells out to `openclaw`. An in-process
  delivery is a possible refinement once the parameter shape is pinned down.

## License

MIT
tools

Comments

Sign in to leave a comment

Loading comments...