fix(session): fall back to reset archive in chat.history when primary transcript is missing#60409
fix(session): fall back to reset archive in chat.history when primary transcript is missing#60409CadanHu wants to merge 10 commits into
Conversation
Greptile SummaryThis PR adds a fallback in The only gap worth noting: the fallback only searches Confidence Score: 5/5Safe 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 AIThis 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 |
| if (!filePath && storePath && sessionId) { | ||
| filePath = findLatestResetArchive(sessionId, path.dirname(storePath)); | ||
| } |
There was a problem hiding this 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.
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.| const files = fs.readdirSync(sessionsDir); | ||
| const archives = files.filter((name) => name.startsWith(prefix)).toSorted(); // archive timestamps are ISO-derived and sort lexicographically |
There was a problem hiding this 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.
| 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.|
Note on CI failures: The failing checks ( The root cause is a TypeScript error in The three files changed in this PR ( |
There was a problem hiding this comment.
💡 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".
| if (!filePath && storePath && sessionId) { | ||
| filePath = findLatestResetArchive(sessionId, path.dirname(storePath)); |
There was a problem hiding this comment.
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 👍 / 👎.
| 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; |
There was a problem hiding this comment.
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 👍 / 👎.
|
Both review findings addressed in the follow-up commit (5e22c50):
|
There was a problem hiding this comment.
💡 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".
| if (archives.length > 0) { | ||
| return path.join(dir, archives[archives.length - 1]); |
There was a problem hiding this comment.
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 👍 / 👎.
|
Addressed the latest Codex P2 in commit f65c703: Global-newest archive selection — |
f65c703 to
0e5a85a
Compare
There was a problem hiding this comment.
💡 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.`; |
There was a problem hiding this comment.
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 👍 / 👎.
5ed0977 to
ac9b85a
Compare
There was a problem hiding this comment.
💡 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".
| const candidateBasenames = candidates.map((c) => path.basename(c)); | ||
| filePath = findLatestResetArchive(sessionId, searchDirs, candidateBasenames); |
There was a problem hiding this comment.
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 👍 / 👎.
ac9b85a to
ff5ec29
Compare
…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.
|
Cross-reference: complementary fix #71537 covers two surfaces this PR doesn't touch — the The two PRs touch entirely disjoint files and merge cleanly:
Combined branch for reference: https://github.com/injinj/openclaw/tree/combined/reset-archive-coverage |
f6c7471 to
b49637a
Compare
|
Codex review: needs real behavior proof before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. at source level: current main creates PR rating Rank-up moves:
What the crustacean ranks mean
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 Risk before merge
Maintainer options:
Next step before merge Security Review findings
Review detailsBest 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 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:
Full review comments:
Overall correctness: patch is incorrect What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 7f943b5d8f83. |
… 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>
|
Status update after 24 days:
Ready for review whenever convenient. |
…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
…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
…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
…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
…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
…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 PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
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.historyandsessions_historyreturned an empty response for that session key becausereadSessionMessagesonly checked the primary file path.This PR makes
readSessionMessagesfall 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: addfindLatestResetArchive(sessionId, sessionsDir)— scans for and returns the path of the most recent reset archive for a sessionsrc/gateway/session-utils.fs.ts: callfindLatestResetArchiveas a fallback inreadSessionMessageswhen no primary transcript is foundsrc/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
.reset.<timestamp>archive is served instead of an empty result[]Related issues
Fixes part of #45003. Also related to #42336, #56131, #57139.
Note: this PR addresses the symptom (empty
chat.historyafter 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 passreadSessionMessagestestsNote:
pnpm tsgohas a pre-existing regression insrc/channels/plugins/contracts/registry.ts(missingOpenClawConfigimport, introduced in upstream commit4b93000d11) unrelated to this PR.🤖 Generated with Claude Code