Skip to content

Role-ordering-conflict auto-reset hard-deletes the previous session transcript with no archive (fs.unlinkSync), keeping only the replayed tail #97529

Description

@yetval

Summary

When the agent runner performs the automatic session reset triggered by a provider role-ordering conflict, the previous conversation transcript is hard-deleted from disk with fs.unlinkSync and no archive is written. Only the last few user/assistant messages are replayed into the new session (cap of 6), so the rest of the conversation is gone with no recoverable .reset/.deleted/archive copy and nothing for memory-core to re-index. Every other reset/rollover path archives the previous transcript; only this path unlinks it.

Environment

  • Commit: 891096926e272e5e3c24fae66133876b8dbe7060 (origin/main at time of filing)
  • Surface: auto-reply agent runner session reset (resetSessionAfterRoleOrderingConflict) -> file session storage lifecycle
  • Trigger provider class: OpenAI-compatible providers that return strict role-alternation errors mid-turn

Steps to reproduce

  1. Hold a conversation in a DM/session long enough that the transcript has more than 6 user/assistant messages.
  2. Have the provider return a role-ordering conflict during a turn (for example "roles must alternate" / "Incorrect role information"), so the trailing-user-message repair at embedded-agent-runner/run/attempt.ts cannot reshape the transcript.
  3. The runner calls resetSessionAfterRoleOrderingConflict, which routes to persistSessionResetLifecycle with cleanupPreviousTranscript: true.
  4. Inspect the sessions directory afterward.

Expected

The previous transcript should be preserved the same way /reset and /new preserve it: rotated to a recoverable archive (a .jsonl.reset.<ts> file) so memory-core can re-index it and the conversation remains recoverable.

Actual

The previous transcript file is unlinked from disk. No .reset/.deleted/archive copy is written. Only the replayed tail (at most 6 user/assistant messages; 0 when the tail is assistant-only) is carried into the new session. The loss is silent (no user-facing notice).

Root cause

src/config/sessions/session-accessor.ts. persistSessionResetLifecycle replays only the recent tail, then on the cleanup flag calls cleanupPreviousResetTranscripts, which hard-deletes:

// session-accessor.ts  cleanupPreviousResetTranscripts
for (const candidate of transcriptCandidates) {
  try {
    fs.unlinkSync(candidate);
  } catch {
    // Best-effort cleanup.
  }
}

The asymmetry: the normal /reset and /new rotation goes through archivePreviousSessionTranscript, which calls the archive runtime with reason: "reset" and produces a recoverable, memory-indexed .jsonl.reset.<ts>:

// session-accessor.ts  archivePreviousSessionTranscript
const archivedTranscripts = archiveSessionTranscriptsDetailed({
  sessionId: params.previousEntry.sessionId,
  ...
  reason: "reset",
  onArchiveError: params.onArchiveError,
});

Only the role-ordering-conflict path uses the unlink branch.

The replay cap is DEFAULT_REPLAY_MAX_MESSAGES = 6 (src/config/sessions/transcript-replay.ts). replayRecentUserAssistantMessages carries at most that many user/assistant messages, and an assistant-only tail replays 0, so the new session can start empty.

Trigger path: resetSessionAfterRoleOrderingConflict (src/auto-reply/reply/agent-runner.ts) is the sole caller passing cleanupTranscripts: true. It is threaded as cleanupPreviousTranscript through agent-runner-session-reset.ts into persistSessionResetLifecycle. The original role-ordering auto-reset (#998) deleted the transcript on purpose to clear a corrupt session; that delete-without-archive predates the .reset archive convention now used by every other rotation, which is the divergence.

Related issues (distinct, not duplicates)

Real behavior proof

Behavior addressed: role-ordering-conflict auto-reset hard-deletes the previous transcript with no archive, keeping only the replayed tail.
Real environment tested: drove the real exported persistSessionResetLifecycle from src/config/sessions/session-accessor.ts with cleanupPreviousTranscript: true against a 25-turn transcript laid out in the real agents/<id>/sessions/ shape, at commit 891096926e272e5e3c24fae66133876b8dbe7060.
Exact steps or command run after this patch: node --import tsx /tmp/openclaw-bug-repros/reset-role-ordering-unlink/repro.mjs
Evidence after fix:

# OBSERVED (buggy, origin/main 891096926e)
=== Scenario 1: normal tail (ends with user turn) ===
previous transcript user messages BEFORE: 25
previous transcript file still on disk?  : false
archive/.reset/.deleted copies preserved : 0 []
user messages carried into NEW session   : 3
user messages PERMANENTLY LOST           : 22

=== Scenario 2: assistant-only tail (conflict mid-assistant) ===
previous transcript user messages BEFORE: 25
previous transcript file still on disk?  : false
archive/.reset/.deleted copies preserved : 0 []
user messages carried into NEW session   : 2
user messages PERMANENTLY LOST           : 23

# EXPECTED (identical inputs, cleanup replaced locally with an archive-style preserving rename)
=== Scenario 1: normal tail (ends with user turn) ===
previous transcript user messages BEFORE: 25
previous transcript file still on disk?  : false
archive/.reset/.deleted copies preserved : 1 ["aaaaaaaa-1111-2222-3333-prevprevprev.jsonl.reset.1782664900271"]
user messages carried into NEW session   : 3

Observed result after fix: on current origin/main the previous transcript is removed with zero recoverable archive copies; replacing the unlink with a preserving rename leaves one recoverable archive on the same inputs.
What was not tested: I did not drive a live provider to emit the role-ordering error end to end, and did not exercise the database-backed session store (the repro covers the file session storage lifecycle only). The EXPECTED run used a preserving rename to demonstrate recoverability; it does not assert the full memory-core re-index step.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:data-lossCan lose, corrupt, or silently drop user/session/config data.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maturity:stableIssue affects a taxonomy feature currently scored M4/M5.no-staleExclude from stale automation

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions