openclaw - ✅(Solved) Fix [Bug]: WebChat session transcript overwritten on every turn (5.2 regression — SessionManager removal) [5 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77012Fetched 2026-05-04 04:59:25
View on GitHub
Comments
2
Participants
3
Timeline
9
Reactions
2
Author
Timeline (top)
cross-referenced ×6commented ×2referenced ×1

In OpenClaw v2026.5.2, the webchat session JSONL transcript is overwritten on every turn — only the latest message exchange survives. On page refresh, all previous messages are gone. This worked correctly on v2026.4.29.

Root Cause

mirrorCodexAppServerTranscript (in run-attempt-*.js) receives the full conversation history via messagesSnapshot (built from readMirroredSessionHistoryMessages at line 3264) and rewrites ALL messages to the JSONL every turn. Each message is written via appendCodexAppServerTranscriptMessage, which calls migrateLinearTranscriptToParentLinked — a function that reads the entire file and rewrites it via fs.writeFile (not append).

The per-turn idempotencyScope (codex-app-server:${threadId}:${turnId}) changes every turn, so old messages get new keys and bypass the idempotency check. The rewrite creates new entries with different parentId chains, causing selectBoundedActiveTailRecords (the tree walk) to follow the newest branch and orphan previous entries.

In v4.29, SessionManager.open().appendMessage() truly appended to the file. The SessionManager was removed in 5.2 and replaced with this read-migrate-rewrite cycle.

Fix Action

Fix / Workaround

Controlled test on macOS arm64, v2026.5.2 (8b2a6e5), stock dist (no patches except prewarm skip):

PR fix notes

PR #77046: fix(codex/app-server): stable mirror idempotency to prevent transcript loss

Description (problem / solution / changelog)

Summary

  • Problem: On v2026.5.2 WebChat, every new turn loses the previous assistant reply. The JSONL transcript ends up with the new user message and the previous assistant reply both pointing at the same parentId (the prior user turn), forming a sibling branch. selectBoundedActiveTailRecords in src/gateway/session-utils.fs.ts only walks the newest leaf back through parentId, so the orphaned assistant entry disappears on the next refresh. Reported in #77012; the failing entries live in extensions/codex/src/app-server/transcript-mirror.ts and the mirrorTranscriptBestEffort call site at extensions/codex/src/app-server/run-attempt.ts:1782.
  • Root Cause: mirrorCodexAppServerTranscript builds its idempotency key as `${idempotencyScope}:${message.role}:${index}` while the scope passed in is `codex-app-server:${threadId}:${turnId}`. Both the index (positional in the per-turn messagesSnapshot) and the turnId half of the scope rotate, so the dedupe key has no stable identity for a given logical message. Any code path that re-emits a previously mirrored message — a retried turn, a snapshot whose ordering shifts when Codex reasoning/Codex plan records are inserted ahead of lastAssistant, or upstream context-engine flows that re-mirror prior turns — sees existingIdempotencyKeys.has(...) return false. appendSessionTranscriptMessage then runs its normal path: it reads the live leaf id from disk and appends a brand new entry whose parentId points at the leaf-at-call-time. The result is a sibling branch under a shared parent, and the tail-walk reader treats the older branch as orphaned. The fundamental defect is keying on volatile metadata (turnId + array index) instead of on the message identity itself.
  • Fix: Derive the idempotency key from a stable content fingerprint of the (role, content) pair using createHash("sha256") truncated to 16 hex chars: `${idempotencyScope}:${message.role}:${fingerprint}`. Then drop turnId from the scope at the call site so the scope is thread-stable. The two changes compose: identical (role, content) under the same thread always produces the same key, so any re-mirror of an already-recorded message is a true no-op regardless of which turn is currently active or where the message lands in the snapshot. We deliberately exclude volatile fields (timestamps, usage, abort flags) from the fingerprint — they would re-introduce the original drift. The before_message_write hook is invoked after the key is computed (line ordering preserved), so a hook that mutates content cannot reshape the dedupe key away from its anchor on the original input.
  • What changed:
    • extensions/codex/src/app-server/transcript-mirror.ts — add fingerprintMirrorMessageContent, a MirroredAgentMessage type alias narrowed via Extract<AgentMessage, { role: "user" | "assistant" }>, and switch the loop from [index, message] to for (const message of messages) with the new content-fingerprint key.
    • extensions/codex/src/app-server/run-attempt.tsmirrorTranscriptBestEffort now passes `codex-app-server:${params.threadId}` (no turnId).
    • extensions/codex/src/app-server/transcript-mirror.test.ts — update the three existing key-format assertions to match the fingerprint format (computed via the same SHA-256 helper inline) and add two regression tests covering both failure modes: (a) repeated mirror with rotated scopes and (b) repeated mirror where a reasoning record shifts the assistant's positional index.
  • What did NOT change (scope boundary):
    • The on-disk JSONL schema is unchanged: entries still carry { type, id, parentId, timestamp, message } and message.idempotencyKey is still a flat string. Existing transcripts written by 5.2 (with old :role:index keys) remain valid; their old keys simply never collide with new fingerprint keys, which is the expected behaviour for a pre-existing entry under any dedupe scheme.
    • appendSessionTranscriptMessage, migrateLinearTranscriptToParentLinked, selectBoundedActiveTailRecords, and the session write-lock are untouched.
    • Telegram and BlueBubbles channels (which mirror through different write paths) are unaffected — the change is confined to the Codex app-server mirror.
    • No public API surface changes; mirrorCodexAppServerTranscript's parameter shape is identical.
    • No any types are introduced.

Reproduction

  1. Check out openclaw/openclaw@main (current 2026.5.3, the regression carries forward from 2026.5.2).
  2. Start the Gateway with WebChat enabled, point a Codex app-server agent at it, and open a chat.
  3. Send msg1, wait for the assistant reply (reply1); take a snapshot of the session JSONL.
  4. Send msg2, wait for reply2.
  5. Diff the JSONL: msg2's entry now shares its parentId with reply1's entry — sibling branch.
  6. Refresh the WebChat page. The reader walks from reply2msg2msg1 and stops; reply1 is gone from the visible transcript.

After this PR, step 5's diff shows msg2's parentId correctly chained to reply1.id, and a re-mirror of any prior content is dropped at the dedupe gate. The same scenario is exercised hermetically by the two new tests in transcript-mirror.test.ts:

  • dedupes mirrored messages by content across rotated scopes
  • dedupes mirrored messages despite snapshot positional shifts

Risk / Mitigation

  • Risk: A genuinely new message that coincidentally has identical role+content to an earlier one in the same thread (e.g., the user types "yes" twice) would be dropped by the new dedupe. In normal WebChat traffic this is unlikely (transcript carries timestamps and surrounding turns), and the same risk exists for any content-keyed dedupe; the previous index-based key avoided it only by being unsound across re-mirrors.
  • Risk: The fingerprint relies on JSON.stringify ordering of { role, content }. The two keys are listed in a fixed object literal, so ordering is deterministic on every supported Node version (Node 22.14+; see engines in package.json). Content arrays preserve their input order.
  • Mitigation: The two new regression tests in transcript-mirror.test.ts exercise both halves of the bug surface (scope rotation and snapshot reordering) and would have failed against the index/turnId-based key. The existing deduplicates app-server turn mirrors by idempotency scope test still passes, confirming the within-turn retry behaviour is preserved. pnpm tsgo:extensions and pnpm tsgo:extensions:test are clean. The hash truncation (16 hex chars = 64 bits) is well above any practical collision threshold for a single thread's transcript.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Codex (app-server transcript mirror)
  • Tests (regression coverage in transcript-mirror.test.ts)

Linked Issue/PR

Fixes #77012

Changed files

  • extensions/codex/src/app-server/run-attempt.ts (modified, +6/-1)
  • extensions/codex/src/app-server/transcript-mirror.test.ts (modified, +134/-26)
  • extensions/codex/src/app-server/transcript-mirror.ts (modified, +19/-3)

PR #77051: test(codex): preserve mirrored transcript branch across turns

Description (problem / solution / changelog)

Summary

  • add a two-turn Codex app-server transcript mirror regression for issue #77012
  • assert consecutive mirror scopes append to one parent-linked active branch
  • verify the gateway session reader can still see both user/assistant turns after the second mirror

Test

  • pnpm test extensions/codex/src/app-server/transcript-mirror.test.ts --run

Refs #77012

Changed files

  • extensions/codex/src/app-server/transcript-mirror.test.ts (modified, +67/-0)

PR #77076: fix(webchat): render read tool output and fix exec output overflow + migrateLinearTranscript idempotent

Description (problem / solution / changelog)

Summary

This PR contains two fixes:

Fix 1: WebChat session transcript overwritten every turn (fixes #77012)

Makes migrateLinearTranscriptToParentLinked idempotent by collecting existing IDs before processing.

Fix 2: Control UI webchat tool output issues (fixes #77054)

Bug 1 - read tool output not rendered:

  • extractToolText() in tool-cards.ts only handled item.text and item.content as strings
  • Modern tool results have item.content as array: [{ type: 'text', text: '...' }]
  • Added array content block handling to extract text from nested blocks

Bug 2 - exec output text clipped:

  • .chat-tool-card had overflow:hidden which clipped long content when max-height exceeded
  • Changed to overflow:auto to allow scrolling

Files Changed

  • ui/src/ui/chat/tool-cards.ts - extractToolText array handling
  • ui/src/styles/chat/tool-cards.css - overflow:hidden to overflow:auto
  • ui/src/ui/chat/tool-cards.test.ts - new tests

Changed files

  • extensions/telegram/src/probe.test.ts (modified, +72/-0)
  • extensions/telegram/src/probe.ts (modified, +11/-2)
  • src/config/sessions/transcript-append.ts (modified, +29/-2)
  • src/config/types.telegram.ts (modified, +8/-0)
  • src/config/zod-schema.providers-core.ts (modified, +9/-0)
  • src/context-engine/registry.ts (modified, +43/-0)
  • src/plugins/loader.ts (modified, +8/-0)
  • ui/src/styles/chat/tool-cards.css (modified, +1/-1)
  • ui/src/ui/chat/tool-cards.test.ts (modified, +42/-1)
  • ui/src/ui/chat/tool-cards.ts (modified, +18/-0)

PR #77091: fix(telegram): add httpTimeoutMs config for health probe timeout control (fixes #77060)

Description (problem / solution / changelog)

Fixes #77060 - Control UI menus freezing due to Telegram blocking health probe

Changed files

  • extensions/telegram/src/probe.test.ts (modified, +72/-0)
  • extensions/telegram/src/probe.ts (modified, +11/-2)
  • src/config/sessions/transcript-append.ts (modified, +29/-2)
  • src/config/types.telegram.ts (modified, +8/-0)
  • src/config/zod-schema.providers-core.ts (modified, +9/-0)
  • src/context-engine/registry.ts (modified, +43/-0)
  • src/plugins/loader.ts (modified, +8/-0)
  • ui/src/styles/chat/tool-cards.css (modified, +1/-1)
  • ui/src/ui/chat/tool-cards.test.ts (modified, +42/-1)
  • ui/src/ui/chat/tool-cards.ts (modified, +18/-0)

PR #77092: fix: persist and restore registered context engines in plugin cache (fixes #77063)

Description (problem / solution / changelog)

Fixes #77063 - lossless-claw selected and enabled but not registered as context engine

Root cause: When a cached plugin registry is reused, context engines registered by plugins (like lossless-claw) are lost because the cached state restoration did not restore registered context engines.

Changes:

  • src/context-engine/registry.ts: Add RegisteredContextEngineEntry type, listRegisteredContextEngines(), and restoreRegisteredContextEngines() functions
  • src/plugins/loader.ts: Add contextEngines field to CachedPluginState, restore context engines when using cached state, and save contextEngines to cache

Changed files

  • extensions/telegram/src/probe.test.ts (modified, +72/-0)
  • extensions/telegram/src/probe.ts (modified, +11/-2)
  • src/config/sessions/transcript-append.ts (modified, +29/-2)
  • src/config/types.telegram.ts (modified, +8/-0)
  • src/config/zod-schema.providers-core.ts (modified, +9/-0)
  • src/context-engine/registry.ts (modified, +43/-0)
  • src/plugins/loader.ts (modified, +8/-0)
  • ui/src/styles/chat/tool-cards.css (modified, +1/-1)
  • ui/src/ui/chat/tool-cards.test.ts (modified, +42/-1)
  • ui/src/ui/chat/tool-cards.ts (modified, +18/-0)
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

In OpenClaw v2026.5.2, the webchat session JSONL transcript is overwritten on every turn — only the latest message exchange survives. On page refresh, all previous messages are gone. This worked correctly on v2026.4.29.

Root cause

mirrorCodexAppServerTranscript (in run-attempt-*.js) receives the full conversation history via messagesSnapshot (built from readMirroredSessionHistoryMessages at line 3264) and rewrites ALL messages to the JSONL every turn. Each message is written via appendCodexAppServerTranscriptMessage, which calls migrateLinearTranscriptToParentLinked — a function that reads the entire file and rewrites it via fs.writeFile (not append).

The per-turn idempotencyScope (codex-app-server:${threadId}:${turnId}) changes every turn, so old messages get new keys and bypass the idempotency check. The rewrite creates new entries with different parentId chains, causing selectBoundedActiveTailRecords (the tree walk) to follow the newest branch and orphan previous entries.

In v4.29, SessionManager.open().appendMessage() truly appended to the file. The SessionManager was removed in 5.2 and replaced with this read-migrate-rewrite cycle.

Steps to reproduce

  1. Open webchat on v2026.5.2
  2. Send message 1, receive response
  3. Send message 2, receive response
  4. Check the session JSONL — message 1's assistant response is gone
  5. Refresh page — only message 2 and its response are visible

Evidence

Controlled test on macOS arm64, v2026.5.2 (8b2a6e5), stock dist (no patches except prewarm skip):

  • Took a JSONL snapshot between turns
  • Diffed after the next turn
  • The previous assistant response (id 44b4ec62, parentId d3471f25) was replaced by the new user message (id 9f4b19a1, parentId d3471f25) — same parent, sibling branch
  • The tree walk follows the new branch, orphaning the old assistant entry
  • 9 minutes between turns — no race condition, purely the mirror rewrite logic

Contributing factors

  1. migrateLinearTranscriptToParentLinked rewrites via writeFile — any concurrent access or stale read loses data
  2. Per-turn turnId in idempotency scope — old messages get new keys, bypassing dedup
  3. Write lock race (#57019)releaseHeldLock deletes HELD_LOCKS before async cleanup, allowing concurrent acquirers on macOS
  4. appendCodexAppServerTranscriptMessage calls migration inside the lock but mirrorCodexAppServerTranscript takes a separate lock per batch — interleaving between the webchat write path and the mirror write path can race

Code references (v2026.5.2 dist)

  • run-attempt-CektiLYp.js:3264finalMessages = readMirroredSessionHistoryMessages(sessionFile) (full history)
  • run-attempt-CektiLYp.js:3598idempotencyScope: codex-app-server:${threadId}:${turnId} (per-turn keys)
  • run-attempt-CektiLYp.js:2082appendCodexAppServerTranscriptMessage triggers migration
  • transcript-C_uDP9Gl.js:121migrateLinearTranscriptToParentLinked rewrites file via writeFile
  • transcript-C_uDP9Gl.js:156fs.writeFile(transcriptPath, ...) — destructive overwrite
  • session-utils.fs-W0CAUUsv.js:564selectBoundedActiveTailRecords tree walk follows one branch

What changed (4.29 → 5.2)

4.295.2
Write methodSessionManager.open().appendMessage() — true appendappendCodexAppServerTranscriptMessagemigrateLinearTranscriptToParentLinkedwriteFile — full rewrite
Session readreadSessionMessages — sync, reads allreadRecentSessionMessagesAsync — async, tail window + tree walk
Branch resolutionSessionManager.open().getBranch()selectBoundedActiveTailRecords — drops orphaned branches
Mirror scopeCurrent turn onlyFull messagesSnapshot (entire history)
IdempotencyN/A (true append)Per-turn scope — changes every turn, old messages get new keys

Environment

  • OpenClaw: v2026.5.2 (8b2a6e5), npm global install
  • OS: macOS 26.4.0 arm64 (Mac Mini M4 Pro)
  • Node: 22
  • Channels affected: WebChat (Telegram and BlueBubbles unaffected — different write path)

Related issues

  • #57019 — Write lock race (contributing factor, not root cause)
  • #76804 — Assistant text not persisted (separate issue, PR #76819 merged)
  • #76892, #76763, #76384 — Duplicate reports of history loss
  • #51549 — Architectural gap in webchat persistence

extent analysis

TL;DR

The issue can be fixed by modifying the migrateLinearTranscriptToParentLinked function to append to the JSONL file instead of overwriting it.

Guidance

  • Identify the migrateLinearTranscriptToParentLinked function in transcript-C_uDP9Gl.js and modify it to use fs.appendFile instead of fs.writeFile to prevent overwriting the entire file.
  • Review the appendCodexAppServerTranscriptMessage function in run-attempt-CektiLYp.js to ensure it correctly handles the new append behavior.
  • Consider implementing a locking mechanism to prevent concurrent access to the JSONL file and ensure data integrity.
  • Verify the fix by testing the webchat functionality and checking the JSONL file for correct message persistence.

Example

// Modified migrateLinearTranscriptToParentLinked function
const fs = require('fs');
const path = require('path');

function migrateLinearTranscriptToParentLinked(transcriptPath, messages) {
  // ...
  fs.appendFile(transcriptPath, JSON.stringify(messages) + '\n', (err) => {
    if (err) {
      console.error(err);
    }
  });
}

Notes

The provided fix assumes that the migrateLinearTranscriptToParentLinked function is the root cause of the issue. However, the contributing factors mentioned in the issue, such as the write lock race and per-turn idempotency scope, may still need to be addressed to ensure the fix is robust.

Recommendation

Apply the workaround by modifying the migrateLinearTranscriptToParentLinked function to append to the JSONL file instead of overwriting it, as this is the most direct fix for the identified root cause.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: WebChat session transcript overwritten on every turn (5.2 regression — SessionManager removal) [5 pull requests, 2 comments, 3 participants]