feat(logic): establish derived-activity engine and agent-session protocols

Today we reached a major architectural conclusion: project history shouldn't be stored, it should be derived. We rejected the overhead of a separate SQLite event store in favor of an O(N) snapshot-diffing engine that computes human-readable narratives directly from the issues.jsonl source of truth.

Key Triumphs:
- Implemented O(N) diffing algorithm in src/lib/snapshot-differ.ts that transforms raw JSONL into 16 distinct social event types.
- Engineered a file-based persistence layer (src/lib/activity-persistence.ts) to solve the 'Next.js HMR Wiped My Memory' bug, ensuring project heartbeat survives server restarts.
- Developed the agent-session data model that unifies Beads, Activity, and Cross-Agent Mail into a single 'Mission' context.

Raw Honest Moment:
We struggled for over an hour with 'missing history' before realizing that development-mode reloads were purging our in-memory buffers. The shift to a file-backed ring buffer was a reactive pivot that became a core project strength.
This commit is contained in:
zenchantlive 2026-02-14 00:19:59 -08:00
parent 4f8f3006e9
commit ab051952bd
12 changed files with 1923 additions and 27 deletions

View file

@ -0,0 +1,57 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import type { ActivityEvent, ActivityEventKind } from '../../src/lib/activity';
describe('Activity Event Model (bb-xhm.1)', () => {
it('should support all 16 required transition types in ActivityEventKind', () => {
const kinds: ActivityEventKind[] = [
'created',
'closed',
'reopened',
'status_changed',
'priority_changed',
'assignee_changed',
'type_changed',
'title_changed',
'description_changed',
'labels_changed',
'dependency_added',
'dependency_removed',
'comment_added',
'due_date_changed',
'estimate_changed',
'field_changed',
];
assert.strictEqual(kinds.length, 16, 'Should have exactly 16 transition types');
// Verify specific important types are present
assert.ok(kinds.includes('created'));
assert.ok(kinds.includes('closed'));
assert.ok(kinds.includes('reopened'));
assert.ok(kinds.includes('comment_added'));
});
it('should allow creating a valid ActivityEvent object', () => {
const event: ActivityEvent = {
id: 'evt-123',
kind: 'status_changed',
beadId: 'bb-1',
beadTitle: 'Test Bead',
projectId: 'proj-1',
projectName: 'Test Project',
timestamp: new Date().toISOString(),
actor: 'zenchantlive',
payload: {
field: 'status',
from: 'open',
to: 'in_progress',
},
};
assert.strictEqual(event.kind, 'status_changed');
assert.strictEqual(event.payload.field, 'status');
assert.strictEqual(event.payload.from, 'open');
assert.strictEqual(event.payload.to, 'in_progress');
});
});

View file

@ -0,0 +1,107 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import type { BeadIssue } from '../../src/lib/types';
import type { ActivityEvent } from '../../src/lib/activity';
import { buildSessionTaskFeed } from '../../src/lib/agent-sessions';
// Mock Data
const MOCK_ISSUE: BeadIssue = {
id: 'task-1',
title: 'Test Task',
description: null,
status: 'in_progress',
priority: 2,
issue_type: 'task',
assignee: 'agent-smith',
owner: 'user',
labels: [],
dependencies: [{ type: 'parent', target: 'epic-1' }],
created_at: '2026-01-01',
updated_at: '2026-01-02',
closed_at: null,
close_reason: null,
closed_by_session: null,
created_by: 'user',
due_at: null,
estimated_minutes: null,
external_ref: null,
metadata: {}
};
const MOCK_EPIC: BeadIssue = {
...MOCK_ISSUE,
id: 'epic-1',
title: 'Test Epic',
issue_type: 'epic',
status: 'open',
dependencies: []
};
const MOCK_ACTIVITY: ActivityEvent = {
id: 'evt-1',
kind: 'comment_added',
beadId: 'task-1',
beadTitle: 'Test Task',
projectId: 'root',
projectName: 'root',
timestamp: new Date().toISOString(), // Just now
actor: 'agent-smith',
payload: { message: 'Working on it' }
};
describe('Agent Sessions Aggregation', () => {
it('should group tasks by epic', () => {
const issues = [MOCK_EPIC, MOCK_ISSUE];
const feed = buildSessionTaskFeed(issues, [], { messages: [] });
assert.strictEqual(feed.length, 1); // 1 Epic group
assert.strictEqual(feed[0].epic.id, 'epic-1');
assert.strictEqual(feed[0].tasks.length, 1);
assert.strictEqual(feed[0].tasks[0].id, 'task-1');
});
it('should handle orphan tasks in "Uncategorized" bucket', () => {
const orphan = { ...MOCK_ISSUE, id: 'orphan-1', dependencies: [] };
const feed = buildSessionTaskFeed([orphan], [], { messages: [] });
assert.strictEqual(feed.length, 1);
assert.strictEqual(feed[0].epic.title, 'Uncategorized');
assert.strictEqual(feed[0].tasks[0].id, 'orphan-1');
});
it('should derive session state: active', () => {
const issues = [MOCK_ISSUE]; // in_progress
const feed = buildSessionTaskFeed(issues, [MOCK_ACTIVITY], { messages: [] });
// MOCK_ISSUE is in_progress and has recent activity -> active
const card = feed[0].tasks[0];
assert.strictEqual(card.sessionState, 'active');
});
it('should derive session state: needs_input (blocked)', () => {
const blocked = { ...MOCK_ISSUE, status: 'blocked' as const };
const feed = buildSessionTaskFeed([blocked], [], { messages: [] });
const card = feed[0].tasks[0];
assert.strictEqual(card.sessionState, 'needs_input');
});
it('should derive session state: completed', () => {
const closed = { ...MOCK_ISSUE, status: 'closed' as const };
const feed = buildSessionTaskFeed([closed], [], { messages: [] });
const card = feed[0].tasks[0];
assert.strictEqual(card.sessionState, 'completed');
});
it('should identify stale sessions', () => {
const staleTime = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); // 25 hours ago
const staleIssue = { ...MOCK_ISSUE, updated_at: staleTime };
const oldActivity = { ...MOCK_ACTIVITY, timestamp: staleTime };
const feed = buildSessionTaskFeed([staleIssue], [oldActivity], { messages: [] });
const card = feed[0].tasks[0];
assert.strictEqual(card.sessionState, 'stale');
});
});

View file

@ -0,0 +1,146 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import type { BeadIssueWithProject } from '../../src/lib/types';
import { diffSnapshots } from '../../src/lib/snapshot-differ';
const MOCK_PROJECT = {
key: 'proj-1',
root: 'C:\\test', // Corrected: Escaped backslash for Windows path
displayPath: 'test',
name: 'Test Project',
source: 'local' as const,
addedAt: null,
};
function createMockIssue(id: string, overrides: Partial<BeadIssueWithProject> = {}): BeadIssueWithProject {
return {
id,
title: `Title ${id}`,
description: null,
status: 'open',
priority: 2,
issue_type: 'task',
assignee: null,
owner: 'owner',
labels: [],
dependencies: [],
created_at: '2026-02-13T00:00:00Z',
updated_at: '2026-02-13T00:00:00Z',
closed_at: null,
close_reason: null,
closed_by_session: null,
created_by: 'creator',
due_at: null,
estimated_minutes: null,
external_ref: null,
metadata: {},
project: MOCK_PROJECT,
...overrides,
};
}
describe('Snapshot Differ (bb-xhm.2)', () => {
it('should emit "created" for new issues', () => {
const prev: BeadIssueWithProject[] = [];
const curr: BeadIssueWithProject[] = [createMockIssue('bb-1')];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'created');
assert.strictEqual(events[0].beadId, 'bb-1');
});
it('should emit "status_changed" for non-closed status transitions', () => {
const prev = [createMockIssue('bb-1', { status: 'open' })];
const curr = [createMockIssue('bb-1', { status: 'in_progress' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'status_changed');
assert.strictEqual(events[0].payload.from, 'open');
assert.strictEqual(events[0].payload.to, 'in_progress');
});
it('should emit "closed" when status moves to closed', () => {
const prev = [createMockIssue('bb-1', { status: 'in_progress' })];
const curr = [createMockIssue('bb-1', { status: 'closed', closed_at: '2026-02-13T01:00:00Z' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'closed');
});
it('should emit "reopened" when status moves from closed to open', () => {
const prev = [createMockIssue('bb-1', { status: 'closed' })];
const curr = [createMockIssue('bb-1', { status: 'open' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'reopened');
});
it('should emit "assignee_changed" (assigned/unassigned/reassigned)', () => {
// Assigned
let events = diffSnapshots(
[createMockIssue('bb-1', { assignee: null })],
[createMockIssue('bb-1', { assignee: 'alice' })]
);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'assignee_changed');
assert.strictEqual(events[0].payload.to, 'alice');
// Unassigned
events = diffSnapshots(
[createMockIssue('bb-1', { assignee: 'alice' })],
[createMockIssue('bb-1', { assignee: null })]
);
assert.strictEqual(events[0].payload.from, 'alice');
assert.strictEqual(events[0].payload.to, null);
// Reassigned
events = diffSnapshots(
[createMockIssue('bb-1', { assignee: 'alice' })],
[createMockIssue('bb-1', { assignee: 'bob' })]
);
assert.strictEqual(events[0].payload.from, 'alice');
assert.strictEqual(events[0].payload.to, 'bob');
});
it('should emit "labels_changed" when labels are modified', () => {
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
const curr = [createMockIssue('bb-1', { labels: ['bug', 'ui'] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'labels_changed');
});
it('should emit "dependency_added" and "dependency_removed"', () => {
const prev = [createMockIssue('bb-1', { dependencies: [] })];
const curr = [createMockIssue('bb-1', { dependencies: [{ type: 'blocks', target: 'bb-2' }] })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 1);
assert.strictEqual(events[0].kind, 'dependency_added');
assert.strictEqual(events[0].payload.to, 'bb-2');
});
it('should ignore noise (updated_at only changes)', () => {
const prev = [createMockIssue('bb-1', { updated_at: '2026-02-13T00:00:00Z' })];
const curr = [createMockIssue('bb-1', { updated_at: '2026-02-13T00:01:00Z' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 0);
});
it('should emit multiple events for multiple field changes', () => {
const prev = [createMockIssue('bb-1', { status: 'open', assignee: null })];
const curr = [createMockIssue('bb-1', { status: 'in_progress', assignee: 'alice' })];
const events = diffSnapshots(prev, curr);
assert.strictEqual(events.length, 2);
const kinds = events.map(e => e.kind);
assert.ok(kinds.includes('status_changed'));
assert.ok(kinds.includes('assignee_changed'));
});
});