fix(server/providers): harden and correct session history normalization/pagination

Address correctness and safety issues in provider session adapters while
preserving existing normalized message shapes.

Claude sessions:
- Ensure user text content parts generate unique normalized message ids.
- Replace duplicate `${baseId}_text` ids with index-suffixed ids to avoid
  collisions when one user message contains multiple text segments.

Cursor sessions:
- Add session id sanitization before constructing SQLite paths to prevent
  path traversal via crafted session ids.
- Enforce containment by resolving the computed DB path and asserting it stays
  under ~/.cursor/chats/<cwdId>.
- Refactor blob parsing to a two-pass flow: first build blobMap and collect
  JSON blobs, then parse binary parent refs against the fully populated map.
- Fix pagination semantics so limit=0 returns an empty page instead of full
  history, with consistent total/hasMore/offset/limit metadata.

Gemini sessions:
- Honor FetchHistoryOptions pagination by reading limit/offset and slicing
  normalized history accordingly.
- Return consistent hasMore/offset/limit metadata for paged responses.

Validation:
- eslint passed for touched files.
- server TypeScript check passed (tsc --noEmit -p server/tsconfig.json).
This commit is contained in:
Haileyesus
2026-04-21 13:24:52 +03:00
parent d297dd2271
commit 1d885076e9
3 changed files with 81 additions and 33 deletions

View File

@@ -70,7 +70,8 @@ export class ClaudeSessionsProvider implements IProviderSessions {
if (raw.message?.role === 'user' && raw.message?.content) {
if (Array.isArray(raw.message.content)) {
for (const part of raw.message.content) {
for (let partIndex = 0; partIndex < raw.message.content.length; partIndex++) {
const part = raw.message.content[partIndex];
if (part.type === 'tool_result') {
messages.push(createNormalizedMessage({
id: `${baseId}_tr_${part.tool_use_id}`,
@@ -88,7 +89,7 @@ export class ClaudeSessionsProvider implements IProviderSessions {
const text = part.text || '';
if (text && !isInternalContent(text)) {
messages.push(createNormalizedMessage({
id: `${baseId}_text`,
id: `${baseId}_text_${partIndex}`,
sessionId,
timestamp: ts,
provider: PROVIDER,