moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

5
packages/weaver/LICENSE Normal file
View File

@@ -0,0 +1,5 @@
Copyright (c) 2026 Marko Markovic. All rights reserved.
This software is proprietary and confidential. Unauthorized copying,
modification, distribution, or use of this software, via any medium,
is strictly prohibited.

View File

@@ -0,0 +1,25 @@
{
"name": "@waggle/weaver",
"version": "0.1.0",
"description": "Waggle weaver — Memory consolidation engine",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"dependencies": {
"@waggle/core": "*"
},
"license": "MIT"
}

View File

@@ -0,0 +1,255 @@
import type { MindDB, FrameStore, MemoryFrame, Importance, SessionStore, KnowledgeGraph } from '@waggle/core';
const IMPORTANCE_UPGRADE: Record<string, Importance> = {
temporary: 'normal',
normal: 'important',
important: 'critical',
};
export class MemoryWeaver {
private db: MindDB;
private frames: FrameStore;
private sessions: SessionStore;
constructor(db: MindDB, frames: FrameStore, sessions: SessionStore) {
this.db = db;
this.frames = frames;
this.sessions = sessions;
}
consolidateGop(gopId: string): MemoryFrame | null {
const state = this.frames.reconstructState(gopId);
if (!state.iframe || state.pframes.length === 0) return null;
// Merge I-frame + P-frames into consolidated content
const parts = [state.iframe.content, ...state.pframes.map(p => p.content)];
const mergedContent = parts.join('\n---\n');
// Create new consolidated I-frame
const consolidated = this.frames.createIFrame(gopId, mergedContent, 'normal');
// Mark old P-frames as deprecated
const raw = this.db.getDatabase();
const pframeIds = state.pframes.map(p => p.id);
const placeholders = pframeIds.map(() => '?').join(',');
raw.prepare(
`UPDATE memory_frames SET importance = 'deprecated' WHERE id IN (${placeholders})`
).run(...pframeIds);
return consolidated;
}
decayFrames(): number {
const raw = this.db.getDatabase();
// Delete deprecated frames with zero access count
// First get the IDs for FTS cleanup
const toDelete = raw.prepare(
"SELECT id FROM memory_frames WHERE importance = 'deprecated' AND access_count = 0"
).all() as { id: number }[];
if (toDelete.length === 0) return 0;
const ids = toDelete.map(r => r.id);
const placeholders = ids.map(() => '?').join(',');
// Delete from FTS index
raw.prepare(
`DELETE FROM memory_frames_fts WHERE rowid IN (${placeholders})`
).run(...ids);
// Delete the frames
const result = raw.prepare(
`DELETE FROM memory_frames WHERE id IN (${placeholders})`
).run(...ids);
return result.changes;
}
strengthenFrames(tempThreshold = 10, normalThreshold = 25): number {
const raw = this.db.getDatabase();
let upgraded = 0;
// Upgrade temporary → normal
const tempResult = raw.prepare(`
UPDATE memory_frames SET importance = 'normal'
WHERE importance = 'temporary' AND access_count >= ?
`).run(tempThreshold);
upgraded += tempResult.changes;
// Upgrade normal → important
const normalResult = raw.prepare(`
UPDATE memory_frames SET importance = 'important'
WHERE importance = 'normal' AND access_count >= ?
`).run(normalThreshold);
upgraded += normalResult.changes;
return upgraded;
}
createDailySummary(gopIds: string[]): MemoryFrame | null {
if (gopIds.length === 0) return null;
const allContent: string[] = [];
for (const gopId of gopIds) {
const gopFrames = this.frames.getGopFrames(gopId);
for (const frame of gopFrames) {
if (frame.frame_type === 'I' || frame.frame_type === 'P') {
allContent.push(frame.content);
}
}
}
if (allContent.length === 0) return null;
// Create a summary session
const summarySession = this.sessions.create('daily-summary');
const summaryContent = allContent.join('\n---\n');
return this.frames.createIFrame(summarySession.gop_id, summaryContent, 'important');
}
archiveClosedSessions(): number {
const raw = this.db.getDatabase();
const result = raw.prepare(
"UPDATE sessions SET status = 'archived' WHERE status = 'closed'"
).run();
return result.changes;
}
/**
* Deprecate temporary frames older than maxAgeDays with low access count.
* Returns the number of frames deprecated.
*/
decayByAge(maxAgeDays: number, maxAccessCount = 2): number {
const raw = this.db.getDatabase();
const result = raw.prepare(`
UPDATE memory_frames SET importance = 'deprecated'
WHERE importance = 'temporary'
AND access_count <= ?
AND created_at <= datetime('now', '-' || ? || ' days')
`).run(maxAccessCount, maxAgeDays);
return result.changes;
}
/**
* Find frames that share entity names (from the knowledge graph) in their content,
* and create B-frame links between them.
* Returns the number of B-frames created.
*/
linkRelatedFrames(kg: KnowledgeGraph): number {
const raw = this.db.getDatabase();
// Get all active entities from the knowledge graph
const entities = raw.prepare(
'SELECT id, name FROM knowledge_entities WHERE valid_to IS NULL'
).all() as { id: number; name: string }[];
if (entities.length === 0) return 0;
// Get all non-deprecated, non-B frames
const allFrames = raw.prepare(
"SELECT id, gop_id, content FROM memory_frames WHERE importance != 'deprecated' AND frame_type != 'B'"
).all() as { id: number; gop_id: string; content: string }[];
if (allFrames.length < 2) return 0;
// Build a map: entity name → frame IDs that mention it
const entityToFrames = new Map<string, Set<number>>();
for (const entity of entities) {
const nameLower = entity.name.toLowerCase();
const matchingFrameIds = new Set<number>();
for (const frame of allFrames) {
if (frame.content.toLowerCase().includes(nameLower)) {
matchingFrameIds.add(frame.id);
}
}
if (matchingFrameIds.size >= 2) {
entityToFrames.set(entity.name, matchingFrameIds);
}
}
// For each entity with 2+ frames, create a B-frame linking them
// Track already-linked pairs to avoid duplicates
const linkedPairs = new Set<string>();
let created = 0;
for (const [entityName, frameIds] of entityToFrames) {
const ids = Array.from(frameIds);
for (let i = 0; i < ids.length; i++) {
for (let j = i + 1; j < ids.length; j++) {
const pairKey = `${Math.min(ids[i], ids[j])}:${Math.max(ids[i], ids[j])}`;
if (linkedPairs.has(pairKey)) continue;
linkedPairs.add(pairKey);
// Find the gop_id of the base frame
const baseFrame = allFrames.find(f => f.id === ids[i]);
if (!baseFrame) continue;
this.frames.createBFrame(
baseFrame.gop_id,
`Shared entity: ${entityName}`,
ids[i],
[ids[j]]
);
created++;
}
}
}
return created;
}
/**
* Distill session content into a durable memory frame.
* Takes pre-extracted session summary and key points, creates an important frame
* that persists across consolidation cycles.
*/
distillSessionContent(sessionDate: string, summary: string, keyPoints: string[]): MemoryFrame {
const parts = [`Session (${sessionDate}): ${summary}`];
if (keyPoints.length > 0) {
parts.push('Key points: ' + keyPoints.join('; '));
}
const content = parts.join('. ');
// Replace-on-update: re-distilling the same session (same date+summary,
// evolving key points) must update the one distilled frame. createIFrame's
// exact-content dedup can't catch the drifting key-points tail — every
// cron re-run appended another near-identical "Session (…)" frame.
this.frames.deleteByContentPrefix(`Session (${sessionDate}): ${summary}`);
// Create a session for the distilled content (or reuse an active one)
const active = this.sessions.getActive();
let gopId: string;
if (active.length > 0) {
gopId = active[0].gop_id;
} else {
gopId = this.sessions.create('distilled').gop_id;
}
return this.frames.createIFrame(gopId, content, 'important');
}
consolidateProject(projectId: string): MemoryFrame | null {
const projectSessions = this.sessions.getByProject(projectId);
const closedSessions = projectSessions.filter(s => s.status === 'closed' || s.status === 'archived');
if (closedSessions.length === 0) return null;
const allContent: string[] = [];
for (const session of closedSessions) {
const latestI = this.frames.getLatestIFrame(session.gop_id);
if (latestI) {
allContent.push(`[${session.gop_id}] ${latestI.content}`);
}
}
if (allContent.length === 0) return null;
const consolidationSession = this.sessions.create(projectId);
return this.frames.createIFrame(
consolidationSession.gop_id,
allContent.join('\n---\n'),
'important'
);
}
}

View File

@@ -0,0 +1,3 @@
export { MemoryWeaver } from './consolidation.js';
export { extractSessionSkills } from './skill-extractor.js';
export type { SessionEntry, ExtractedSkill } from './skill-extractor.js';

View File

@@ -0,0 +1,48 @@
export interface SessionEntry {
role: string;
content: string;
tools_used?: string[];
}
export interface ExtractedSkill {
name: string;
description: string;
tools: string[];
frequency: number;
}
export function extractSessionSkills(entries: SessionEntry[]): ExtractedSkill[] {
// Group user→agent pairs by tool combo key
const toolCombos = new Map<string, { tools: string[]; count: number; prompts: string[] }>();
for (let i = 0; i < entries.length - 1; i++) {
const user = entries[i];
const agent = entries[i + 1];
if (user.role !== 'user' || agent.role !== 'agent') continue;
if (!agent.tools_used || agent.tools_used.length === 0) continue;
const key = [...agent.tools_used].sort().join('+');
const existing = toolCombos.get(key);
if (existing) {
existing.count++;
existing.prompts.push(user.content);
} else {
toolCombos.set(key, { tools: agent.tools_used, count: 1, prompts: [user.content] });
}
}
// Return patterns with frequency >= 2, or multi-tool at frequency 1
const skills: ExtractedSkill[] = [];
for (const [, combo] of toolCombos) {
if (combo.count >= 2 || combo.tools.length >= 2) {
skills.push({
name: combo.tools.sort().join('+'),
description: `Pattern: ${combo.prompts[0]}`,
tools: combo.tools,
frequency: combo.count,
});
}
}
return skills;
}

View File

@@ -0,0 +1,167 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MindDB, FrameStore, SessionStore, KnowledgeGraph } from '@waggle/core';
import { MemoryWeaver } from '../src/consolidation.js';
describe('Enhanced Memory Consolidation', () => {
let db: MindDB;
let frames: FrameStore;
let sessions: SessionStore;
let kg: KnowledgeGraph;
let weaver: MemoryWeaver;
beforeEach(() => {
db = new MindDB(':memory:');
frames = new FrameStore(db);
sessions = new SessionStore(db);
kg = new KnowledgeGraph(db);
weaver = new MemoryWeaver(db, frames, sessions);
});
afterEach(() => {
db.close();
});
describe('decayByAge — time-based decay', () => {
it('deprecates temporary frames older than maxAgeDays with low access', () => {
const session = sessions.create();
const raw = db.getDatabase();
// Create a temporary frame and backdate it to 45 days ago
const oldFrame = frames.createIFrame(session.gop_id, 'Old temporary note', 'temporary');
raw.prepare(
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
).run(oldFrame.id);
// Create a recent temporary frame (should NOT be deprecated)
frames.createIFrame(session.gop_id, 'Recent temporary note', 'temporary');
// Create an old normal frame (should NOT be deprecated — wrong importance)
const normalFrame = frames.createIFrame(session.gop_id, 'Old normal note', 'normal');
raw.prepare(
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
).run(normalFrame.id);
const deprecated = weaver.decayByAge(30);
expect(deprecated).toBe(1);
// Verify the old temporary frame is now deprecated
const updated = frames.getById(oldFrame.id);
expect(updated!.importance).toBe('deprecated');
// Verify the recent temporary frame is still temporary
const allFrames = frames.getGopFrames(session.gop_id);
const stillTemp = allFrames.filter(f => f.importance === 'temporary');
expect(stillTemp).toHaveLength(1);
expect(stillTemp[0].content).toBe('Recent temporary note');
});
it('does not deprecate old temporary frames with high access count', () => {
const session = sessions.create();
const raw = db.getDatabase();
const frame = frames.createIFrame(session.gop_id, 'Frequently accessed old temp', 'temporary');
raw.prepare(
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
).run(frame.id);
// Give it many accesses
for (let i = 0; i < 5; i++) frames.touch(frame.id);
const deprecated = weaver.decayByAge(30);
expect(deprecated).toBe(0);
const updated = frames.getById(frame.id);
expect(updated!.importance).toBe('temporary');
});
it('returns 0 when no frames match criteria', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Normal frame', 'normal');
frames.createIFrame(session.gop_id, 'Important frame', 'important');
const deprecated = weaver.decayByAge(30);
expect(deprecated).toBe(0);
});
});
describe('linkRelatedFrames — entity-aware linking', () => {
it('creates B-frames linking frames that share entities', () => {
const s1 = sessions.create('project:test');
const s2 = sessions.create('project:test');
// Create frames mentioning the same entity
frames.createIFrame(s1.gop_id, 'Working on the React frontend today');
frames.createIFrame(s2.gop_id, 'React component performance optimization');
// Create an entity in the knowledge graph
kg.createEntity('technology', 'React', { category: 'frontend' });
const linked = weaver.linkRelatedFrames(kg);
expect(linked).toBe(1);
// Verify a B-frame was created
const gopFrames = frames.getGopFrames(s1.gop_id);
const bframes = gopFrames.filter(f => f.frame_type === 'B');
expect(bframes).toHaveLength(1);
// Verify B-frame content references the shared entity
const parsed = JSON.parse(bframes[0].content);
expect(parsed.description).toContain('React');
expect(parsed.references).toHaveLength(1);
});
it('does not create B-frames when no shared entities exist', () => {
const s1 = sessions.create('project:test');
const s2 = sessions.create('project:test');
frames.createIFrame(s1.gop_id, 'Working on the frontend');
frames.createIFrame(s2.gop_id, 'Backend database migration');
kg.createEntity('technology', 'React', { category: 'frontend' });
// Only one frame mentions React, so no link should be created
const linked = weaver.linkRelatedFrames(kg);
expect(linked).toBe(0);
});
it('handles multiple shared entities without duplicate B-frames', () => {
const s1 = sessions.create('project:test');
const s2 = sessions.create('project:test');
// Both frames mention React and TypeScript
frames.createIFrame(s1.gop_id, 'Building React components with TypeScript');
frames.createIFrame(s2.gop_id, 'React + TypeScript best practices');
kg.createEntity('technology', 'React', {});
kg.createEntity('technology', 'TypeScript', {});
const linked = weaver.linkRelatedFrames(kg);
// Only 1 B-frame for the pair, not 2 (one per entity)
expect(linked).toBe(1);
});
it('returns 0 when no entities exist in the knowledge graph', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Some content');
const linked = weaver.linkRelatedFrames(kg);
expect(linked).toBe(0);
});
it('links three frames sharing an entity with correct B-frame count', () => {
const s1 = sessions.create('project:multi');
const s2 = sessions.create('project:multi');
const s3 = sessions.create('project:multi');
frames.createIFrame(s1.gop_id, 'SQLite schema design');
frames.createIFrame(s2.gop_id, 'SQLite performance tuning');
frames.createIFrame(s3.gop_id, 'SQLite backup strategy');
kg.createEntity('technology', 'SQLite', {});
const linked = weaver.linkRelatedFrames(kg);
// 3 frames → 3 pairs: (1,2), (1,3), (2,3)
expect(linked).toBe(3);
});
});
});

View File

@@ -0,0 +1,254 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MindDB, FrameStore, type Importance, SessionStore } from '@waggle/core';
import { MemoryWeaver } from '../src/consolidation.js';
describe('Memory Weaver (Consolidation)', () => {
let db: MindDB;
let frames: FrameStore;
let sessions: SessionStore;
let weaver: MemoryWeaver;
beforeEach(() => {
db = new MindDB(':memory:');
frames = new FrameStore(db);
sessions = new SessionStore(db);
weaver = new MemoryWeaver(db, frames, sessions);
});
afterEach(() => {
db.close();
});
describe('P-frame merging into consolidated I-frame', () => {
it('merges P-frames into a new I-frame within a GOP', () => {
const session = sessions.create();
const iframe = frames.createIFrame(session.gop_id, JSON.stringify({ state: 'initial', items: ['a'] }));
frames.createPFrame(session.gop_id, JSON.stringify({ added: 'b' }), iframe.id);
frames.createPFrame(session.gop_id, JSON.stringify({ added: 'c' }), iframe.id);
frames.createPFrame(session.gop_id, JSON.stringify({ added: 'd' }), iframe.id);
const consolidated = weaver.consolidateGop(session.gop_id);
expect(consolidated).toBeDefined();
expect(consolidated!.frame_type).toBe('I');
// Old P-frames should be marked deprecated
const gopFrames = frames.getGopFrames(session.gop_id);
const deprecated = gopFrames.filter(f => f.importance === 'deprecated' && f.frame_type === 'P');
expect(deprecated).toHaveLength(3);
});
it('skips consolidation if no P-frames exist', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Just a keyframe');
const result = weaver.consolidateGop(session.gop_id);
expect(result).toBeNull();
});
it('consolidated I-frame contains merged content', () => {
const session = sessions.create();
const iframe = frames.createIFrame(session.gop_id, 'Base state');
frames.createPFrame(session.gop_id, 'Delta 1: user asked about weather', iframe.id);
frames.createPFrame(session.gop_id, 'Delta 2: showed forecast', iframe.id);
const consolidated = weaver.consolidateGop(session.gop_id);
expect(consolidated!.content).toContain('Base state');
expect(consolidated!.content).toContain('Delta 1');
expect(consolidated!.content).toContain('Delta 2');
});
});
describe('Decay: remove deprecated frames', () => {
it('removes deprecated frames with zero access', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Active frame', 'normal');
frames.createIFrame(session.gop_id, 'Deprecated unused', 'deprecated');
frames.createIFrame(session.gop_id, 'Deprecated but accessed', 'deprecated');
// Touch the third frame to give it accesses
const gopFrames = frames.getGopFrames(session.gop_id);
const accessedFrame = gopFrames[2];
frames.touch(accessedFrame.id);
frames.touch(accessedFrame.id);
const removed = weaver.decayFrames();
expect(removed).toBe(1); // Only the zero-access deprecated frame
});
it('does not remove non-deprecated frames', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Normal frame', 'normal');
frames.createIFrame(session.gop_id, 'Temporary frame', 'temporary');
const removed = weaver.decayFrames();
expect(removed).toBe(0);
});
});
describe('Strengthen: upgrade frequently accessed frames', () => {
it('upgrades temporary frames to normal after threshold accesses', () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Getting popular', 'temporary');
// Simulate many accesses
for (let i = 0; i < 10; i++) frames.touch(frame.id);
const upgraded = weaver.strengthenFrames(10);
expect(upgraded).toBe(1);
const updated = frames.getById(frame.id);
expect(updated!.importance).toBe('normal');
});
it('upgrades normal frames to important after higher threshold', () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Very popular', 'normal');
for (let i = 0; i < 25; i++) frames.touch(frame.id);
const upgraded = weaver.strengthenFrames(10, 25);
expect(upgraded).toBe(1);
const updated = frames.getById(frame.id);
expect(updated!.importance).toBe('important');
});
it('does not upgrade already critical frames', () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Already critical', 'critical');
for (let i = 0; i < 50; i++) frames.touch(frame.id);
const upgraded = weaver.strengthenFrames(10);
expect(upgraded).toBe(0);
});
});
describe('Daily summary', () => {
it('creates a compressed I-frame from day activity', () => {
const session = sessions.create('project:daily');
const iframe = frames.createIFrame(session.gop_id, 'Morning start');
frames.createPFrame(session.gop_id, 'Checked emails', iframe.id);
frames.createPFrame(session.gop_id, 'Had meeting with team', iframe.id);
frames.createPFrame(session.gop_id, 'Reviewed PRs', iframe.id);
frames.createPFrame(session.gop_id, 'Deployed v2.1', iframe.id);
const summary = weaver.createDailySummary([session.gop_id]);
expect(summary).toBeDefined();
expect(summary!.frame_type).toBe('I');
expect(summary!.importance).toBe('important');
expect(summary!.content).toContain('Morning start');
});
it('returns null when no sessions provided', () => {
const summary = weaver.createDailySummary([]);
expect(summary).toBeNull();
});
});
describe('Session archival', () => {
it('closes and archives old sessions', () => {
const s1 = sessions.create();
const s2 = sessions.create();
frames.createIFrame(s1.gop_id, 'S1 content');
frames.createIFrame(s2.gop_id, 'S2 content');
// Close s1
sessions.close(s1.gop_id, 'Done');
const archived = weaver.archiveClosedSessions();
expect(archived).toBe(1);
const s1Updated = sessions.getByGopId(s1.gop_id);
expect(s1Updated!.status).toBe('archived');
});
it('does not archive active sessions', () => {
sessions.create();
const archived = weaver.archiveClosedSessions();
expect(archived).toBe(0);
});
});
describe('Cross-GOP consolidation', () => {
it('merges related GOPs from same project', () => {
const s1 = sessions.create('project:waggle');
const s2 = sessions.create('project:waggle');
frames.createIFrame(s1.gop_id, 'Session 1: Designed the schema');
frames.createIFrame(s2.gop_id, 'Session 2: Implemented the schema');
sessions.close(s1.gop_id, 'Schema design complete');
sessions.close(s2.gop_id, 'Schema implementation complete');
const merged = weaver.consolidateProject('project:waggle');
expect(merged).toBeDefined();
expect(merged!.content).toContain('Session 1');
expect(merged!.content).toContain('Session 2');
});
it('returns null for project with no closed sessions', () => {
sessions.create('project:empty');
const merged = weaver.consolidateProject('project:empty');
expect(merged).toBeNull();
});
});
describe('Session distillation', () => {
it('creates a durable memory frame from session summary and key points', () => {
const frame = weaver.distillSessionContent(
'2026-03-10',
'Discussed Q2 marketing strategy',
['decided to focus on social media', 'agreed on $50k budget']
);
expect(frame).toBeDefined();
expect(frame.frame_type).toBe('I');
expect(frame.importance).toBe('important');
expect(frame.content).toContain('Session (2026-03-10)');
expect(frame.content).toContain('Discussed Q2 marketing strategy');
expect(frame.content).toContain('decided to focus on social media');
expect(frame.content).toContain('agreed on $50k budget');
});
it('re-distilling the same session replaces the frame instead of duplicating it', () => {
weaver.distillSessionContent('2026-03-10', 'Discussed Q2 marketing strategy', ['point A']);
weaver.distillSessionContent('2026-03-10', 'Discussed Q2 marketing strategy', ['point A', 'point B']);
// A different session the same day must NOT be replaced.
weaver.distillSessionContent('2026-03-10', 'Separate standup recap', []);
const distilled = frames
.getRecent(20)
.filter((f) => f.content.startsWith('Session (2026-03-10)'));
expect(distilled).toHaveLength(2);
const strategy = distilled.filter((f) => f.content.includes('Q2 marketing strategy'));
expect(strategy).toHaveLength(1);
expect(strategy[0].content).toContain('point B');
});
it('creates a frame even without key points', () => {
const frame = weaver.distillSessionContent(
'2026-03-11',
'Quick check-in about project status',
[]
);
expect(frame).toBeDefined();
expect(frame.content).toContain('Session (2026-03-11)');
expect(frame.content).toContain('Quick check-in about project status');
expect(frame.content).not.toContain('Key points');
});
it('distilled frames are marked important (survive decay)', () => {
const frame = weaver.distillSessionContent(
'2026-03-10',
'Important strategic discussion',
['decided to pivot to enterprise']
);
// Run decay — important frames should NOT be affected
weaver.decayByAge(0, 0); // aggressive decay
const afterDecay = frames.getById(frame.id);
expect(afterDecay).toBeDefined();
expect(afterDecay!.importance).toBe('important');
});
});
});

View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import { extractSessionSkills } from '../src/skill-extractor.js';
describe('Session Skill Extraction', () => {
it('extracts repeated tool usage patterns', () => {
const entries = [
{ role: 'user', content: 'Find all TypeScript files' },
{ role: 'agent', content: 'Found 42 files.', tools_used: ['search_files'] },
{ role: 'user', content: 'Find all test files' },
{ role: 'agent', content: 'Found 20 files.', tools_used: ['search_files'] },
];
const skills = extractSessionSkills(entries);
expect(skills.length).toBeGreaterThanOrEqual(1);
expect(skills.some(s => s.tools.includes('search_files'))).toBe(true);
});
it('returns empty for trivial conversations', () => {
const entries = [
{ role: 'user', content: 'Hi' },
{ role: 'agent', content: 'Hello!', tools_used: [] },
];
expect(extractSessionSkills(entries)).toEqual([]);
});
it('extracts multi-tool patterns at frequency 1', () => {
const entries = [
{ role: 'user', content: 'Search and edit the config' },
{ role: 'agent', content: 'Done.', tools_used: ['search_files', 'edit_file'] },
];
const skills = extractSessionSkills(entries);
expect(skills.length).toBe(1);
expect(skills[0].tools).toContain('search_files');
expect(skills[0].tools).toContain('edit_file');
expect(skills[0].frequency).toBe(1);
});
it('ignores single-tool patterns with frequency 1', () => {
const entries = [
{ role: 'user', content: 'Read the file' },
{ role: 'agent', content: 'Here it is.', tools_used: ['read_file'] },
];
const skills = extractSessionSkills(entries);
expect(skills).toEqual([]);
});
it('skips non user→agent pairs', () => {
const entries = [
{ role: 'agent', content: 'Welcome!' },
{ role: 'agent', content: 'Done.', tools_used: ['search_files'] },
];
const skills = extractSessionSkills(entries);
expect(skills).toEqual([]);
});
});

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"],
"references": [
{ "path": "../core" }
]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
testTimeout: 30_000,
include: ['tests/**/*.test.ts'],
},
});