Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/cli/src/commands/chat-hydration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,55 @@ describe('hydrateLedgerFromTranscript — compaction events', () => {
expect(previewContents.filter((c) => c === 'kept question')).toHaveLength(1);
});

it('includes labeled system turns in tailPreview, excluding continuation noise', () => {
// Heartbeat triggers / channel-delivered messages must stay visible in
// the replay (regression: moving them from type:user to type:system_turn
// dropped them from the preview — answers appeared without questions).
writeTranscript([
{ type: 'system_turn', content: '[HEARTBEAT TRIGGER] check email', label: 'heartbeat' },
{ type: 'assistant', content: 'heartbeat cycle complete', backend: 'claude' },
{
type: 'system_turn',
content: 'Continue working. Use signal_status…',
label: 'continuation',
},
{ type: 'assistant', content: 'still done', backend: 'claude' },
]);

const ledger = new ContextLedger();
const result = hydrateLedgerFromTranscript(ledger, transcriptPath);

const previews = result.tailPreview.map((p) => ({ role: p.role, label: p.label }));
expect(result.tailPreview[0].content).toContain('[HEARTBEAT TRIGGER]');
expect(previews[0]).toEqual({ role: 'system', label: 'heartbeat' });
// Continuation prompts excluded from replay; both assistant replies kept
expect(result.tailPreview.filter((p) => p.label === 'continuation')).toHaveLength(0);
expect(result.tailPreview.filter((p) => p.role === 'assistant')).toHaveLength(2);
});

it('keeps labeled system turns visible through compaction keptEntries', () => {
writeTranscript([
{ type: 'user', content: 'old' },
{
type: 'compaction',
summary: 'the summary',
keptEntries: [
{ role: 'system', content: '[HEARTBEAT TRIGGER] hourly check', source: 'heartbeat' },
{ role: 'assistant', content: 'cycle complete', source: 'claude' },
{ role: 'system', content: 'internal echo', source: 'pcp-activity' },
],
},
]);

const ledger = new ContextLedger();
const result = hydrateLedgerFromTranscript(ledger, transcriptPath);

const labels = result.tailPreview.map((p) => p.label || p.role);
expect(labels).toContain('heartbeat');
expect(labels).not.toContain('pcp-activity'); // internal sources stay out of replay
expect(result.tailPreview.map((p) => p.content)).not.toContain('old');
});

it('skips malformed keptEntries without crashing', () => {
writeTranscript([
{
Expand Down
76 changes: 65 additions & 11 deletions packages/cli/src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,13 @@ interface HistoryHydrationResult {
messageCount: number;
source: 'repl-transcript' | 'pcp-session-context' | 'none';
transcriptPath?: string;
tailPreview: Array<{ role: 'user' | 'assistant' | 'inbox'; content: string; ts?: string }>;
tailPreview: Array<{
role: 'user' | 'assistant' | 'inbox' | 'system';
content: string;
ts?: string;
/** Display label for system entries (e.g., "heartbeat", "continuation") */
label?: string;
}>;
seenInboxIds?: string[];
seenActivityIds?: string[];
/** True when hydration collapsed history at a compaction event */
Expand Down Expand Up @@ -619,8 +625,13 @@ export function hydrateLedgerFromTranscript(
// (and only them; entries that pre-date hydration are left alone).
const hydratedEntryIds: number[] = [];

const pushPreview = (role: 'user' | 'assistant' | 'inbox', content: string, ts?: string) => {
preview.push({ role, content: compactForHistoryPreview(role, content), ts });
const pushPreview = (
role: 'user' | 'assistant' | 'inbox' | 'system',
content: string,
ts?: string,
label?: string
) => {
preview.push({ role, content: compactForHistoryPreview(role, content), ts, label });
if (preview.length > HISTORY_PREVIEW_MAX) {
preview.shift();
}
Expand Down Expand Up @@ -657,11 +668,9 @@ export function hydrateLedgerFromTranscript(
keptRecord.role === 'system'
? keptRecord.role
: 'system';
const entry = ledger.addEntry(
role,
keptRecord.content,
typeof keptRecord.source === 'string' ? keptRecord.source : 'compaction-tail'
);
const source =
typeof keptRecord.source === 'string' ? keptRecord.source : 'compaction-tail';
const entry = ledger.addEntry(role, keptRecord.content, source);
hydratedEntryIds.push(entry.id);
loaded += 1;
if (role === 'user' || role === 'assistant' || role === 'inbox') {
Expand All @@ -671,6 +680,15 @@ export function hydrateLedgerFromTranscript(
keptRecord.content,
typeof event.ts === 'string' ? event.ts : undefined
);
} else if (role === 'system' && !INTERNAL_SYSTEM_SOURCES.has(source)) {
// Kept system turns with a meaningful channel label (heartbeat,
// telegram, …) stay visible in the replay
pushPreview(
'system',
keptRecord.content,
typeof event.ts === 'string' ? event.ts : undefined,
source
);
}
}
continue;
Expand Down Expand Up @@ -712,6 +730,16 @@ export function hydrateLedgerFromTranscript(
hydratedEntryIds.push(entry.id);
loaded += 1;
messageCount += 1;
// Continuation prompts are repetitive noise — keep delivered messages
// (heartbeat triggers, channel messages) visible in the replay.
if (label !== 'continuation') {
pushPreview(
'system',
event.content,
typeof event.ts === 'string' ? event.ts : undefined,
label
);
}
continue;
}
if (type === 'hook_injection' && typeof event.content === 'string') {
Expand Down Expand Up @@ -835,7 +863,25 @@ function compactForLedger(content: string, maxChars = LEDGER_COMPACT_CHARS): str
return `${normalized.slice(0, Math.max(1, maxChars - 1))}…`;
}

function compactForHistoryPreview(role: 'user' | 'assistant' | 'inbox', content: string): string {
// System-entry sources that are runtime bookkeeping, not conversation —
// excluded from the visible history replay (they stay in the ledger).
const INTERNAL_SYSTEM_SOURCES = new Set([
'continuation',
'compaction-tail',
'compaction-history',
'pcp-activity',
'pcp-activity-history',
'passive-recall',
'budget-monitor',
'auto-run',
'hook-history',
'bootstrap',
]);

function compactForHistoryPreview(
role: 'user' | 'assistant' | 'inbox' | 'system',
content: string
): string {
if (role === 'inbox') {
return compactForLedger(content.replace(/\s+/g, ' ').trim(), 180);
}
Expand Down Expand Up @@ -4342,9 +4388,17 @@ export async function runChat(options: ChatOptions): Promise<void> {
? ('user' as const)
: entry.role === 'assistant'
? ('assistant' as const)
: ('inbox' as const);
: entry.role === 'system'
? ('system' as const)
: ('inbox' as const);
const label =
entry.role === 'user' ? 'you' : entry.role === 'assistant' ? agentId : '📬 inbox';
entry.role === 'user'
? 'you'
: entry.role === 'assistant'
? agentId
: entry.role === 'system'
? entry.label || 'system'
: '📬 inbox';
inkRepl.addMessage(role, entry.content, {
label,
time: formatHumanTime(entry.ts, runtime.userTimezone),
Expand Down
Loading