Skip to content

Commit beb172e

Browse files
Fix history not showing old messages (#3141)
* Fix history not showing old messages due to linking * ccr comment
1 parent 6935d24 commit beb172e

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

src/extension/agents/claude/node/claudeCodeSessionService.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ type RawStoredSDKMessage = SDKMessage & {
2323
readonly timestamp: string;
2424
readonly isMeta?: boolean;
2525
}
26+
27+
/**
28+
* Minimal entry used only for parent-chain resolution.
29+
* These entries lack a message field and are marked as meta entries
30+
* so they're filtered from final output but still enable parent traversal.
31+
*/
32+
interface ChainLinkEntry {
33+
readonly uuid: string;
34+
readonly parentUuid: string | null;
35+
}
36+
2637
interface SummaryEntry {
2738
readonly type: 'summary';
2839
readonly summary: string;
@@ -37,7 +48,7 @@ type StoredSDKMessage = SDKMessage & {
3748
}
3849

3950
interface ParsedSessionMessage {
40-
readonly raw: RawStoredSDKMessage;
51+
readonly raw: RawStoredSDKMessage | ChainLinkEntry;
4152
readonly isMeta: boolean;
4253
}
4354

@@ -359,6 +370,21 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
359370
raw: normalizedRaw,
360371
isMeta: Boolean(isMeta)
361372
});
373+
} else if ('uuid' in entry && entry.uuid && 'parentUuid' in entry) {
374+
// Handle entries without 'message' field (e.g., system messages, metadata entries)
375+
// These are needed for parent chain linking but should not appear in final output
376+
const uuid = entry.uuid;
377+
const parentUuid = ('parentUuid' in entry ? entry.parentUuid : null) as string | null;
378+
379+
const chainLink: ChainLinkEntry = {
380+
uuid,
381+
parentUuid: parentUuid ?? null
382+
};
383+
384+
rawMessages.set(uuid, {
385+
raw: chainLink,
386+
isMeta: true // Mark as meta so it's used for linking but filtered from output
387+
});
362388
} else if ('summary' in entry && entry.summary && !entry.summary.toLowerCase().startsWith('api error: 401') && !entry.summary.toLowerCase().startsWith('invalid api key')) {
363389
const summaryEntry = entry as SummaryEntry;
364390
const uuid = summaryEntry.leafUuid;
@@ -453,6 +479,10 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
453479
.trim();
454480
}
455481

482+
private _isRawStoredSDKMessage(entry: RawStoredSDKMessage | ChainLinkEntry): entry is RawStoredSDKMessage {
483+
return 'type' in entry && 'sessionId' in entry && 'timestamp' in entry;
484+
}
485+
456486
private _reviveStoredMessages(rawMessages: Map<string, ParsedSessionMessage>): Map<string, StoredSDKMessage> {
457487
const messages = new Map<string, StoredSDKMessage>();
458488

@@ -461,6 +491,11 @@ export class ClaudeCodeSessionService implements IClaudeCodeSessionService {
461491
continue;
462492
}
463493

494+
// Non-meta entries should always be RawStoredSDKMessage, not ChainLinkEntry
495+
if (!this._isRawStoredSDKMessage(entry.raw)) {
496+
continue;
497+
}
498+
464499
const parentUuid = this._resolveParentUuid(entry.raw.parentUuid ?? null, rawMessages);
465500
const revived = this._reviveStoredSDKMessage({
466501
...entry.raw,

src/extension/agents/claude/node/test/claudeCodeSessionService.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,87 @@ describe('ClaudeCodeSessionService', () => {
359359
});
360360
});
361361

362+
it('maintains chain through system messages without message field', async () => {
363+
// This test verifies that system messages (which don't have a 'message' field)
364+
// are correctly used for parent chain linking, even though they're filtered from output
365+
const fileName = 'session-with-system-messages.jsonl';
366+
const timestamp = new Date().toISOString();
367+
368+
const fileContents = [
369+
// First user message (root)
370+
JSON.stringify({
371+
parentUuid: null,
372+
sessionId: 'test-session',
373+
type: 'user',
374+
message: { role: 'user', content: 'first message' },
375+
uuid: 'uuid-user-1',
376+
timestamp
377+
}),
378+
// First assistant message
379+
JSON.stringify({
380+
parentUuid: 'uuid-user-1',
381+
sessionId: 'test-session',
382+
type: 'assistant',
383+
message: { role: 'assistant', content: [{ type: 'text', text: 'first response' }] },
384+
uuid: 'uuid-assistant-1',
385+
timestamp
386+
}),
387+
// System message WITHOUT 'message' field (this used to break the chain)
388+
JSON.stringify({
389+
parentUuid: 'uuid-assistant-1',
390+
sessionId: 'test-session',
391+
type: 'system',
392+
subtype: 'stop_hook_summary',
393+
hookCount: 1,
394+
uuid: 'uuid-system-1',
395+
timestamp
396+
}),
397+
// Second user message (child of system message)
398+
JSON.stringify({
399+
parentUuid: 'uuid-system-1',
400+
sessionId: 'test-session',
401+
type: 'user',
402+
message: { role: 'user', content: 'second message' },
403+
uuid: 'uuid-user-2',
404+
timestamp
405+
}),
406+
// Second assistant message
407+
JSON.stringify({
408+
parentUuid: 'uuid-user-2',
409+
sessionId: 'test-session',
410+
type: 'assistant',
411+
message: { role: 'assistant', content: [{ type: 'text', text: 'second response' }] },
412+
uuid: 'uuid-assistant-2',
413+
timestamp
414+
})
415+
].join('\n');
416+
417+
mockFs.mockDirectory(dirUri, [[fileName, FileType.File]]);
418+
mockFs.mockFile(URI.joinPath(dirUri, fileName), fileContents, 1000);
419+
420+
const sessions = await service.getAllSessions(CancellationToken.None);
421+
422+
expect(sessions).toHaveLength(1);
423+
const session = sessions[0];
424+
425+
// The session should have 4 messages (system message is filtered out as isMeta)
426+
expect(session.messages).toHaveLength(4);
427+
428+
// Verify the chain is intact: user1 -> assistant1 -> user2 -> assistant2
429+
// (system message is used for linking but not included in output)
430+
expect(session.messages[0].uuid).toBe('uuid-user-1');
431+
expect(session.messages[1].uuid).toBe('uuid-assistant-1');
432+
expect(session.messages[2].uuid).toBe('uuid-user-2');
433+
expect(session.messages[3].uuid).toBe('uuid-assistant-2');
434+
435+
// Verify message content is preserved
436+
const userMessage1 = session.messages[0] as SDKUserMessage;
437+
expect(userMessage1.message.content).toBe('first message');
438+
439+
const userMessage2 = session.messages[2] as SDKUserMessage;
440+
expect(userMessage2.message.content).toBe('second message');
441+
});
442+
362443
describe('no-workspace scenario', () => {
363444
let noWorkspaceDirUri: URI;
364445
let noWorkspaceService: ClaudeCodeSessionService;

0 commit comments

Comments
 (0)