feat: add timeline-report skill with token economics, compress context output 53%

## Summary
- New timeline-report skill for generating narrative project history reports
- Compressed markdown context output ~53% (tables → flat compact lines, verbose labels → terse format)
- Added `full=true` param to /api/context/inject for fetching all observations
- Split TimelineRenderer into separate markdown/color rendering paths
- Removed arbitrary file write vulnerability (dump_to_file param)
- Fixed timestamp ditto marker leaking across session summary boundaries

## Review
- Rebased on main (v10.6.0) to preserve OpenClaw system prompt injection
- Reviewed by /review (gstack) + /octo:review (Codex, Gemini, Claude fleet)
- Security fix (dump_to_file removal) confirmed by all 3 reviewers
- Timestamp bug caught by Codex, fixed

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Alex Newman
2026-03-18 13:57:20 -07:00
committed by GitHub
parent 648c84804c
commit 7e07210635
8 changed files with 468 additions and 269 deletions
+101 -86
View File
@@ -1,7 +1,8 @@
/**
* TimelineRenderer - Renders the chronological timeline of observations and summaries
*
* Handles day grouping, file grouping within days, and table rendering.
* Handles day grouping and rendering. In markdown (LLM) mode, uses flat compact lines.
* In color (terminal) mode, uses file grouping with visual formatting.
*/
import type {
@@ -49,6 +50,103 @@ function getDetailField(obs: Observation, config: ContextConfig): string | null
return obs.facts ? parseJsonArray(obs.facts).join('\n') : null;
}
/**
* Render a single day's timeline items (markdown/LLM mode - flat compact lines)
*/
function renderDayTimelineMarkdown(
day: string,
dayItems: TimelineItem[],
fullObservationIds: Set<number>,
config: ContextConfig,
): string[] {
const output: string[] = [];
output.push(...Markdown.renderMarkdownDayHeader(day));
let lastTime = '';
for (const item of dayItems) {
if (item.type === 'summary') {
lastTime = '';
const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime);
output.push(...Markdown.renderMarkdownSummaryItem(summary, formattedTime));
} else {
const obs = item.data as Observation;
const time = formatTime(obs.created_at);
const showTime = time !== lastTime;
const timeDisplay = showTime ? time : '';
lastTime = time;
const shouldShowFull = fullObservationIds.has(obs.id);
if (shouldShowFull) {
const detailField = getDetailField(obs, config);
output.push(...Markdown.renderMarkdownFullObservation(obs, timeDisplay, detailField, config));
} else {
output.push(Markdown.renderMarkdownTableRow(obs, timeDisplay, config));
}
}
}
return output;
}
/**
* Render a single day's timeline items (color/terminal mode - file grouped with tables)
*/
function renderDayTimelineColor(
day: string,
dayItems: TimelineItem[],
fullObservationIds: Set<number>,
config: ContextConfig,
cwd: string,
): string[] {
const output: string[] = [];
output.push(...Color.renderColorDayHeader(day));
let currentFile: string | null = null;
let lastTime = '';
for (const item of dayItems) {
if (item.type === 'summary') {
currentFile = null;
lastTime = '';
const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime);
output.push(...Color.renderColorSummaryItem(summary, formattedTime));
} else {
const obs = item.data as Observation;
const file = extractFirstFile(obs.files_modified, cwd, obs.files_read);
const time = formatTime(obs.created_at);
const showTime = time !== lastTime;
lastTime = time;
const shouldShowFull = fullObservationIds.has(obs.id);
// Check if we need a new file section
if (file !== currentFile) {
output.push(...Color.renderColorFileHeader(file));
currentFile = file;
}
if (shouldShowFull) {
const detailField = getDetailField(obs, config);
output.push(...Color.renderColorFullObservation(obs, time, showTime, detailField, config));
} else {
output.push(Color.renderColorTableRow(obs, time, showTime, config));
}
}
}
output.push('');
return output;
}
/**
* Render a single day's timeline items
*/
@@ -60,93 +158,10 @@ export function renderDayTimeline(
cwd: string,
useColors: boolean
): string[] {
const output: string[] = [];
// Day header
if (useColors) {
output.push(...Color.renderColorDayHeader(day));
} else {
output.push(...Markdown.renderMarkdownDayHeader(day));
return renderDayTimelineColor(day, dayItems, fullObservationIds, config, cwd);
}
let currentFile: string | null = null;
let lastTime = '';
let tableOpen = false;
for (const item of dayItems) {
if (item.type === 'summary') {
// Close any open table before summary
if (tableOpen) {
output.push('');
tableOpen = false;
currentFile = null;
lastTime = '';
}
const summary = item.data as SummaryTimelineItem;
const formattedTime = formatDateTime(summary.displayTime);
if (useColors) {
output.push(...Color.renderColorSummaryItem(summary, formattedTime));
} else {
output.push(...Markdown.renderMarkdownSummaryItem(summary, formattedTime));
}
} else {
const obs = item.data as Observation;
const file = extractFirstFile(obs.files_modified, cwd, obs.files_read);
const time = formatTime(obs.created_at);
const showTime = time !== lastTime;
const timeDisplay = showTime ? time : '';
lastTime = time;
const shouldShowFull = fullObservationIds.has(obs.id);
// Check if we need a new file section
if (file !== currentFile) {
if (tableOpen) {
output.push('');
}
if (useColors) {
output.push(...Color.renderColorFileHeader(file));
} else {
output.push(...Markdown.renderMarkdownFileHeader(file));
}
currentFile = file;
tableOpen = true;
}
if (shouldShowFull) {
const detailField = getDetailField(obs, config);
if (useColors) {
output.push(...Color.renderColorFullObservation(obs, time, showTime, detailField, config));
} else {
// Close table for full observation in markdown mode
if (tableOpen && !useColors) {
output.push('');
tableOpen = false;
}
output.push(...Markdown.renderMarkdownFullObservation(obs, timeDisplay, detailField, config));
currentFile = null; // Reset to trigger new table header if needed
}
} else {
if (useColors) {
output.push(Color.renderColorTableRow(obs, time, showTime, config));
} else {
output.push(Markdown.renderMarkdownTableRow(obs, timeDisplay, config));
}
}
}
}
// Close any remaining open table
if (tableOpen) {
output.push('');
}
return output;
return renderDayTimelineMarkdown(day, dayItems, fullObservationIds, config);
}
/**