Skip to content

fix(session): fall back to reset archive in chat.history when primary transcript is missing#60409

Closed
CadanHu wants to merge 10 commits into
openclaw:mainfrom
CadanHu:fix/session-history-archive-fallback
Closed

fix(session): fall back to reset archive in chat.history when primary transcript is missing#60409
CadanHu wants to merge 10 commits into
openclaw:mainfrom
CadanHu:fix/session-history-archive-fallback

Conversation

@CadanHu

@CadanHu CadanHu commented Apr 3, 2026

Copy link
Copy Markdown

Summary

When a session resets (daily 4 AM reset or manual /new//reset), the primary transcript is archived as <sessionId>.jsonl.reset.<timestamp>. After the reset, chat.history and sessions_history returned an empty response for that session key because readSessionMessages only checked the primary file path.

This PR makes readSessionMessages fall back to the most recent .reset.* archive when no primary transcript exists, so context is not silently lost after a reset.

Changes

  • src/gateway/session-transcript-files.fs.ts: add findLatestResetArchive(sessionId, sessionsDir) — scans for and returns the path of the most recent reset archive for a session
  • src/gateway/session-utils.fs.ts: call findLatestResetArchive as a fallback in readSessionMessages when no primary transcript is found
  • src/gateway/session-utils.fs.test.ts: 4 new tests covering the fallback (archive used when primary missing, primary preferred when both exist, latest archive picked when multiple exist, empty when neither exists)

Behaviour

  • No change when the primary transcript exists (existing path unaffected)
  • When the primary transcript is missing, the most recent .reset.<timestamp> archive is served instead of an empty result
  • Sessions with no primary and no archive still return []

Related issues

Fixes part of #45003. Also related to #42336, #56131, #57139.

Note: this PR addresses the symptom (empty chat.history after reset). Two follow-up proposals are outlined in #45003 — fixing the daily-reset trigger logic (PR 2) and exposing archives in the UI sidebar (PR 3).

Test plan

  • pnpm test -- src/gateway/session-utils.fs.test.ts — all 55 tests pass
  • No regressions in existing readSessionMessages tests
  • New tests cover all four cases: archive fallback, primary preference, latest-archive selection, empty result

Note: pnpm tsgo has a pre-existing regression in src/channels/plugins/contracts/registry.ts (missing OpenClawConfig import, introduced in upstream commit 4b93000d11) unrelated to this PR.

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Apr 3, 2026
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a fallback in readSessionMessages to serve the most recent .reset.<timestamp> archive when no primary transcript exists, fixing empty chat.history responses after a daily or manual session reset. The approach is well-scoped, consistent with existing path resolution patterns, and covered by four targeted tests.

The only gap worth noting: the fallback only searches path.dirname(storePath) for archives, while the primary transcript resolution also considers the legacy ~/.openclaw/sessions directory. Sessions that were reset while their transcript lived in the legacy dir won't benefit from the fallback.

Confidence Score: 5/5

Safe to merge; the fix is well-scoped, well-tested, and does not regress any existing path.

Both findings are P2: one is a narrow edge case limited to legacy-dir sessions, and the other is a minor defensive-coding suggestion. The primary scenario (current sessions dir) is fixed correctly and fully tested.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.fs.ts
Line: 104-106

Comment:
**Legacy sessions dir not searched for reset archives**

`findLatestResetArchive` only scans `path.dirname(storePath)`, but `resolveSessionTranscriptCandidates` (the primary resolution path) also always appends the legacy directory (`~/.openclaw/sessions`) as a candidate — regardless of `storePath`. If a session's primary transcript lived in the legacy dir and was reset, the archive will be there, but this fallback won't find it.

For most current installations this won't matter, but users who migrated from an older layout and whose last session was reset before migration could still see an empty `chat.history` for that session.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/gateway/session-transcript-files.fs.ts
Line: 278-279

Comment:
**`readdirSync` may include directories**

`fs.readdirSync` without `withFileTypes: true` returns all entries — including subdirectories — whose names happen to start with the prefix. In practice no directory would match the `uuid.jsonl.reset.timestamp` pattern, but passing `{ withFileTypes: true }` and filtering on `dirent.isFile()` makes the intent explicit and guards against edge-case filesystem states.

```suggestion
    const files = fs.readdirSync(sessionsDir, { withFileTypes: true });
    const archives = files
      .filter((d) => d.isFile() && d.name.startsWith(prefix))
      .map((d) => d.name)
      .toSorted(); // archive timestamps are ISO-derived and sort lexicographically
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(session): fall back to reset archive..." | Re-trigger Greptile

Comment thread src/gateway/session-utils.fs.ts Outdated
Comment on lines +104 to +106
if (!filePath && storePath && sessionId) {
filePath = findLatestResetArchive(sessionId, path.dirname(storePath));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Legacy sessions dir not searched for reset archives

findLatestResetArchive only scans path.dirname(storePath), but resolveSessionTranscriptCandidates (the primary resolution path) also always appends the legacy directory (~/.openclaw/sessions) as a candidate — regardless of storePath. If a session's primary transcript lived in the legacy dir and was reset, the archive will be there, but this fallback won't find it.

For most current installations this won't matter, but users who migrated from an older layout and whose last session was reset before migration could still see an empty chat.history for that session.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.fs.ts
Line: 104-106

Comment:
**Legacy sessions dir not searched for reset archives**

`findLatestResetArchive` only scans `path.dirname(storePath)`, but `resolveSessionTranscriptCandidates` (the primary resolution path) also always appends the legacy directory (`~/.openclaw/sessions`) as a candidate — regardless of `storePath`. If a session's primary transcript lived in the legacy dir and was reset, the archive will be there, but this fallback won't find it.

For most current installations this won't matter, but users who migrated from an older layout and whose last session was reset before migration could still see an empty `chat.history` for that session.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +278 to +279
const files = fs.readdirSync(sessionsDir);
const archives = files.filter((name) => name.startsWith(prefix)).toSorted(); // archive timestamps are ISO-derived and sort lexicographically

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 readdirSync may include directories

fs.readdirSync without withFileTypes: true returns all entries — including subdirectories — whose names happen to start with the prefix. In practice no directory would match the uuid.jsonl.reset.timestamp pattern, but passing { withFileTypes: true } and filtering on dirent.isFile() makes the intent explicit and guards against edge-case filesystem states.

Suggested change
const files = fs.readdirSync(sessionsDir);
const archives = files.filter((name) => name.startsWith(prefix)).toSorted(); // archive timestamps are ISO-derived and sort lexicographically
const files = fs.readdirSync(sessionsDir, { withFileTypes: true });
const archives = files
.filter((d) => d.isFile() && d.name.startsWith(prefix))
.map((d) => d.name)
.toSorted(); // archive timestamps are ISO-derived and sort lexicographically
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/session-transcript-files.fs.ts
Line: 278-279

Comment:
**`readdirSync` may include directories**

`fs.readdirSync` without `withFileTypes: true` returns all entries — including subdirectories — whose names happen to start with the prefix. In practice no directory would match the `uuid.jsonl.reset.timestamp` pattern, but passing `{ withFileTypes: true }` and filtering on `dirent.isFile()` makes the intent explicit and guards against edge-case filesystem states.

```suggestion
    const files = fs.readdirSync(sessionsDir, { withFileTypes: true });
    const archives = files
      .filter((d) => d.isFile() && d.name.startsWith(prefix))
      .map((d) => d.name)
      .toSorted(); // archive timestamps are ISO-derived and sort lexicographically
```

How can I resolve this? If you propose a fix, please make it concise.

@CadanHu

CadanHu commented Apr 3, 2026

Copy link
Copy Markdown
Author

Note on CI failures: The failing checks (check, build-artifacts, checks-fast-contracts-protocol) are pre-existing regressions in upstream main unrelated to this PR.

The root cause is a TypeScript error in extensions/bluebubbles/src/actions.ts(80,50)Property 'actions' does not exist on type 'BlueBubblesAccountConfig' — which was already failing on the base commit 4b93000d11 before this PR was opened.

The three files changed in this PR (src/gateway/session-transcript-files.fs.ts, src/gateway/session-utils.fs.ts, src/gateway/session-utils.fs.test.ts) have no TypeScript errors when checked in isolation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ee9e8800a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/session-utils.fs.ts Outdated
Comment on lines +104 to +105
if (!filePath && storePath && sessionId) {
filePath = findLatestResetArchive(sessionId, path.dirname(storePath));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Search reset archives in resolved transcript directories

This fallback only checks path.dirname(storePath), but readSessionMessages explicitly supports sessionFile paths that can resolve outside that store (for example legacy cross-agent absolute transcript paths). In that case, once the primary transcript is reset and archived in the sessionFile directory, this code still returns [], so chat.history/sessions_history remains empty for those sessions. The archive lookup should follow the resolved transcript candidate location(s), not just the current store directory.

Useful? React with 👍 / 👎.

Comment on lines +279 to +280
const archives = files.filter((name) => name.startsWith(prefix)).toSorted(); // archive timestamps are ISO-derived and sort lexicographically
return archives.length > 0 ? path.join(sessionsDir, archives[archives.length - 1]) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict reset fallback to valid regular archive files

findLatestResetArchive selects any directory entry with the reset prefix and returns it without validating archive format or file type. If a matching non-file or malformed artifact exists, readSessionMessages will try to readFileSync it and can throw (or load the wrong content), turning history reads into errors or incorrect data. Filter candidates to valid timestamped reset archives and regular files before choosing the latest one.

Useful? React with 👍 / 👎.

@CadanHu

CadanHu commented Apr 3, 2026

Copy link
Copy Markdown
Author

Both review findings addressed in the follow-up commit (5e22c50):

  1. readdirSync filtering — switched to { withFileTypes: true } with d.isFile() filter as suggested.
  2. Legacy sessions dirfindLatestResetArchive now also searches ~/.openclaw/sessions, consistent with resolveSessionTranscriptCandidates. Added a test that remaps HOME to verify archives in the legacy layout are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e22c50d26

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +302 to +303
if (archives.length > 0) {
return path.join(dir, archives[archives.length - 1]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Select latest reset archive across all searched dirs

findLatestResetArchive returns immediately after finding any matching archive in the first directory (sessionsDir), so if both sessionsDir and legacy ~/.openclaw/sessions contain reset archives, it can return an older file even when a newer archive exists in the later directory. That breaks the function’s “most recent” behavior and can surface stale session history after store-path migrations or mixed legacy/current layouts; gather matches from all searched directories and then choose the newest archive globally.

Useful? React with 👍 / 👎.

@CadanHu

CadanHu commented Apr 3, 2026

Copy link
Copy Markdown
Author

Addressed the latest Codex P2 in commit f65c703:

Global-newest archive selectionfindLatestResetArchive previously returned as soon as any archive was found in the first directory. Now it scans all search dirs before deciding, comparing parseSessionArchiveTimestamp values to pick the globally newest archive. This prevents surfacing a stale archive from the primary dir when a more recent one exists in the legacy ~/.openclaw/sessions dir (e.g. after a store-path migration).

@CadanHu CadanHu force-pushed the fix/session-history-archive-fallback branch from f65c703 to 0e5a85a Compare April 4, 2026 02:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e5a85adc7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

sessionId: string,
searchDirs: readonly string[],
): string | undefined {
const prefix = `${sessionId}.jsonl.reset.`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match reset archives to candidate transcript filenames

The fallback currently hardcodes the archive prefix as <sessionId>.jsonl.reset.*. That misses reset archives created from non-canonical transcript names, because archiving appends .reset.<timestamp> to the original transcript path (for example custom/fork/topic sessionFile names that resolveSessionTranscriptCandidates already supports). In those cases, when the primary transcript is missing, readSessionMessages still returns an empty history even though a reset archive exists. Use the resolved candidate basenames (or otherwise match valid reset archives for those candidates) instead of assuming only <sessionId>.jsonl.

Useful? React with 👍 / 👎.

@CadanHu CadanHu force-pushed the fix/session-history-archive-fallback branch 2 times, most recently from 5ed0977 to ac9b85a Compare April 4, 2026 05:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac9b85adb7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/session-utils.fs.ts Outdated
Comment on lines +108 to +109
const candidateBasenames = candidates.map((c) => path.basename(c));
filePath = findLatestResetArchive(sessionId, searchDirs, candidateBasenames);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude stale basenames from reset-archive fallback

resolveSessionTranscriptCandidates can intentionally include a stale sessionFile path from a different session ID for compatibility, but this fallback forwards every candidate basename into findLatestResetArchive. When the current primary transcript is missing, a newer .reset.* file for that stale basename can be selected, causing readSessionMessages to return another session’s archived messages for the requested sessionId. Restrict archive-prefix matching to basenames that are guaranteed to belong to the requested session before calling the fallback lookup.

Useful? React with 👍 / 👎.

@CadanHu CadanHu force-pushed the fix/session-history-archive-fallback branch from ac9b85a to ff5ec29 Compare April 4, 2026 08:30
cholaolu-boop pushed a commit to cholaolu-boop/openclaw that referenced this pull request Apr 20, 2026
…ise sort ties

Addresses third codex review pass on openclaw#68765 (both P1 and P2 on commit
b5842d3).

P1 (reset-semantics correctness): drop .jsonl.reset.* siblings from the
compaction-chain replay. resolveMainSessionTranscriptFiles now reads
only ${sessionId}.jsonl per checkpoint session id. Upstream
session-reset-service.ts preserves compactionCheckpoints[] across a
/reset, so including per-session .reset.* archives would resurrect
content the user explicitly cleared via reset and violate reset
semantics. Reset-archive fallback for the non-chained path remains out
of scope for this PR and is handled by openclaw#60409.

P2 (determinism): when both compared file mtimes are equal, fall
through to a filename localeCompare tie-break instead of returning 0
and relying on readdir order. Filesystems with coarse timestamp
resolution (FAT32, some network mounts) or rapid writes can produce
identical mtimes; without a secondary key, the replay order and
assigned seq values are non-deterministic across runs and platforms.

Behavior: for sessions whose chained transcripts have distinct mtimes,
unchanged. For sessions with coincident mtimes, deterministic and
stable. Reset-after-compaction scenarios no longer resurrect cleared
content. No change to the non-chained fallback path.

Also removes the now-redundant listExistingTranscriptVariantsForSessionId
helper (its single call site used only the primary-jsonl lookup after
this change); inlined into resolveMainSessionTranscriptFiles for
clarity.
@injinj

injinj commented Apr 26, 2026

Copy link
Copy Markdown

Cross-reference: complementary fix #71537 covers two surfaces this PR doesn't touch — the session-memory hook (which auto-summarizes the previous transcript at /new / /reset and writes it to the daily memory file via direct fs.readFile, not readSessionMessages) and the session-logs skill examples (the bash globs explicitly skipped .reset.*Z archives).

The two PRs touch entirely disjoint files and merge cleanly:

  • 6 files changed, +390 / −7
  • No file or hunk overlap
  • pnpm tsgo: exit 0
  • vitest on affected files: 60/60 (gateway-core) + 19/19 (hooks) all green

Combined branch for reference: https://github.com/injinj/openclaw/tree/combined/reset-archive-coverage

@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Workflow note: Future ClawSweeper reviews update this same comment in place.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Summary
This PR adds reset-archive fallback lookup to synchronous session message/title readers and focused regression tests.

Reproducibility: yes. at source level: current main creates .jsonl.reset.<timestamp> archives, while chat.history and sessions-history HTTP call async readers that return empty when no active transcript exists. I did not run a live gateway repro in this read-only review.

PR rating
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🦪 silver shellfish
Summary: The PR is not quality-ready because real behavior proof is absent and the patch misses the async production history readers named in the PR.

Rank-up moves:

  • Route or defer the fallback through the async readers used by chat.history and sessions-history HTTP, or explicitly scope this PR to sync/title-only behavior.
  • Post redacted real behavior proof from a gateway chat.history or sessions-history HTTP call against an archive-only reset transcript.
  • Rebase against current main after the implementation direction is chosen.
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Real behavior proof
Needs real behavior proof before merge: The PR body and comments provide unit-test/CI claims but no redacted terminal output, logs, recording, screenshot, or artifact showing after-fix gateway archive-history behavior; the contributor should add proof, redact private details, and update the PR body for re-review.

Risk before merge

  • Merging as-is can still leave chat.history and sessions-history HTTP empty for archive-only reset sessions because those production paths call async readers that this branch does not change.
  • The automatic default fallback may re-surface reset-archived transcript content without the explicit archive-history policy still under discussion in related PRs.
  • The external PR lacks after-fix real behavior proof and is currently conflicting against main.

Maintainer options:

  1. Retarget To Production Readers (recommended)
    Route the missing-active fallback through the async message, count, title, and HTTP readers that chat.history and sessions-history actually use, then add real gateway proof.
  2. Use Explicit Archive Opt-In
    If reset archives should be visible only on request, keep default history behavior unchanged and continue the includeArchived direction instead of merging an automatic fallback.
  3. Defer To The Async Replacement
    If fix(sessions): fall back to latest reset archive for missing async transcripts #76134 remains the canonical fix, pause or close this branch after preserving any useful sync/title coverage there.

Next step before merge
Needs contributor real behavior proof, conflict resolution, and maintainer direction on automatic reset-archive fallback versus explicit archived-history semantics before automation should touch it.

Security
Cleared: The diff adds local transcript archive lookup and tests without dependency, workflow, secret, network, or code-execution changes.

Review findings

  • [P2] Route reset fallback through async history readers — src/gateway/session-utils.fs.ts:101-115
Review details

Best possible solution:

Converge on one archive-read contract: either implement the missing-active fallback in the async production readers or intentionally use an explicit archived-history API with approved reset-archive semantics.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level: current main creates .jsonl.reset.<timestamp> archives, while chat.history and sessions-history HTTP call async readers that return empty when no active transcript exists. I did not run a live gateway repro in this read-only review.

Is this the best way to solve the issue?

No, not as written. The sync fallback is useful, but the maintainable fix must cover the async production readers or deliberately defer to an explicit archived-history API.

Label justifications:

  • P2: The PR targets a real gateway session-history recovery gap with limited but user-visible session-state impact.
  • merge-risk: 🚨 session-state: The PR changes reset transcript archive visibility and can still misrepresent session history because the production async readers are not covered.
  • rating: 🧂 unranked krab: Current PR rating is 🧂 unranked krab because proof is 🧂 unranked krab, patch quality is 🦪 silver shellfish, and The PR is not quality-ready because real behavior proof is absent and the patch misses the async production history readers named in the PR.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body and comments provide unit-test/CI claims but no redacted terminal output, logs, recording, screenshot, or artifact showing after-fix gateway archive-history behavior; the contributor should add proof, redact private details, and update the PR body for re-review.

Full review comments:

  • [P2] Route reset fallback through async history readers — src/gateway/session-utils.fs.ts:101-115
    The PR adds the archive fallback to synchronous readSessionMessages, but chat.history and sessions-history HTTP use readRecentSessionMessagesAsync, readRecentSessionMessagesWithStatsAsync, and readSessionMessagesAsync. With only a .reset.* archive and no active transcript, those production paths still return empty results, so the advertised gateway behavior remains unfixed.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.89

What I checked:

  • PR diff targets sync helpers: The latest PR diff adds the fallback around synchronous readSessionMessages and readSessionTitleFieldsFromTranscript; it does not change the async readers used by the advertised gateway paths. (src/gateway/session-utils.fs.ts:101, bd85546dff54)
  • chat.history production path is async: Current main handles chat.history by calling readRecentSessionMessagesAsync, so a sync-only fallback is not exercised by this gateway method. (src/gateway/server-methods/chat.ts:1831, 7f943b5d8f83)
  • sessions-history HTTP path is async: The HTTP sessions-history path uses readRecentSessionMessagesWithStatsAsync for bounded reads and readSessionMessagesAsync for full/cursor reads, both outside this PR's sync fallback. (src/gateway/sessions-history-http.ts:159, 7f943b5d8f83)
  • async reader returns empty without active transcript: readRecentSessionMessagesAsync resolves through findExistingTranscriptPath and returns [] when no active transcript path exists. (src/gateway/session-utils.fs.ts:645, 7f943b5d8f83)
  • session list title path is async: Current session listing/title derivation calls readSessionTitleFieldsFromTranscriptAsync, so the PR's sync title fallback does not cover this production list path either. (src/gateway/session-utils.ts:2323, 7f943b5d8f83)
  • reset archive files are current-main state: Current main archives transcript files by renaming to ${filePath}.${reason}.${ts}, producing the .jsonl.reset.<timestamp> files the PR tries to recover. (src/gateway/session-transcript-files.fs.ts:127, 7f943b5d8f83)

Likely related people:

  • steipete: Recent merged work bounded async transcript history reads and the current checkout blame points the central async reader and chat.history code through the latest session utility extraction. (role: recent area contributor; confidence: high; commits: 4d9c658f4058, ec10d1211212; files: src/gateway/session-utils.fs.ts, src/gateway/server-methods/chat.ts, src/gateway/sessions-history-http.ts)
  • mcaxtr: Commit 31537c6 introduced archiving old transcript files on /new and /reset, creating the .jsonl.reset.<timestamp> files involved here. (role: introduced archive behavior; confidence: high; commits: 31537c669a01; files: src/gateway/session-utils.fs.ts, src/gateway/session-transcript-files.fs.ts)
  • vincentkoc: Commit aec83af changed bounded chat history transcript reads in the same gateway/session-history surface. (role: recent session-history contributor; confidence: medium; commits: aec83af23d49; files: src/gateway/session-utils.fs.ts, src/gateway/server-methods/chat.ts)
  • clay-datacurve: Commit 7b61ca1 added the direct session history HTTP surface that consumes the async transcript readers. (role: session-history HTTP contributor; confidence: medium; commits: 7b61ca1b0615; files: src/gateway/sessions-history-http.ts, src/gateway/session-utils.fs.ts, src/gateway/session-utils.ts)
  • masatohoshino: Open related PR fix(sessions): fall back to latest reset archive for missing async transcripts #76134 targets the production async readers, counts, titles, and HTTP pagination path this branch currently misses. (role: adjacent replacement contributor; confidence: medium; commits: abc9c164f481; files: src/gateway/session-utils.fs.ts, src/gateway/session-transcript-files.fs.ts, src/gateway/sessions-history-http.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 7f943b5d8f83.

忽一涛 and others added 6 commits April 28, 2026 08:38
… transcript is missing

When a session is reset (daily or manual /new//reset), the primary transcript
is archived as <sessionId>.jsonl.reset.<timestamp>. Previously, chat.history
and sessions_history returned an empty response for that session key because
only the primary file path was checked.

This change makes readSessionMessages fall back to the most recent reset archive
when no primary transcript exists, so context is not silently lost after a reset.

Related: openclaw#42336, openclaw#45003, openclaw#56131, openclaw#57139
- Use readdirSync with withFileTypes to filter files only, guarding
  against edge-case dir entries matching the archive prefix
- Also search the legacy ~/.openclaw/sessions dir, consistent with
  resolveSessionTranscriptCandidates, so archives created before a
  state-dir migration are found by the fallback
- Add test covering the legacy-dir fallback path
…with parseSessionArchiveTimestamp

- findLatestResetArchive now accepts readonly string[] searchDirs instead of
  a single sessionsDir, removing the internal legacy-dir logic
- caller in readSessionMessages derives unique dirs from resolveSessionTranscriptCandidates
  output so archive lookup stays consistent with primary transcript resolution
- uses parseSessionArchiveTimestamp to validate archive filename format before
  including files in the result set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously findLatestResetArchive returned on the first directory that had
any matching archive. When both the primary dir and legacy ~/.openclaw/sessions
contain archives (e.g. after a store-path migration), this could surface an
older file instead of the globally newest one. Scan all search dirs first
and pick the archive with the highest parseSessionArchiveTimestamp value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
findLatestResetArchive previously hardcoded the archive prefix as
<sessionId>.jsonl.reset.*, missing archives whose transcript filename
is non-canonical (e.g. topic-scoped <sessionId>-topic-<id>.jsonl).

- Accept optional candidateBasenames in findLatestResetArchive and
  build one prefix per candidate basename; fall back to the canonical
  <sessionId>.jsonl prefix when not provided.
- Pass candidateBasenames from readSessionMessages so all resolved
  candidate transcript filenames are covered.
- Add test: topic-scoped sessionFile archive found when primary missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CadanHu

CadanHu commented Apr 28, 2026

Copy link
Copy Markdown
Author

Status update after 24 days:

Ready for review whenever convenient.

masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 2, 2026
…reads when active is missing

When the active session transcript file is missing, async read paths used by
chat.history (`readSessionMessagesAsync`, `readRecentSessionMessagesAsync`)
and the session title/preview lookup
(`readSessionTitleFieldsFromTranscriptAsync`) returned an empty result instead
of any archived content. Reset rotates the active `<id>.jsonl` to a sibling
`<id>.jsonl.reset.<UTC timestamp>` and creates a new empty active file, so
sessions reset before the next read became invisible through the production
async API even when the archive still existed on disk.

This change adds a narrow auto-fallback on the async paths only:

- New `findLatestResetArchiveAsync(candidatePaths)` helper in
  `session-transcript-files.fs.ts` does an async `readdir` of each candidate
  parent directory, validates archive entries via the existing
  `parseSessionArchiveTimestamp(entry, "reset")` primitive, and returns only
  the path with the newest archive timestamp.
- A small internal helper `findActiveOrLatestResetArchiveAsync` in
  `session-utils.fs.ts` reuses the existing active-candidate `existsSync`
  check first; only when no active candidate exists does it consult the new
  helper.
- The three async functions above are re-routed through this helper.

Behavior boundaries (kept narrow on purpose):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation, no
  `previousFragments`, no rolled logical-history reconstruction).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory hook,
  and the session-logs skill are untouched.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 2, 2026
…hive fallback as readers

Codex/clawsweeper review on PR openclaw#76134 found that
`readRecentSessionMessagesWithStatsAsync` still derived
`totalMessages` from `readSessionMessageCountAsync`, which kept the
active-only `findExistingTranscriptPath` resolution. After the new
async reader fallback, an archive-only transcript longer than the
bounded tail window therefore returned the newest messages with low
`seq` values and `hasMore = false`. The sessions-history HTTP first
page passes `boundedSnapshot.totalMessages` into
`buildSessionHistorySnapshot`, so a `nextCursor` follow-up could skip
the middle archive page.

This commit routes the async count and visit helpers
(`readSessionMessageCountAsync` and `visitSessionMessagesAsync`)
through the same `findActiveOrLatestResetArchiveAsync` resolution that
the message readers use, so an archive-only transcript reports a
consistent total, gives messages the right tail-end `seq` values, and
exposes the archive's earlier pages via `nextCursor`.

Behavior boundaries (kept narrow, same as the original PR):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory
  hook, and the session-logs skill are still untouched.

Tests: 4 new cases in `session-utils.fs.test.ts` cover the count and
stats helpers (active-only, archive-only with archive longer than the
tail window, both-exist active-priority, both-missing) and assert the
returned `seq` metadata matches the archive's tail position. One new
case in `sessions-history-http.test.ts` exercises the archive-only
HTTP pagination path: archive a 3-message transcript, request the
first `limit=2` page, follow `nextCursor`, and assert the older
archive message is visible without skipping middle pages.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 17, 2026
…reads when active is missing

When the active session transcript file is missing, async read paths used by
chat.history (`readSessionMessagesAsync`, `readRecentSessionMessagesAsync`)
and the session title/preview lookup
(`readSessionTitleFieldsFromTranscriptAsync`) returned an empty result instead
of any archived content. Reset rotates the active `<id>.jsonl` to a sibling
`<id>.jsonl.reset.<UTC timestamp>` and creates a new empty active file, so
sessions reset before the next read became invisible through the production
async API even when the archive still existed on disk.

This change adds a narrow auto-fallback on the async paths only:

- New `findLatestResetArchiveAsync(candidatePaths)` helper in
  `session-transcript-files.fs.ts` does an async `readdir` of each candidate
  parent directory, validates archive entries via the existing
  `parseSessionArchiveTimestamp(entry, "reset")` primitive, and returns only
  the path with the newest archive timestamp.
- A small internal helper `findActiveOrLatestResetArchiveAsync` in
  `session-utils.fs.ts` reuses the existing active-candidate `existsSync`
  check first; only when no active candidate exists does it consult the new
  helper.
- The three async functions above are re-routed through this helper.

Behavior boundaries (kept narrow on purpose):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation, no
  `previousFragments`, no rolled logical-history reconstruction).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory hook,
  and the session-logs skill are untouched.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 17, 2026
…hive fallback as readers

Codex/clawsweeper review on PR openclaw#76134 found that
`readRecentSessionMessagesWithStatsAsync` still derived
`totalMessages` from `readSessionMessageCountAsync`, which kept the
active-only `findExistingTranscriptPath` resolution. After the new
async reader fallback, an archive-only transcript longer than the
bounded tail window therefore returned the newest messages with low
`seq` values and `hasMore = false`. The sessions-history HTTP first
page passes `boundedSnapshot.totalMessages` into
`buildSessionHistorySnapshot`, so a `nextCursor` follow-up could skip
the middle archive page.

This commit routes the async count and visit helpers
(`readSessionMessageCountAsync` and `visitSessionMessagesAsync`)
through the same `findActiveOrLatestResetArchiveAsync` resolution that
the message readers use, so an archive-only transcript reports a
consistent total, gives messages the right tail-end `seq` values, and
exposes the archive's earlier pages via `nextCursor`.

Behavior boundaries (kept narrow, same as the original PR):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory
  hook, and the session-logs skill are still untouched.

Tests: 4 new cases in `session-utils.fs.test.ts` cover the count and
stats helpers (active-only, archive-only with archive longer than the
tail window, both-exist active-priority, both-missing) and assert the
returned `seq` metadata matches the archive's tail position. One new
case in `sessions-history-http.test.ts` exercises the archive-only
HTTP pagination path: archive a 3-message transcript, request the
first `limit=2` page, follow `nextCursor`, and assert the older
archive message is visible without skipping middle pages.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 17, 2026
…reads when active is missing

When the active session transcript file is missing, async read paths used by
chat.history (`readSessionMessagesAsync`, `readRecentSessionMessagesAsync`)
and the session title/preview lookup
(`readSessionTitleFieldsFromTranscriptAsync`) returned an empty result instead
of any archived content. Reset rotates the active `<id>.jsonl` to a sibling
`<id>.jsonl.reset.<UTC timestamp>` and creates a new empty active file, so
sessions reset before the next read became invisible through the production
async API even when the archive still existed on disk.

This change adds a narrow auto-fallback on the async paths only:

- New `findLatestResetArchiveAsync(candidatePaths)` helper in
  `session-transcript-files.fs.ts` does an async `readdir` of each candidate
  parent directory, validates archive entries via the existing
  `parseSessionArchiveTimestamp(entry, "reset")` primitive, and returns only
  the path with the newest archive timestamp.
- A small internal helper `findActiveOrLatestResetArchiveAsync` in
  `session-utils.fs.ts` reuses the existing active-candidate `existsSync`
  check first; only when no active candidate exists does it consult the new
  helper.
- The three async functions above are re-routed through this helper.

Behavior boundaries (kept narrow on purpose):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation, no
  `previousFragments`, no rolled logical-history reconstruction).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory hook,
  and the session-logs skill are untouched.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request May 17, 2026
…hive fallback as readers

Codex/clawsweeper review on PR openclaw#76134 found that
`readRecentSessionMessagesWithStatsAsync` still derived
`totalMessages` from `readSessionMessageCountAsync`, which kept the
active-only `findExistingTranscriptPath` resolution. After the new
async reader fallback, an archive-only transcript longer than the
bounded tail window therefore returned the newest messages with low
`seq` values and `hasMore = false`. The sessions-history HTTP first
page passes `boundedSnapshot.totalMessages` into
`buildSessionHistorySnapshot`, so a `nextCursor` follow-up could skip
the middle archive page.

This commit routes the async count and visit helpers
(`readSessionMessageCountAsync` and `visitSessionMessagesAsync`)
through the same `findActiveOrLatestResetArchiveAsync` resolution that
the message readers use, so an archive-only transcript reports a
consistent total, gives messages the right tail-end `seq` values, and
exposes the archive's earlier pages via `nextCursor`.

Behavior boundaries (kept narrow, same as the original PR):

- Active wins when present (no active+archive merging).
- Only the single newest archive is returned (no chain aggregation).
- Sync paths, `findExistingTranscriptPath`, the protocol schema, the
  `includeArchived` opt-in, sessions_history schema, the session-memory
  hook, and the session-logs skill are still untouched.

Tests: 4 new cases in `session-utils.fs.test.ts` cover the count and
stats helpers (active-only, archive-only with archive longer than the
tail window, both-exist active-priority, both-missing) and assert the
returned `seq` metadata matches the archive's tail position. One new
case in `sessions-history-http.test.ts` exercises the archive-only
HTTP pagination path: archive a 3-message transcript, request the
first `limit=2` page, follow `nextCursor`, and assert the older
archive message is visible without skipping middle pages.

Refs openclaw#56131 openclaw#43929 openclaw#45003 openclaw#60409 openclaw#73883
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper clawsweeper Bot added P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 5, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #clawtributors on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants