Append-only reasoning ledger — every decision, discovery, fix, and failure recorded.
Timeline
The Timeline is an append-only reasoning ledger. Every decision, discovery, failure, and learning is recorded as a TimelineEntry with reasoning, confidence, context, and outcome. Entries are buffered in memory and flushed to disk periodically or on demand. This is what prevents thousands of agents from repeating the same mistakes.
import { Timeline, CortexIO } from '@codmir/cortex';Constructor
new Timeline(io: CortexIO)| Parameter | Type | Description |
|---|---|---|
io | CortexIO | The CortexIO instance for file operations |
const io = new CortexIO('/home/dev/my-project');
await io.init();
const timeline = new Timeline(io);Recording
record()
Creates a new timeline entry and adds it to the in-memory buffer. The entry is not written to disk until flush() is called (or auto-flush triggers).
async record(opts: {
agentId: string;
type: TimelineEntryType;
action: string;
reasoning: string;
confidence?: number;
context?: Partial<TimelineContext>;
refs?: string[];
parent?: string;
outcome?: 'success' | 'failure' | 'pending' | 'abandoned';
}): Promise<TimelineEntry>| Parameter | Type | Description |
|---|---|---|
opts.agentId | string | The agent recording this entry |
opts.type | TimelineEntryType | Entry type (decision, failure, learning, etc.) |
opts.action | string | What was done |
opts.reasoning | string | Why it was done |
opts.confidence | number (optional) | Confidence score 0-1 (default: 0.8) |
opts.context | Partial<TimelineContext> (optional) | Files, branch, trigger, state |
opts.refs | string[] (optional) | Reference IDs (other entries, tasks, etc.) |
opts.parent | string (optional) | Parent entry ID for nested reasoning |
opts.outcome | string (optional) | Initial outcome (default: 'pending') |
Returns the created TimelineEntry with a generated id and timestamp.
const entry = await timeline.record({
agentId: 'agent_alpha',
type: 'decision',
action: 'Chose NestJS over Express for API migration',
reasoning: 'NestJS provides DI, decorators, and module system needed for the server architecture',
confidence: 0.9,
context: {
files: ['apps/server/src/main.ts'],
branch: 'feat/api-migration',
state: { migratedRoutes: 42 },
},
});resolve()
Updates the outcome of an existing entry, both in the buffer and on disk.
async resolve(
entryId: string,
outcome: 'success' | 'failure' | 'pending' | 'abandoned',
detail?: string
): Promise<void>| Parameter | Type | Description |
|---|---|---|
entryId | string | The entry ID to resolve |
outcome | string | New outcome value |
detail | string (optional) | Additional detail about the outcome |
await timeline.resolve(entry.id, 'success', 'Migration completed, all tests pass');flush()
Writes all buffered entries to disk via CortexIO.appendTimeline() and clears the buffer. Returns the number of entries flushed.
async flush(): Promise<number>const flushed = await timeline.flush();
console.log(`Flushed ${flushed} entries to disk`);Auto-Flush
startAutoFlush()
Starts a periodic flush interval.
startAutoFlush(intervalMs?: number): void| Parameter | Type | Description |
|---|---|---|
intervalMs | number (optional) | Flush interval in ms (default: 5000) |
timeline.startAutoFlush(3000); // flush every 3 secondsstopAutoFlush()
Stops the periodic flush interval.
stopAutoFlush(): voidQuerying
query()
Queries timeline entries from both disk and the in-memory buffer. Results are sorted by timestamp descending (newest first).
async query(opts: {
agentId?: string;
type?: TimelineEntryType;
since?: number;
limit?: number;
outcome?: 'success' | 'failure' | 'pending' | 'abandoned';
}): Promise<TimelineEntry[]>| Parameter | Type | Description |
|---|---|---|
opts.agentId | string (optional) | Filter by agent ID |
opts.type | TimelineEntryType (optional) | Filter by entry type |
opts.since | number (optional) | Minimum timestamp |
opts.limit | number (optional) | Maximum entries to return |
opts.outcome | string (optional) | Filter by outcome |
const recentDecisions = await timeline.query({
type: 'decision',
since: Date.now() - 3600_000,
limit: 10,
});failures()
Shorthand for querying all failure entries.
async failures(since?: number): Promise<TimelineEntry[]>const recentFailures = await timeline.failures(Date.now() - 86400_000);learnings()
Shorthand for querying all learning entries.
async learnings(since?: number): Promise<TimelineEntry[]>decisions()
Shorthand for querying all decision entries.
async decisions(since?: number): Promise<TimelineEntry[]>forTask()
Reads per-task reasoning entries from reasoning/<taskId>.crtx.
async forTask(taskId: string): Promise<TimelineEntry[]>| Parameter | Type | Description |
|---|---|---|
taskId | string | The task identifier |
const reasoning = await timeline.forTask('task_abc');
for (const entry of reasoning) {
console.log(`[${entry.type}] ${entry.action}: ${entry.reasoning}`);
}recordForTask()
Records an entry and immediately appends it to the per-task reasoning file (in addition to the buffer).
async recordForTask(
taskId: string,
opts: Parameters<Timeline['record']>[0]
): Promise<TimelineEntry>const entry = await timeline.recordForTask('task_abc', {
agentId: 'agent_beta',
type: 'experiment',
action: 'Tried parallel file processing',
reasoning: 'Sequential was too slow for 200 files',
confidence: 0.6,
});getBufferSize()
Returns the number of entries currently in the in-memory buffer (not yet flushed to disk).
getBufferSize(): numberTypes
TimelineEntry
interface TimelineEntry {
id: string;
timestamp: number;
agentId: string;
type: TimelineEntryType;
action: string;
reasoning: string;
confidence: number;
context: TimelineContext;
refs: string[];
outcome: 'success' | 'failure' | 'pending' | 'abandoned';
outcomeDetail?: string;
parent?: string;
children?: string[];
}TimelineEntryType
type TimelineEntryType =
| 'decision'
| 'discovery'
| 'fix'
| 'refactor'
| 'experiment'
| 'failure'
| 'learning'
| 'delegation'
| 'escalation'
| 'merge'
| 'conflict';TimelineContext
interface TimelineContext {
files: string[];
task?: string;
branch?: string;
trigger?: string;
dependencies?: string[];
state: Record<string, unknown>;
}Example
import { CortexIO, Timeline } from '@codmir/cortex';
const io = new CortexIO('/home/dev/my-project');
await io.init();
const timeline = new Timeline(io);
timeline.startAutoFlush(5000);
// Record agent reasoning
const decision = await timeline.record({
agentId: 'coordinator',
type: 'delegation',
action: 'Assigned auth migration to agent_alpha',
reasoning: 'Agent has expertise in edge-auth and JWT patterns',
confidence: 0.85,
context: {
files: ['apps/web/src/app/api/auth'],
branch: 'feat/cortex-auth',
state: { totalRoutes: 12, assignedRoutes: 4 },
},
});
// Agent completes work
await timeline.resolve(decision.id, 'success', 'All 4 auth routes migrated');
// Record a task-specific entry
await timeline.recordForTask('auth-migration', {
agentId: 'agent_alpha',
type: 'learning',
action: 'CLI tokens require DB lookup fallback',
reasoning: 'Opaque 32-char tokens are not JWTs — validateToken must try JWT first, then cli_tokens table',
confidence: 1.0,
context: {
files: ['apps/web/src/lib/auth.ts'],
state: {},
},
outcome: 'success',
});
// Query recent failures to avoid repeating mistakes
const failures = await timeline.failures(Date.now() - 86400_000);
console.log(`${failures.length} failures in the last 24 hours`);
// Force flush before shutdown
await timeline.flush();
timeline.stopAutoFlush();