Skip to content

fix(memory): resolve archived transcript hits during session visibility filtering (follow-up to #76311, AI-assisted)#76452

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
buyitsydney:fix/session-files-archive-path
May 3, 2026
Merged

fix(memory): resolve archived transcript hits during session visibility filtering (follow-up to #76311, AI-assisted)#76452
vincentkoc merged 2 commits into
openclaw:mainfrom
buyitsydney:fix/session-files-archive-path

Conversation

@buyitsydney

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #76311 (socket retry). Closes the other half of the CarHer downstream fix: archived session transcript hits from memory_search are currently being dropped by the visibility guard because the stem extractor doesn't recognise the archive suffix.

  • Problem: memory_search returns no hits on any .jsonl.reset.<iso> or .jsonl.deleted.<iso> archived transcript, even when the user has full visibility on the live session the archive was rotated from.
  • Why it matters: Any session reset silently loses its entire searchable history (session content, user confirmations, decisions, context) — hits land on the archived copy and get filtered out. Discovered downstream while investigating why post-reset memory_search queries returned empty for clearly indexed content.
  • What changed: Teach extractTranscriptStemFromSessionsMemoryHit to recognise the two archive suffixes and map them back to the same live stem the archive was rotated from. The existing visibility guard (filterMemorySearchHitsBySessionVisibilityresolveTranscriptStemToSessionKeys) then correctly gates archived hits against the requester's session store entry just like live-transcript hits.
  • What did NOT change (scope boundary): Stem extraction only. No changes to buildSessionEntry, no changes to the filterMemorySearchHitsBySessionVisibility body, no changes to tools.ts or citations.test.ts. The upstream already has the messageTimestampsMs plumbing and the visibility filter skeleton — only the stem-suffix recognition was missing.

Change Type

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause

  • Root cause: extractTranscriptStemFromSessionsMemoryHit(hitPath) only recognises endsWith(".jsonl") and endsWith(".md"). Archived basenames look like <stem>.jsonl.reset.2026-02-16T22-26-33.000Z, so neither branch matches and the function returns null. filterMemorySearchHitsBySessionVisibility then continues past the hit without resolving any session keys, and the hit is permanently filtered out.
  • Missing detection / guardrail: No unit test asserted stem extraction on archived suffixes, so the regression was invisible at the contract layer. Memory search integration tests that exercise archived transcripts on the happy path don't cover the visibility guard leg of the flow.
  • Contributing context: Session rotation and deletion write sidecar archives with ISO timestamp suffixes by design, precisely so content survives reset; that design only works end-to-end if the downstream consumers of source: "sessions" hits recognise those suffixes.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/plugin-sdk/session-transcript-hit.test.ts
  • Scenario the test should lock in:
    • extractTranscriptStemFromSessionsMemoryHit("sessions/abc.jsonl.reset.<iso>") returns "abc"
    • Same for .jsonl.deleted.<iso>
    • Works with and without the sessions/ prefix
    • Unrelated suffixes (.jsonl.backup.*, etc.) still return null (no false positives)
  • Why this is the smallest reliable guardrail: The bug is a pure path-parsing miss; locking the contract at the unit layer is sufficient and cheap.
  • Existing test that already covers this: None (the existing tests only exercise .jsonl and .md).
  • If no new test is added, why not: N/A — 4 new cases added.

User-visible / Behavior Changes

  • memory_search results from source: "sessions" now include hits on archived transcripts (.jsonl.reset.<iso> / .jsonl.deleted.<iso>) when the live session's visibility allows it, restoring searchability of historical session content after reset.
  • No config/env/defaults changed.

Diagram

Before:
memory_search → raw hit on sessions/abc.jsonl.reset.<iso>
              → filterMemorySearchHitsBySessionVisibility
              → extractTranscriptStemFromSessionsMemoryHit → null (endsWith ".jsonl" false)
              → continue (dropped)
              → 0 archived hits surface

After:
memory_search → raw hit on sessions/abc.jsonl.reset.<iso>
              → filterMemorySearchHitsBySessionVisibility
              → extractTranscriptStemFromSessionsMemoryHit → "abc"
              → resolveTranscriptStemToSessionKeys → ["agent:main:abc"]
              → guard.check(key).allowed → true (if live session visible)
              → hit passes
              → archived hit surfaces normally

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No — the visibility guard is still enforced. Archived hits now pass the stem-extraction stage, but only surface if the live-session visibility guard already allows them. An archive for a session the requester cannot see is still dropped, just further downstream than before (at the guard instead of at stem extraction). Net effect: no broadening of access scope.

Repro + Verification

Environment

  • OS: Linux (container)
  • Runtime: Node 24.14.0, pnpm 10.33.2
  • Model/provider: any
  • Config: default

Steps

  1. Reset an active session (/reset or gateway session rotation) so a .jsonl.reset.<iso> archive lands in sessions/.
  2. Trigger a reindex so the archived content is chunked.
  3. Call memory_search with a query that uniquely matches content only present in the archived transcript.
  4. Before this PR: results is empty even though the archive is on disk and indexed.
  5. After this PR: the archived hit surfaces with the same visibility rules as any live-session hit.

Evidence

Local validation on upstream main at this commit SHA (Linux, Node 24.14.0, pnpm 10.33.2):

  • pnpm checkEXIT 0 (typecheck, lint, policy guards, import-cycle check, temp-path guardrails all green)
  • npx vitest run src/plugin-sdk/session-transcript-hit.test.ts8 passed (4 pre-existing + 4 new archive-suffix cases)
  • npx vitest run extensions/memory-core/src/session-search-visibility.test.ts → passing (visibility guard consumes the extractor and sees correctly-extracted stems now)
  • npx vitest run extensions/memory-core/src/tools.citations.test.ts14 passed (no regression in citation surfacing)
  • Screenshot/recording — not applicable (no UI change)

Human Verification

  • Verified scenarios: The fix is in active production use on a CarHer downstream deployment since 2026-05-02; memory_search on archived session content returns hits instead of empty. The stem extractor change is covered by targeted unit tests with explicit examples of both archive suffixes and the negative case.
  • Edge cases checked: sessions/ prefix with and without; bare basenames; unrelated .jsonl.backup.* style suffixes (correctly rejected as not-an-archive); existing .jsonl / .md cases (unchanged).
  • What I did NOT verify: Full pnpm build and the full pnpm test sweep on upstream main at this exact SHA — only the three targeted test files were exercised; CI will cover the rest on the PR.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — strictly widens the input space the extractor accepts; previous inputs still resolve identically.
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: A path that happens to contain .jsonl.reset. or .jsonl.deleted. mid-name (not as a terminal archive suffix) could be misread as an archive.
    • Mitigation: The regex /\.jsonl\.(?:reset|deleted)\.[^/]+$/ is anchored to the end of the basename and requires content after the ISO dot, so mid-name false positives cannot match. A unit test explicitly rejects weird.jsonl.backup.<ts>.zst.
  • Risk: An archive for a session the requester should not see passes stem extraction and only gets filtered later by the visibility guard.
    • Mitigation: That is the same two-stage filtering model already used for live .jsonl hits; the guard's authoritative check (guard.check(key).allowed) is unchanged.

AI-assisted PR: Prepared with Claude (Opus 4.7). Lightly tested — the underlying fix is in active production use downstream; upstream-rebased version has been unit-test-validated against three targeted test files. Full pnpm test sweep on upstream main is pending CI.

@clawsweeper

clawsweeper Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Summary
This PR indexes reset/deleted session transcript archives, resolves archived session memory hits through owner-aware visibility filtering, adds focused regression tests, and updates the changelog.

Reproducibility: yes. Current-main source inspection shows usage-counted archives are listed, then buildSessionEntry empties archive artifacts and the visibility extractor rejects archive suffixes; I did not run tests in this read-only pass.

Next step before merge
Queue a narrow repair because the blocking issue is localized to session path identity and regression coverage, with no product or security decision needed.

Security
Cleared: No concrete security or supply-chain regression found; the diff touches local session indexing, memory visibility filtering, tests, and changelog only.

Review findings

  • [P2] Preserve live session index paths — packages/memory-host-sdk/src/host/session-files.ts:321-323
Review details

Best possible solution:

Keep the archive indexing and owner-aware visibility fix, but preserve legacy live transcript path identity or add an explicit migration/full reindex before landing with focused memory session tests.

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

Yes. Current-main source inspection shows usage-counted archives are listed, then buildSessionEntry empties archive artifacts and the visibility extractor rejects archive suffixes; I did not run tests in this read-only pass.

Is this the best way to solve the issue?

No. The current PR is directionally correct for archive content and visibility, but rewriting all live transcript index paths is not the narrowest maintainable fix; preserve legacy live paths or add an explicit migration/full reindex.

Full review comments:

  • [P2] Preserve live session index paths — packages/memory-host-sdk/src/host/session-files.ts:321-323
    sessionPathForFile now prefixes every transcript under agents/<id>/sessions with the agent id, so existing live transcripts move from sessions/<basename>.jsonl to sessions/<agent>/<basename>.jsonl. Targeted session sync returns before stale-row pruning, so the first incremental update can leave the old live-session row searchable beside the new row until a full reindex. Please keep the legacy path for non-archive transcripts, or add an explicit migration/full-reindex trigger for the path-format change.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.86

Acceptance criteria:

  • pnpm test packages/memory-host-sdk/src/host/session-files.test.ts
  • pnpm test src/plugin-sdk/session-transcript-hit.test.ts
  • pnpm test extensions/memory-core/src/session-search-visibility.test.ts
  • pnpm test extensions/memory-core/src/memory/manager-session-sync-state.test.ts extensions/memory-core/src/memory/manager-targeted-sync.test.ts
  • pnpm exec oxfmt --check --threads=1 packages/memory-host-sdk/src/host/session-files.ts packages/memory-host-sdk/src/host/session-files.test.ts src/plugin-sdk/session-transcript-hit.ts src/plugin-sdk/session-transcript-hit.test.ts extensions/memory-core/src/session-search-visibility.ts extensions/memory-core/src/session-search-visibility.test.ts CHANGELOG.md

What I checked:

  • current-main archive indexing gap: buildSessionEntry currently short-circuits every session archive artifact before reading JSONL content, so usage-counted reset/deleted archives produce empty session entries on main. (packages/memory-host-sdk/src/host/session-files.ts:64, d89be34360ec)
  • current-main visibility extraction gap: The current extractor only accepts .jsonl and .md basenames, so .jsonl.reset.<iso> and .jsonl.deleted.<iso> hit paths return null and are dropped by the visibility filter. (src/plugin-sdk/session-transcript-hit.ts:12, d89be34360ec)
  • archive artifact contract: Reset and deleted transcript archives are usage-counted session transcript filenames, while .bak archives are not. (src/config/sessions/artifacts.ts:71, d89be34360ec)
  • PR changes live session path identity: The PR head prefixes any transcript under agents/<id>/sessions with sessions/<agentId>/, not only reset/deleted archives that need owner metadata. (packages/memory-host-sdk/src/host/session-files.ts:319, 4881323bc176)
  • targeted sync does not prune old session rows: When activePaths is null for targeted session sync, syncSessionFiles returns before stale-row deletion, so changing an existing live transcript path can leave the old row searchable until a full scan. (extensions/memory-core/src/memory/manager-sync-ops.ts:896, d89be34360ec)
  • PR state and CI snapshot: Public PR metadata shows head 4881323bc1760778d0d0e98776a3c57d5e983798, mergeable=true, mergeable_state=unstable; public check-runs had many successes but at least one in-progress run during inspection. (4881323bc176)

Likely related people:

  • steipete: Recent main-branch commits repeatedly refactored and maintained packages/memory-host-sdk, including session-files.ts and adjacent runtime adapter surfaces. (role: recent memory-host maintainer; confidence: high; commits: 89079a32ef95, 025081dbc576, dc3df62e67c7; files: packages/memory-host-sdk/src/host/session-files.ts, packages/memory-host-sdk/src/host/internal.ts)
  • nefainl: Commit 2c716f5677de added the memory search session visibility filter and the session-transcript-hit helper surfaces affected by this PR. (role: introduced visibility guard behavior; confidence: high; commits: 2c716f5677de; files: extensions/memory-core/src/session-search-visibility.ts, src/plugin-sdk/session-transcript-hit.ts, extensions/memory-core/src/memory/manager-sync-ops.ts)
  • vincentkoc: Main history includes archived QMD session transcript work in session-files.ts, and the current PR head also contains archive visibility hardening by this maintainer. (role: archive/session adjacent maintainer; confidence: high; commits: b7d59f783152, 4881323bc176; files: packages/memory-host-sdk/src/host/session-files.ts, src/plugin-sdk/session-transcript-hit.ts, extensions/memory-core/src/session-search-visibility.ts)

Remaining risk / open question:

  • The targeted Vitest lanes were not executed in this read-only review; the blocker is based on source and diff inspection.

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

@buyitsydney buyitsydney force-pushed the fix/session-files-archive-path branch from a1e1ae0 to a7fe5b6 Compare May 3, 2026 05:22
@buyitsydney

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @clawsweeper — actioned the [P2] findings in force-push a7fe5b65ba.

What changed in this push

  • Extended buildSessionEntry to actually index usage-counted archives (*.jsonl.reset.<iso> and *.jsonl.deleted.<iso>) instead of short-circuiting them to { content: "", lineMap: [] }. shouldSkipTranscriptFileForDreaming now distinguishes usage-counted archives from opaque ones: compaction checkpoints and .bak / legacy store backups stay skipped; .reset/.deleted now fall through to the normal JSONL reader and produce real content, lineMap, and timestamps.
  • Kept the earlier extractor change (extractTranscriptStemFromSessionsMemoryHit recognising the archive suffix). Both halves of the fix now ship together so archive content is actually indexed and the visibility guard can resolve the hit path back to a live session entry.
  • Updated packages/memory-host-sdk/src/host/session-files.test.ts to assert the new contract: .reset/.deleted surface real content/lineMap; .bak and checkpoints stay empty.
  • Updated CHANGELOG.md bullet to describe the end-to-end fix.

AC re-run (locally, on this SHA)

  • npx vitest run src/plugin-sdk/session-transcript-hit.test.ts → 8 passed
  • npx vitest run packages/memory-host-sdk/src/host/session-files.test.ts → 7 passed
  • npx vitest run extensions/memory-core/src/session-search-visibility.test.ts → 5 passed
  • npx vitest run extensions/memory-core/src/memory/index.test.ts → 8 passed (3 skipped, pre-existing)
  • pnpm exec oxfmt --check --threads=1 <all touched files + CHANGELOG.md> → all formatted correctly
  • pnpm check → EXIT 0 (typecheck, lint, policy guards, import-cycle check, temp-path guardrails)

Security note unchanged: the authoritative visibility guard is untouched. An archive hit for a session the requester cannot see is still dropped — just further downstream at the guard instead of silently at extraction/empty-content. .bak and compaction checkpoints still produce no searchable content.

Re-requesting review.

@clawsweeper re-review

buyitsydney and others added 2 commits May 3, 2026 00:57
…ive hits during session visibility filtering

Two coupled regressions together made post-reset memory_search return
empty on archived session content, even when the live session was
visible to the requester.

1. `buildSessionEntry` short-circuits every archive file (`.reset`,
   `.deleted`, `.bak`) to `{ content: '', lineMap: [] }` via
   `shouldSkipTranscriptFileForDreaming`. That is correct for opaque
   backups and compaction checkpoints, but it also zeros out the two
   usage-counted archive kinds (`.jsonl.reset.<iso>` and
   `.jsonl.deleted.<iso>`) that are meant to be searchable as the
   rotated copy of a real session. No chunks are ever produced from
   them, so `memory_search` has nothing to return.

2. `extractTranscriptStemFromSessionsMemoryHit` recognises only
   `.jsonl` and `.md` suffixes. Even if archive content is indexed,
   hits land on `sessions/<stem>.jsonl.reset.<iso>` paths that the
   stem extractor resolves to `null`, so
   `filterMemorySearchHitsBySessionVisibility` drops them before the
   guard check runs.

Fix

- `shouldSkipTranscriptFileForDreaming` now distinguishes
  usage-counted archives from opaque ones: compaction checkpoints and
  non-usage-counted archives (`.bak`, legacy store backups) stay
  skipped; `.jsonl.reset.<iso>` and `.jsonl.deleted.<iso>` fall through
  to the normal JSONL reader and produce searchable content,
  `lineMap`, and timestamps like any other session transcript.

- `extractTranscriptStemFromSessionsMemoryHit` learns the archive
  suffix shape (`\.jsonl\.(?:reset|deleted)\.<iso>$`) and maps those
  hit paths back to the same live `<stem>`. The existing visibility
  guard (`resolveTranscriptStemToSessionKeys` →
  `createSessionVisibilityGuard`) is then authoritative and gates the
  archive hit against the same session entry as a live `.jsonl` hit.

Security / scope

- No change to the authoritative visibility guard. An archive hit for
  a session the requester cannot see is still dropped, just further
  downstream than before (at the guard instead of silently at stem
  extraction / empty-content). `.bak` and compaction checkpoints still
  produce no searchable content.

Tests

- `packages/memory-host-sdk/src/host/session-files.test.ts` now
  asserts that reset/deleted archives produce real content and
  `lineMap`, while `.bak` and compaction checkpoints stay empty.
- `src/plugin-sdk/session-transcript-hit.test.ts` covers both
  archive suffixes with and without the `sessions/` prefix, plus a
  negative case rejecting unrelated `.jsonl.backup.` shaped suffixes.

Refs openclaw#56131. Follow-up to openclaw#76311 (socket retry).

AI-assisted: Prepared with Claude (Opus 4.7). Validated locally with
`pnpm check` (typecheck/lint/guards EXIT 0) and the exact vitest AC
set from the earlier Codex review (session-transcript-hit,
session-files, session-search-visibility, memory/index) all green.
@vincentkoc vincentkoc force-pushed the fix/session-files-archive-path branch from e215eef to 4881323 Compare May 3, 2026 08:01
@vincentkoc vincentkoc self-assigned this May 3, 2026
@vincentkoc vincentkoc merged commit 2ffdb5d into openclaw:main May 3, 2026
101 checks passed
buyitsydney added a commit to buyitsydney/openclaw that referenced this pull request May 3, 2026
… delta threshold for usage-counted archives so new reset/deleted artifacts index incrementally

Two coupled gaps together meant post-reset `.jsonl.reset.<iso>` /
`.jsonl.deleted.<iso>` archives never entered `chunks` on the
incremental memory sync path, only on a manual
`openclaw memory index --force` full reindex.

1. `archiveFileOnDisk` renamed the live `.jsonl` onto disk but never
   emitted a `sessionTranscriptUpdate` event, so the memory sync's
   `ensureSessionListener` never learned the archived path existed.
   Every other in-process session-file mutation already goes through
   `emitSessionTranscriptUpdate` (appendMessage, compaction,
   tool-result rewrite, chat transcript inject, command execution,
   pi-embedded tool-result truncation, `config/sessions/transcript`
   append/truncate plumbing, session tool-result guard); archive was
   the sole emit gap.

2. Even with the emit in place, `processSessionDeltaBatch` would
   forward the event to `updateSessionDelta` and gate it behind the
   `deltaBytes` / `deltaMessages` thresholds (defaults
   `agents.defaults.memorySearch.sync.sessions.deltaBytes: 100000` and
   `deltaMessages: 50`). Those thresholds are designed for live
   transcripts accumulating appended messages; a one-shot archive
   rename below the threshold would never be marked dirty and never
   trigger a session-delta sync, so the archive would still not
   reindex.

Fix

- `src/gateway/session-transcript-files.fs.ts::archiveFileOnDisk`
  emits `emitSessionTranscriptUpdate({ sessionFile: archived })` after
  `fs.renameSync`. Same pattern as the eight existing emit points.

- `extensions/memory-core/src/memory/manager-sync-ops.ts::`
  `processSessionDeltaBatch` now short-circuits the delta accounting
  for usage-counted archive artifacts (classified by
  `isSessionArchiveArtifactName` + `isUsageCountedSessionTranscriptFileName`),
  marking the archive path dirty directly and triggering a session-delta
  sync regardless of size. `.jsonl.bak.<iso>` is explicitly NOT treated
  as usage-counted and therefore does not bypass \u2014 it stays opaque to
  the indexer the same way `buildSessionEntry` already skips it.

- `packages/memory-host-sdk/src/engine-qmd.ts` re-exports the two
  classification helpers so memory-core can consume them via the
  existing plugin-sdk boundary.

Scope

- No change to indexing policy for live transcripts, the chokidar
  watch set, force-reindex behavior, visibility filtering, the
  downstream `onSessionTranscriptUpdate` contract, or the archive
  `.bak`/compaction-checkpoint skip list. The authoritative visibility
  guard remains authoritative.

Tests

- `src/gateway/session-transcript-files.fs.archive-events.test.ts`
  asserts archive emit for reset / deleted / bak.
- `extensions/memory-core/src/memory/manager-sync-ops.archive-delta-bypass.test.ts`
  locks in the classification invariants the bypass depends on
  (reset+deleted bypass, bak explicitly does not, live `.jsonl`
  explicitly does not, compaction checkpoint does not), plus an
  end-to-end verification that `emitSessionTranscriptUpdate` delivers
  the archived path verbatim through the transcript-events bus.

Refs openclaw#56131. Follow-up to openclaw#76311 (socket retry, merged) and openclaw#76452
(archive content indexing + visibility stem). Together with those two
PRs this closes the archive-path memory indexing story: archive files
are read (`openclaw#76452`), mapped back to the live transcript stem
(`openclaw#76452`), the archive event is emitted (this PR), and the memory
sync incremental path now actually reindexes the archive instead of
dropping the event at the delta gate (this PR).

AI-assisted: Prepared with Claude (Opus 4.7). Validated locally with
`pnpm check` EXIT 0 and the two new test files (6 + 8 passed across
all three gateway test environments + extension-memory environment).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants