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:
parent
4f8f3006e9
commit
ab051952bd
12 changed files with 1923 additions and 27 deletions
107
tests/lib/agent-sessions.test.ts
Normal file
107
tests/lib/agent-sessions.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue