Skip to content

fix(sessions-history): make archive fallback actually read .jsonl.reset.<ts> transcripts#85

Merged
ryan-dyer-sp merged 3 commits into
brightfire/sessions-history-archivedfrom
claw/vash/sessions-history-archive-fallback
Jun 4, 2026
Merged

fix(sessions-history): make archive fallback actually read .jsonl.reset.<ts> transcripts#85
ryan-dyer-sp merged 3 commits into
brightfire/sessions-history-archivedfrom
claw/vash/sessions-history-archive-fallback

Conversation

@ryan-dyer-sp

Copy link
Copy Markdown
Member

Summary

sessions_history against an archived session (rollover/reset/delete twin) returned an empty transcript even though the .jsonl.reset.<ISO-timestamp> file was on disk and valid. This commit fixes the async read path so archive twins return their actual content, and tidies up a parameter that was advertised but never read.

Motivation

Enable the "resume yesterday's conversation" workflow. When an OpenClaw session rolls over overnight (lazy reset on first message after the daily boundary), the predecessor session is archived under a twin key. Agents and humans should be able to call sessions_history sessionKey=<predecessor-sessionId> to load that prior transcript and pick up where they left off. Today that call returns nothing.

This sits on top of #82 (archive-write fix) and #84 (sessions_list key filter for lineage discovery). Together those three PRs land the full "rollover + discover + resume" pipeline.

Repro (vash, today)

agent:main:main rolled over at 2026-06-04 07:18:15 CDT. Predecessor sessionId 8f5bd13c-418f-4448-9cc8-fd29bcb18620. Transcript file 8f5bd13c-...jsonl.reset.2026-06-04T12-18-15.908Z exists on disk (873KB, 371 lines, valid JSONL).

> sessions_history sessionKey=agent:main:main:archived:8f5bd13c-418f-4448-9cc8-fd29bcb18620
{ messages: [], archived: true, bytes: 2 }

The archived: true flag is set correctly — the resolver found the right store entry — but the transcript content was never loaded.

Verified live on aster too (running an earlier bf-version of the patch): the sync readSessionMessages returns 89 messages, but the async readRecentSessionMessagesAsync returns 0 against the same archive file. Same bug shape.

Root cause

findExistingTranscriptPath in src/gateway/session-utils.fs.ts only checked live .jsonl candidates. All four async read paths (readRecentSessionMessagesAsync, readSessionMessagesAsync, readSessionMessageCountAsync, visitSessionMessagesAsync) route through it. When the resolved key is an archive twin, no live candidate exists and the path returns null, so reads silently return empty. Only the sync readSessionMessages had an inline archive fallback.

chat.history (which sessions_history uses) goes through the async path.

Changes

Commit 1 — fix(sessions-history): archive fallback in findExistingTranscriptPath

  • src/gateway/session-utils.fs.tsfindExistingTranscriptPath now falls back to resolveArchivedTranscriptPaths() when no live candidate exists. Live .jsonl is still preferred when present (no behaviour change for live reads).
  • src/gateway/session-transcript-files.fs.archived-history.test.ts — 4 new tests (prefers-live, sessionId-disambiguation across multiple archives in same dir, picks-most-recent-reset when multiple resets exist for one sessionId, returns-empty when nothing on disk) plus 1 case flipped from "documents the bug" to a positive assertion. 18 tests total (was 14).

Commit 2 — refactor(sessions-history): remove unused includeArchived parameter

  • src/agents/tools/sessions-history-tool.ts — removed the includeArchived schema field. Was advertised but never read by execute.
  • src/agents/tool-description-presets.ts — rewrote the sessions_history tool description to document the now-functional shape: sessionKey accepts canonical keys / :archived:<sessionId> keys / bare sessionIds; archive fallback is automatic; response carries archived: true when reading from an archive; canonical "resume yesterday" recipe via sessions_listsessions_history.
  • src/agents/tools/sessions-history-tool.test.ts — schema test asserts the removed field is gone; existing "archived flag" case invokes without the removed parameter.

Test plan

pnpm test src/gateway/session-transcript-files.fs.archived-history.test.ts   # 18 ✓
pnpm test src/agents/tools/sessions-history-tool.test.ts                     # 4 ✓
pnpm test src/gateway/sessions-history-http.test.ts                          # 12 ✓
pnpm test src/gateway/session-utils.fs.test.ts                               # 76 ✓
pnpm test src/gateway/session-transcript-files.fs.archive-events.test.ts     # 2 ✓
pnpm exec oxfmt --check                                                       # clean

pnpm check:changed has one pre-existing tsgo:all failure in src/config/sessions/store.pruning.integration.test.ts (missing sessionHistoryRetentionMs on a test fixture). Reproduces on the base branch with my changes stashed — not caused by this work. Flagging for a follow-up; not in scope here.

Out of scope

  • No new tools, new RPC methods, or new schema.
  • No summarization / tail-mode / digest features (those would be the next layer up if we want a token-budget-friendly "resume" UX rather than full-transcript replay).
  • No changes to the archive write side or the canonical/live transcript reading path.
  • No new patch manifest row — this folds into the existing Store-Based Session Archiving entry (Branch HEAD will refresh post-merge via BF: Register Patch).

Refs

Vash added 2 commits June 4, 2026 09:14
The async transcript read path (readRecentSessionMessagesAsync,
readSessionMessagesAsync, readSessionMessageCountAsync,
visitSessionMessagesAsync) all route through findExistingTranscriptPath
in session-utils.fs.ts, which only checked live .jsonl candidates. When
the resolved key was an archive twin (agent:<id>:archived:<sessionId>),
no live candidate existed and the path returned null, so reads
silently returned empty even though .jsonl.reset.<ISO-timestamp>
was on disk. Only the sync readSessionMessages had an inline
archive fallback.

chat.history uses the async read path, so sessions_history against
archived sessions returned an empty transcript. This blocks the
'resume yesterday' workflow: agents woke up post-rollover with no
way to fetch their predecessor's conversation history.

Fix: findExistingTranscriptPath now falls back to
resolveArchivedTranscriptPaths() when no live candidate exists.
Live .jsonl is still preferred when present, so live reads are
unaffected. The fallback handles disambiguation across multiple
archive twins under the same sessionId by picking the
most-recent reset file (matches the canonical-archive-most-recent
behaviour that sessions.resolve already uses).

Tests in session-transcript-files.fs.archived-history.test.ts: 4 new
cases (prefers-live, sessionId-disambiguation across archives in same
dir, picks-most-recent when multiple resets exist for one sessionId,
returns-empty when nothing on disk) plus 1 flip from negative-assert
to positive-assert.

Verified live on aster (running bf4 with the same bug shape): the
sync read returned 89 messages, the async read returned 0 against
the same archive file.

Refs: #82 (archive write side, where this began)
Refs: #84 (sessions_list key filter, predecessor)
sessions_history takes a single sessionKey and returns one
transcript, so the includeArchived flag never had meaning here -
the resolver always lands on either a live or archived store entry
and (with the previous commit) returns the right transcript either
way. The flag was advertised on the schema but never read by execute.

Removed from the tool schema. Tool description rewritten to
document the now-functional shape:
- sessionKey accepts canonical key, archive-twin key, or bare sessionId
- archived sessions return their JSONL transcript automatically
- Response carries archived: true when the resolved entry is an archive
- Canonical 'resume yesterday' recipe: sessions_list key=<canonical>
  includeArchived=true to discover archive twin sessionIds, then
  sessions_history sessionKey=<twin sessionId>

Schema test updated to assert the field is gone; existing 'archived
flag' test now invokes without the removed parameter.

@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: be6c601131

ℹ️ 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
`resolveArchivedTranscriptPaths` only matched archived entries whose name
started with `${sessionId}.jsonl.`, so the topic-qualified shape
`<sessionId>-topic-<N>.jsonl.<reset|deleted>.<ts>` was silently skipped.
After a rollover or delete on a topic-qualified session, the archive
fallback in `findExistingTranscriptPath` therefore returned nothing and
`chat.history` / `sessions_history` came back empty for those sessions.

This change extends the scan to also accept the topic-qualified base
shape — `${sessionId}-topic-<N>.jsonl.<reason>.<ts>` — while still
validating archive artifact suffixes via `isSessionArchiveArtifactName`
and parsing timestamps via `parseSessionArchiveTimestamp`. A small
helper, `matchesSessionArchiveBase`, guards the topic prefix so we do
not accidentally match a different sessionId whose canonical id begins
with `${sessionId}-topic-` as a substring.

Tests:
- New unit cases on `resolveArchivedTranscriptPaths`:
  - finds `<sessionId>-topic-<N>.jsonl.reset.<ts>` for the requested id
  - returns bare + topic-qualified archives sorted timestamp-desc
  - does not match topic archives belonging to a different sessionId
- New end-to-end case via `readRecentSessionMessagesAsync` showing
  that `findExistingTranscriptPath` now resolves the topic-qualified
  archive when only `<sessionId>-topic-<N>.jsonl.reset.<ts>` exists.

Addresses Codex review feedback on PR #85.
@ryan-dyer-sp ryan-dyer-sp merged commit 9c4cf8b into brightfire/sessions-history-archived Jun 4, 2026
78 of 96 checks passed
@github-actions github-actions Bot deleted the claw/vash/sessions-history-archive-fallback branch June 4, 2026 14:55
ryan-dyer-sp added a commit that referenced this pull request Jun 4, 2026
Three pre-existing lint annotations on brightfire/sessions-history-archived
surfaced by pre-push checks after PR #85's CI run:

- src/gateway/sessions-resolve.ts:148 — Array#sort() -> Array#toSorted()
- src/gateway/sessions-resolve.ts:187 — Array#sort() -> Array#toSorted()
- src/config/sessions/archive-entry.test.ts:12 — drop unused loadSessionStore
  import

toSorted is non-mutating (Node 22+ baseline; vash's tier supports it). The
filtered arrays were already fresh from Object.entries().filter(), so the
behaviour is identical; this is purely a hygiene change to clear the patch
branch of lint debt that pre-dates PRs #84/#85.

Refs: #85

Co-authored-by: Vash <vash@brightfire.net>
ryan-dyer-sp added a commit that referenced this pull request Jun 4, 2026
Three pre-existing lint annotations on brightfire/xgw surfaced by
pre-push checks during PR #85's build-stable run:

- src/gateway/xgw/outbound.ts:68
- src/gateway/server-methods/sessions-xgw.ts:47
- src/gateway/server-methods/sessions-xgw.ts:48

All three are non-negative integer arguments, so slice() and
substring() behave identically. Pure hygiene change to clear lint
debt that pre-dates the xgw canonical's latest PR (#72).

Co-authored-by: Vash <vash@brightfire.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant