#Undo message

1 messages · Page 1 of 1 (latest)

tired parcel
#

How to undo message in current session just like opencode?

rapid sleetBOT
lunar canyonBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

modest orbitBOT
#

Not exactly. I don’t see an OpenClaw equivalent of OpenCode’s “undo last message” in the current docs/source.

Closest options:

  • /reset or /new → start a fresh session for this chat/thread
  • /compact → keep the session, but summarize older context to shrink it

So if you want to fully wipe the current thread context, send:

/reset

Docs:

#

If you mean “remove only the last turn, but keep everything before it”, that does not look supported right now.

tired parcel
#

Yes I really means “remove only the last turn, but keep everything before it”, as I tried to manually edit json in session folder, and it seems like openclaw parse it properly. Maybe I need to make this as a skill?

modest orbitBOT
#

Yeah, if manual edit “worked”, that makes sense. The transcript is real JSONL on disk, and OpenClaw rebuilds context from it.

But I would not make this a normal skill.

Better options, in order:

  1. Core feature / GitHub issue
  2. Plugin command like /undo
  3. Raw transcript hack as a private experiment only

Why plugin command, not skill:

  • skills go through the model, which is the wrong layer for precise session mutation
  • plugin commands bypass the LLM
  • plugin command context gets sessionKey, sessionId, and sessionFile, which is exactly what you need

Important caveat: this is not just “delete the last line”.

#

The transcript is a tree with id + parentId, so “undo last turn” should usually remove:

  • the last user message
  • the assistant reply
  • any tool calls / tool results descended from that turn

Also, sessions.json is mutable, but the gateway is still the authority and may rewrite state while live, so doing transcript surgery behind its back is brittle.

So my recommendation is:

  • don’t ship this as a skill
  • if you want to prototype it, make a plugin command
  • if you want it to be solid, it probably deserves a first-class /undo feature

Docs:

#

If you want, I can sketch the minimal /undo plugin design.

tired parcel
#

Can you build the plugin for me?

modest orbitBOT
#

I can’t ship files from Discord, but I can get you very close. I’d make it a plugin command, not a skill.

This starter adds /undo and does a conservative transcript rewrite with an automatic backup.

package.json

{
  "name": "@local/openclaw-session-undo",
  "version": "0.0.1",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": "YOUR_OPENCLAW_VERSION",
      "minGatewayVersion": "YOUR_OPENCLAW_VERSION"
#
    },
    "build": {
      "openclawVersion": "YOUR_OPENCLAW_VERSION",
      "pluginSdkVersion": "YOUR_OPENCLAW_VERSION"
    }
  }
}

openclaw.plugin.json

{
  "id": "session-undo",
  "name": "Session Undo",
#
  "description": "Adds /undo to remove the last user turn from the active session transcript.",
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}

index.ts

import fs from "node:fs/promises";
import path from "node:path";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
#
type TranscriptLine = {
  type?: string;
  id?: string;
  parentId?: string;
  message?: {
    role?: string;
  };
};

function backupSuffix(): string {
  return new Date().toISOString().replace(/[:.]/g, "-");
}

function tryParseJson(line: string): TranscriptLine | null {
  try {
#
    return JSON.parse(line) as TranscriptLine;
  } catch {
    return null;
  }
}

export default definePluginEntry({
  id: "session-undo",
  name: "Session Undo",
  description: "Adds /undo to remove the last user turn from the active session transcript.",
  register(api) {
    api.registerCommand({
      name: "undo",
      description: "Remove the last user turn from the current session",
      async handler(ctx) {
#
        if (!ctx.sessionFile) {
          return { text: "No active session file found for this conversation." };
        }

        const sessionFile = ctx.sessionFile;
        const raw = await fs.readFile(sessionFile, "utf8");
        const lines = raw.split(/\r?\n/).filter((line) => line.length > 0);

        if (lines.length <= 1) {
          return { text: "Nothing to undo." };
        }

        const parsed = lines.map((rawLine, index) => ({
          index,
          raw: rawLine,
#
          json: tryParseJson(rawLine),
        }));

        const header = parsed[0]?.json;
        if (!header || header.type !== "session") {
          return { text: "Session header missing or invalid, refusing to edit transcript." };
        }

        let lastUserIndex = -1;
        for (let i = parsed.length - 1; i >= 1; i -= 1) {
          const entry = parsed[i]?.json;
          if (entry?.type === "message" && entry?.message?.role === "user") {
            lastUserIndex = i;
            break;
          }
#
        }

        if (lastUserIndex < 0) {
          return { text: "No user turn found to undo." };
        }

        const target = parsed[lastUserIndex]!;
        const removeIndexes = new Set<number>([lastUserIndex]);

        const targetId = typeof target.json?.id === "string" ? target.json.id : "";

        if (targetId) {
          const removeIds = new Set<string>([targetId]);
          let changed = true;
#
          while (changed) {
            changed = false;

            for (let i = 1; i < parsed.length; i += 1) {
              if (removeIndexes.has(i)) continue;
              const entry = parsed[i]?.json;
              if (!entry) continue;

              const parentId = typeof entry.parentId === "string" ? entry.parentId : "";
              const id = typeof entry.id === "string" ? entry.id : "";

              if (parentId && removeIds.has(parentId)) {
                removeIndexes.add(i);
                if (id) removeIds.add(id);
                changed = true;
#
              }
            }
          }
        } else {
          for (let i = lastUserIndex + 1; i < parsed.length; i += 1) {
            removeIndexes.add(i);
          }
        }

        const keptLines = parsed
          .filter((entry) => !removeIndexes.has(entry.index))
          .map((entry) => entry.raw);

        if (keptLines.length < 1) {
          return { text: "Refusing to write an empty transcript." };
#
        }

        const backupPath = `${sessionFile}.undo.${backupSuffix()}`;
        await fs.copyFile(sessionFile, backupPath);
        await fs.writeFile(sessionFile, `${keptLines.join("\n")}\n`, "utf8");

        const removedCount = parsed.length - keptLines.length;

        return {
          text:
            `Undid the last turn. Removed ${removedCount} transcript ` +
            `${removedCount === 1 ? "entry" : "entries"}.\n` +
            `Backup: ${path.basename(backupPath)}`
        };
      },
#
    });
  },
});

Install

If you have a global install:

openclaw plugins install -l ./session-undo
openclaw plugins enable session-undo
openclaw gateway restart

If you run from source, use pnpm openclaw ... instead.