diff --git a/package-lock.json b/package-lock.json index 7dfa94f..2d9c2a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,37 +1,27 @@ { "name": "@stackmemoryai/stackmemory", -<<<<<<< HEAD - "version": "0.3.26", -======= - "version": "0.5.0", ->>>>>>> swarm/developer-implement-core-feature + "version": "0.5.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stackmemoryai/stackmemory", -<<<<<<< HEAD - "version": "0.3.26", -======= - "version": "0.5.0", ->>>>>>> swarm/developer-implement-core-feature + "version": "0.5.33", "hasInstallScript": true, "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.71.2", + "@anthropic-ai/tokenizer": "^0.0.4", "@aws-sdk/client-s3": "^3.958.0", "@browsermcp/mcp": "^0.1.3", "@google-cloud/storage": "^7.18.0", "@linear/sdk": "^68.1.0", "@modelcontextprotocol/sdk": "^0.5.0", "@stackmemoryai/stackmemory": "^0.3.19", - "@types/bcryptjs": "^2.4.6", "@types/blessed": "^0.1.27", "@types/inquirer": "^9.0.9", "@types/pg": "^8.16.0", - "bcryptjs": "^3.0.3", "better-sqlite3": "^9.2.2", - "blessed": "^0.1.81", "chalk": "^5.3.0", "chromadb": "^3.2.2", "cli-table3": "^0.6.5", @@ -45,17 +35,12 @@ "helmet": "^8.1.0", "ignore": "^7.0.5", "inquirer": "^9.3.8", - "ioredis": "^5.8.2", - "jsonwebtoken": "^9.0.3", - "jwks-rsa": "^3.2.0", "msgpackr": "^1.10.1", "ngrok": "^5.0.0-beta.2", "open": "^11.0.0", "ora": "^9.0.0", "pg": "^8.17.1", - "puppeteer": "^24.34.0", "rate-limiter-flexible": "^9.0.1", - "redis": "^5.10.0", "shell-escape": "^0.2.0", "socket.io": "^4.6.0", "socket.io-client": "^4.6.0", @@ -66,7 +51,10 @@ "zod": "^3.22.4" }, "bin": { + "claude-sm": "bin/claude-sm", + "claude-smd": "bin/claude-smd", "codex-sm": "dist/cli/codex-sm.js", + "opencode-sm": "bin/opencode-sm", "stackmemory": "dist/cli/index.js" }, "devDependencies": { @@ -120,6 +108,31 @@ } } }, + "node_modules/@anthropic-ai/tokenizer": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@anthropic-ai/tokenizer/-/tokenizer-0.0.4.tgz", + "integrity": "sha512-EHRKbxlxlc8W4KCBEseByJ7YwyYCmgu9OyN59H9+IYIGPoKv8tXyQXinkeGDI+cI8Tiuz9wk2jZb/kK7AyvL7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "tiktoken": "^1.0.10" + } + }, + "node_modules/@anthropic-ai/tokenizer/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/tokenizer/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -11345,6 +11358,12 @@ "b4a": "^1.6.4" } }, + "node_modules/tiktoken": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.22.tgz", + "integrity": "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/src/__tests__/generated/frame-manager.bench.ts b/src/__tests__/generated/frame-manager.bench.ts index 36d1d52..cbb3960 100644 --- a/src/__tests__/generated/frame-manager.bench.ts +++ b/src/__tests__/generated/frame-manager.bench.ts @@ -4,7 +4,7 @@ */ import { bench, describe } from 'vitest'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import Database from 'better-sqlite3'; import { join } from 'path'; import { mkdtempSync, rmSync } from 'fs'; diff --git a/src/__tests__/generated/frame-manager.generated.test.ts b/src/__tests__/generated/frame-manager.generated.test.ts index f83f460..ad8d626 100644 --- a/src/__tests__/generated/frame-manager.generated.test.ts +++ b/src/__tests__/generated/frame-manager.generated.test.ts @@ -4,7 +4,7 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import Database from 'better-sqlite3'; import { join } from 'path'; import { mkdtempSync, rmSync } from 'fs'; diff --git a/src/__tests__/integration/fixtures/test-data-generator.ts b/src/__tests__/integration/fixtures/test-data-generator.ts index f339970..7184805 100644 --- a/src/__tests__/integration/fixtures/test-data-generator.ts +++ b/src/__tests__/integration/fixtures/test-data-generator.ts @@ -3,7 +3,7 @@ * Creates realistic test data for integration testing */ -import type { Frame, Event } from '../../../core/context/frame-manager.js'; +import type { Frame, Event } from '../../../core/context/index.js'; export interface TestFrameOptions { count?: number; diff --git a/src/__tests__/integration/helpers/test-environment.ts b/src/__tests__/integration/helpers/test-environment.ts index 7c1b017..8a2693e 100644 --- a/src/__tests__/integration/helpers/test-environment.ts +++ b/src/__tests__/integration/helpers/test-environment.ts @@ -4,7 +4,7 @@ */ import { SQLiteAdapter, SQLiteConfig } from '../../../core/database/sqlite-adapter.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { SharedContextLayer } from '../../../core/context/shared-context-layer.js'; import { ContextBridge } from '../../../core/context/context-bridge.js'; import { ContextRetriever } from '../../../core/retrieval/context-retriever.js'; diff --git a/src/agents/core/agent-task-manager.ts b/src/agents/core/agent-task-manager.ts index e75be85..10f7f60 100644 --- a/src/agents/core/agent-task-manager.ts +++ b/src/agents/core/agent-task-manager.ts @@ -15,7 +15,7 @@ import { TaskPriority, } from '../../features/tasks/linear-task-manager.js'; import { logger } from '../../core/monitoring/logger.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { TaskError, ErrorCode } from '../../core/errors/index.js'; export interface AgentTaskSession { diff --git a/src/cli/commands/clear.ts b/src/cli/commands/clear.ts index 37dab9a..ef8e782 100644 --- a/src/cli/commands/clear.ts +++ b/src/cli/commands/clear.ts @@ -12,7 +12,7 @@ import * as path from 'path'; import Database from 'better-sqlite3'; import { existsSync } from 'fs'; import { ClearSurvival } from '../../core/session/clear-survival.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { HandoffGenerator } from '../../core/session/handoff-generator.js'; import { sessionManager } from '../../core/session/session-manager.js'; import { getEnv, getOptionalEnv } from '../../utils/env.js'; diff --git a/src/cli/commands/context.ts b/src/cli/commands/context.ts index b719f26..9f65ff6 100644 --- a/src/cli/commands/context.ts +++ b/src/cli/commands/context.ts @@ -7,7 +7,7 @@ import { Command } from 'commander'; import Database from 'better-sqlite3'; import { join } from 'path'; import { existsSync } from 'fs'; -import { FrameManager, FrameType } from '../../core/context/frame-manager.js'; +import { FrameManager, FrameType } from '../../core/context/index.js'; import { createContextRehydrateCommand } from './context-rehydrate.js'; // Type-safe environment variable access diff --git a/src/cli/commands/dashboard.ts b/src/cli/commands/dashboard.ts index 2aefd9f..6d9d9f7 100644 --- a/src/cli/commands/dashboard.ts +++ b/src/cli/commands/dashboard.ts @@ -6,7 +6,7 @@ import chalk from 'chalk'; import Table from 'cli-table3'; import { SessionManager } from '../../core/session/session-manager.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import Database from 'better-sqlite3'; import { join } from 'path'; import { existsSync } from 'fs'; diff --git a/src/cli/commands/discovery.ts b/src/cli/commands/discovery.ts index 70f81ca..9694d82 100644 --- a/src/cli/commands/discovery.ts +++ b/src/cli/commands/discovery.ts @@ -8,7 +8,7 @@ import Database from 'better-sqlite3'; import { join } from 'path'; import { existsSync } from 'fs'; import chalk from 'chalk'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { LLMContextRetrieval } from '../../core/retrieval/index.js'; import { DiscoveryHandlers } from '../../integrations/mcp/handlers/discovery-handlers.js'; diff --git a/src/cli/commands/handoff.ts b/src/cli/commands/handoff.ts index 625ba26..f0ac523 100644 --- a/src/cli/commands/handoff.ts +++ b/src/cli/commands/handoff.ts @@ -15,7 +15,7 @@ import { import { join } from 'path'; import Database from 'better-sqlite3'; import { z } from 'zod'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { LinearTaskManager } from '../../features/tasks/linear-task-manager.js'; import { logger } from '../../core/monitoring/logger.js'; import { EnhancedHandoffGenerator } from '../../core/session/enhanced-handoff.js'; diff --git a/src/cli/commands/monitor.ts b/src/cli/commands/monitor.ts index 4e5ed67..c960cc9 100644 --- a/src/cli/commands/monitor.ts +++ b/src/cli/commands/monitor.ts @@ -12,7 +12,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import { SessionMonitor } from '../../core/monitoring/session-monitor.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import Database from 'better-sqlite3'; // getProjectRoot function will be defined below import * as fs from 'fs/promises'; diff --git a/src/cli/commands/quality.ts b/src/cli/commands/quality.ts index f425641..3880f2a 100644 --- a/src/cli/commands/quality.ts +++ b/src/cli/commands/quality.ts @@ -14,7 +14,7 @@ import { PostTaskConfig, QualityGateResult, } from '../../integrations/claude-code/post-task-hooks.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import Database from 'better-sqlite3'; // getProjectRoot function will be defined below import * as fs from 'fs/promises'; diff --git a/src/cli/commands/skills.ts b/src/cli/commands/skills.ts index bed927e..06014f3 100644 --- a/src/cli/commands/skills.ts +++ b/src/cli/commands/skills.ts @@ -17,7 +17,7 @@ import { } from '../../skills/unified-rlm-orchestrator.js'; import { DualStackManager } from '../../core/context/dual-stack-manager.js'; import { FrameHandoffManager } from '../../core/context/frame-handoff-manager.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { ContextRetriever } from '../../core/retrieval/context-retriever.js'; import { SQLiteAdapter } from '../../core/database/sqlite-adapter.js'; import { LinearTaskManager } from '../../features/tasks/linear-task-manager.js'; diff --git a/src/cli/commands/workflow.ts b/src/cli/commands/workflow.ts index 63469e7..af54486 100644 --- a/src/cli/commands/workflow.ts +++ b/src/cli/commands/workflow.ts @@ -8,7 +8,7 @@ import chalk from 'chalk'; import * as path from 'path'; import Database from 'better-sqlite3'; import { existsSync } from 'fs'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import { workflowTemplates } from '../../core/frame/workflow-templates.js'; import { sessionManager } from '../../core/session/session-manager.js'; import { getEnv, getOptionalEnv } from '../../utils/env.js'; diff --git a/src/cli/commands/worktree.ts b/src/cli/commands/worktree.ts index 3eaeffa..c7374c6 100644 --- a/src/cli/commands/worktree.ts +++ b/src/cli/commands/worktree.ts @@ -7,7 +7,7 @@ import { Command } from 'commander'; import { WorktreeManager } from '../../core/worktree/worktree-manager.js'; import { ProjectManager } from '../../core/projects/project-manager.js'; -import { FrameManager } from '../../core/context/frame-manager.js'; +import { FrameManager } from '../../core/context/index.js'; import chalk from 'chalk'; import Table from 'cli-table3'; import { join } from 'path'; diff --git a/src/cli/index.ts b/src/cli/index.ts index e0e9290..eb46c05 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -16,7 +16,7 @@ initializeTracing(); import { program } from 'commander'; import { logger } from '../core/monitoring/logger.js'; -import { FrameManager } from '../core/context/frame-manager.js'; +import { FrameManager } from '../core/context/index.js'; import { sessionManager, FrameQueryMode } from '../core/session/index.js'; import { sharedContextLayer } from '../core/context/shared-context-layer.js'; import { UpdateChecker } from '../core/utils/update-checker.js'; diff --git a/src/core/context/__tests__/context-bridge.test.ts b/src/core/context/__tests__/context-bridge.test.ts index 4ee5690..c94ac7d 100644 --- a/src/core/context/__tests__/context-bridge.test.ts +++ b/src/core/context/__tests__/context-bridge.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ContextBridge, contextBridge } from '../context-bridge.js'; -import { FrameManager } from '../frame-manager.js'; +import { FrameManager, type Frame } from '../index.js'; import { sharedContextLayer } from '../shared-context-layer.js'; import { sessionManager } from '../../session/session-manager.js'; -import type { Frame } from '../frame-manager.js'; vi.mock('../shared-context-layer.js'); vi.mock('../../session/session-manager.js'); diff --git a/src/core/context/__tests__/dual-stack-manager-integration.test.ts b/src/core/context/__tests__/dual-stack-manager-integration.test.ts index 0a61b26..eca8523 100644 --- a/src/core/context/__tests__/dual-stack-manager-integration.test.ts +++ b/src/core/context/__tests__/dual-stack-manager-integration.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { DualStackManager } from '../dual-stack-manager.js'; -import { FrameManager } from '../frame-manager.js'; +import { FrameManager } from '../index.js'; import { SQLiteAdapter } from '../../database/sqlite-adapter.js'; import * as fs from 'fs'; import * as path from 'path'; diff --git a/src/core/context/__tests__/frame-manager-cycle-detection.test.ts b/src/core/context/__tests__/frame-manager-cycle-detection.test.ts index 8044e11..1c918b9 100644 --- a/src/core/context/__tests__/frame-manager-cycle-detection.test.ts +++ b/src/core/context/__tests__/frame-manager-cycle-detection.test.ts @@ -2,21 +2,19 @@ * Unit tests for circular reference detection in frame management */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import Database from 'better-sqlite3'; -import { FrameManager } from '../frame-manager.js'; -import { RefactoredFrameManager } from '../refactored-frame-manager.js'; +import { FrameManager } from '../index.js'; import { ErrorCode } from '../../errors/index.js'; describe('Frame Manager - Circular Reference Detection', () => { let db: Database.Database; let frameManager: FrameManager; - + beforeEach(() => { db = new Database(':memory:'); frameManager = new FrameManager(db, 'test-project', { - skipContextBridge: true, - maxFrameDepth: 10, // Lower limit for testing + maxStackDepth: 100, // Stack limit for testing }); }); @@ -24,7 +22,7 @@ describe('Frame Manager - Circular Reference Detection', () => { db.close(); }); - describe('FrameManager', () => { + describe('Cycle Detection', () => { it('should detect direct circular reference (A -> B -> A)', () => { // Create frame A const frameA = frameManager.createFrame({ @@ -49,7 +47,6 @@ describe('Frame Manager - Circular Reference Detection', () => { frameManager.updateParentFrame(frameA, frameB); } catch (error: any) { expect(error.code).toBe(ErrorCode.FRAME_CYCLE_DETECTED); - expect(error.message).toContain('circular reference'); } }); @@ -89,7 +86,6 @@ describe('Frame Manager - Circular Reference Detection', () => { }); it('should prevent creating a frame that would cause a cycle', () => { - // This test requires modifying the createFrame to accept an ID // In practice, cycles are mainly prevented during parent updates const frameA = frameManager.createFrame({ type: 'task', @@ -106,40 +102,55 @@ describe('Frame Manager - Circular Reference Detection', () => { expect(frameManager.getFrame(frameB)?.parent_frame_id).toBe(frameA); }); - it('should enforce maximum depth limit', () => { + it('should detect cycle during traversal safety check', () => { + // Create a chain let parentId: string | undefined; - - // Create a chain of frames up to the limit (depth starts at 0) - for (let i = 0; i <= 10; i++) { + const frameIds: string[] = []; + + for (let i = 0; i < 8; i++) { parentId = frameManager.createFrame({ type: 'task', name: `Frame ${i}`, parentFrameId: parentId, }); + frameIds.push(parentId); } - // Try to create one more frame (should fail due to depth limit) + const lastFrame = frameIds[frameIds.length - 1]; + const firstFrame = frameIds[0]; + + // This should fail due to cycle detection expect(() => { - frameManager.createFrame({ - type: 'task', - name: 'Frame 12', - parentFrameId: parentId, - }); + frameManager.updateParentFrame(firstFrame, lastFrame); }).toThrow(); + }); + }); + + describe('Depth Limits', () => { + it('should enforce maximum depth limit', () => { + let parentId: string | undefined; + let errorThrown = false; - // Check the error is about stack overflow + // Create frames until we hit the depth limit try { - frameManager.createFrame({ - type: 'task', - name: 'Frame 12', - parentFrameId: parentId, - }); + for (let i = 0; i <= 100; i++) { + parentId = frameManager.createFrame({ + type: 'task', + name: `Frame ${i}`, + parentFrameId: parentId, + }); + } } catch (error: any) { + errorThrown = true; expect(error.code).toBe(ErrorCode.FRAME_STACK_OVERFLOW); - expect(error.message).toContain('Maximum frame depth exceeded'); } + + // Should have thrown an error at some point + expect(errorThrown).toBe(true); }); + }); + describe('Parent Updates', () => { it('should allow valid parent updates that do not create cycles', () => { const frameA = frameManager.createFrame({ type: 'task', @@ -188,160 +199,68 @@ describe('Frame Manager - Circular Reference Detection', () => { const updatedFrame = frameManager.getFrame(frameB); expect(updatedFrame?.parent_frame_id).toBeNull(); }); - - it('should detect cycle during traversal safety check', () => { - // Create a chain close to the limit - let parentId: string | undefined; - const frameIds: string[] = []; - - for (let i = 0; i < 8; i++) { - parentId = frameManager.createFrame({ - type: 'task', - name: `Frame ${i}`, - parentFrameId: parentId, - }); - frameIds.push(parentId); - } - - // Attempting to create a very deep chain should trigger safety checks - const lastFrame = frameIds[frameIds.length - 1]; - const firstFrame = frameIds[0]; - - // This should fail due to cycle detection - expect(() => { - frameManager.updateParentFrame(firstFrame, lastFrame); - }).toThrow(); - }); }); - describe('RefactoredFrameManager', () => { - let refactoredManager: RefactoredFrameManager; - - beforeEach(() => { - refactoredManager = new RefactoredFrameManager(db, 'test-project', { - maxStackDepth: 100, // Higher stack limit so we can test frame depth - }); - }); - - it('should detect circular references in refactored manager', () => { - // Create frame A - const frameA = refactoredManager.createFrame({ - type: 'task', - name: 'Frame A', - }); - - // Create frame B as child of A - const frameB = refactoredManager.createFrame({ - type: 'subtask', - name: 'Frame B', - parentFrameId: frameA, - }); - - // Try to update A's parent to B (should fail) - expect(() => { - refactoredManager.updateParentFrame(frameA, frameB); - }).toThrow(); - - // Check the error - try { - refactoredManager.updateParentFrame(frameA, frameB); - } catch (error: any) { - expect(error.code).toBe(ErrorCode.FRAME_CYCLE_DETECTED); - } - }); - + describe('Hierarchy Validation', () => { it('should validate entire frame hierarchy', () => { // Create a valid hierarchy - const frameA = refactoredManager.createFrame({ + const frameA = frameManager.createFrame({ type: 'task', name: 'Frame A', }); - const frameB = refactoredManager.createFrame({ + const frameB = frameManager.createFrame({ type: 'subtask', name: 'Frame B', parentFrameId: frameA, }); - const frameC = refactoredManager.createFrame({ + const frameC = frameManager.createFrame({ type: 'tool_scope', name: 'Frame C', parentFrameId: frameB, }); // Validate hierarchy (should be valid) - const validation = refactoredManager.validateFrameHierarchy(); + const validation = frameManager.validateFrameHierarchy(); expect(validation.isValid).toBe(true); expect(validation.errors).toHaveLength(0); }); - it('should detect depth violations in hierarchy validation', () => { - // Skip this test for refactored manager as it uses different depth tracking - // The refactored manager tracks stack depth, not hierarchy depth - const validation = refactoredManager.validateFrameHierarchy(); - expect(validation.isValid).toBe(true); - }); - - it('should enforce maximum depth in refactored manager', () => { - let parentId: string | undefined; - let errorThrown = false; - - // The refactored manager enforces stack depth, not hierarchy depth - // So we test that it properly tracks depth through hierarchy - try { - for (let i = 0; i <= 100; i++) { - parentId = refactoredManager.createFrame({ - type: 'task', - name: `Frame ${i}`, - parentFrameId: parentId, - }); - } - } catch (error: any) { - errorThrown = true; - // Either stack overflow or depth exceeded is acceptable - expect([ErrorCode.FRAME_STACK_OVERFLOW, ErrorCode.FRAME_STACK_OVERFLOW]).toContain(error.code); - } - - // Should have thrown an error at some point - expect(errorThrown).toBe(true); - }); - it('should handle complex hierarchy validations', () => { // Create a tree structure - const root1 = refactoredManager.createFrame({ + const root1 = frameManager.createFrame({ type: 'task', name: 'Root 1', }); - const root2 = refactoredManager.createFrame({ + const root2 = frameManager.createFrame({ type: 'task', name: 'Root 2', }); - const child1_1 = refactoredManager.createFrame({ + const child1_1 = frameManager.createFrame({ type: 'subtask', name: 'Child 1.1', parentFrameId: root1, }); - const child1_2 = refactoredManager.createFrame({ + const child1_2 = frameManager.createFrame({ type: 'subtask', name: 'Child 1.2', parentFrameId: root1, }); - const child2_1 = refactoredManager.createFrame({ + const child2_1 = frameManager.createFrame({ type: 'subtask', name: 'Child 2.1', parentFrameId: root2, }); // Validate - should be valid - const validation = refactoredManager.validateFrameHierarchy(); + const validation = frameManager.validateFrameHierarchy(); expect(validation.isValid).toBe(true); expect(validation.errors).toHaveLength(0); - - // All frames should be at safe depths expect(validation.warnings).toHaveLength(0); }); }); @@ -373,22 +292,21 @@ describe('Frame Manager - Circular Reference Detection', () => { const fakeId = 'non-existent-frame-id'; - // Try to set parent to non-existent frame - this will actually throw - // because getFrame returns undefined for non-existent frames + // Try to set parent to non-existent frame expect(() => { frameManager.updateParentFrame(frameA, fakeId); - }).toThrow(); // Will throw when trying to get the non-existent parent frame + }).toThrow(); // Try to update non-existent frame expect(() => { frameManager.updateParentFrame(fakeId, frameA); - }).toThrow(); // Should throw for non-existent frame to update + }).toThrow(); }); it('should handle concurrent frame operations safely', () => { // Create multiple frames rapidly const frameIds: string[] = []; - + for (let i = 0; i < 5; i++) { const id = frameManager.createFrame({ type: 'task', @@ -404,4 +322,4 @@ describe('Frame Manager - Circular Reference Detection', () => { }).toThrow(); }); }); -}); \ No newline at end of file +}); diff --git a/src/core/context/__tests__/frame-manager.test.ts b/src/core/context/__tests__/frame-manager.test.ts index 5b07862..2efda84 100644 --- a/src/core/context/__tests__/frame-manager.test.ts +++ b/src/core/context/__tests__/frame-manager.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import Database from 'better-sqlite3'; -import { FrameManager } from '../frame-manager'; +import { FrameManager } from '../index.js'; import { join } from 'path'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; diff --git a/src/core/context/auto-context.ts b/src/core/context/auto-context.ts index aab2ac4..ff4ffbd 100644 --- a/src/core/context/auto-context.ts +++ b/src/core/context/auto-context.ts @@ -3,7 +3,7 @@ * Automatically creates and manages context frames */ -import { FrameManager } from './frame-manager.js'; +import { FrameManager } from './index.js'; import { logger } from '../monitoring/logger.js'; export class AutoContext { diff --git a/src/core/context/compaction-handler.ts b/src/core/context/compaction-handler.ts index f1c07c5..05a2939 100644 --- a/src/core/context/compaction-handler.ts +++ b/src/core/context/compaction-handler.ts @@ -3,7 +3,8 @@ * Preserves critical context across token limit boundaries */ -import { FrameManager, Anchor, Event } from './frame-manager.js'; +import { FrameManager } from './index.js'; +import type { Anchor, Event } from './index.js'; import { logger } from '../monitoring/logger.js'; export interface CompactionMetrics { diff --git a/src/core/context/context-bridge.ts b/src/core/context/context-bridge.ts index a43cb9c..3c9fb7b 100644 --- a/src/core/context/context-bridge.ts +++ b/src/core/context/context-bridge.ts @@ -7,11 +7,10 @@ * - Maintains consistency across sessions */ -import { FrameManager } from './frame-manager.js'; +import { FrameManager, type Frame } from './index.js'; import { sharedContextLayer } from './shared-context-layer.js'; import { sessionManager } from '../session/session-manager.js'; import { logger } from '../monitoring/logger.js'; -import type { Frame } from './frame-manager.js'; export interface BridgeOptions { autoSync: boolean; diff --git a/src/core/context/dual-stack-manager.ts b/src/core/context/dual-stack-manager.ts index ce8febb..7b97a99 100644 --- a/src/core/context/dual-stack-manager.ts +++ b/src/core/context/dual-stack-manager.ts @@ -3,8 +3,8 @@ * Manages both individual and shared team stacks for collaboration */ -import type { Frame, Event, Anchor } from './frame-manager.js'; -import { FrameManager } from './frame-manager.js'; +import type { Frame, Event, Anchor } from './frame-types.js'; +import { FrameManager } from './index.js'; import type { DatabaseAdapter } from '../database/database-adapter.js'; import { SQLiteAdapter } from '../database/sqlite-adapter.js'; import { logger } from '../monitoring/logger.js'; diff --git a/src/core/context/enhanced-rehydration.ts b/src/core/context/enhanced-rehydration.ts index 45fe9e6..65b4709 100644 --- a/src/core/context/enhanced-rehydration.ts +++ b/src/core/context/enhanced-rehydration.ts @@ -6,7 +6,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from '../monitoring/logger.js'; -import { FrameManager } from './frame-manager.js'; +import { FrameManager } from './index.js'; import { CompactionHandler } from './compaction-handler.js'; export interface FileSnapshot { diff --git a/src/core/context/frame-database.ts b/src/core/context/frame-database.ts index b4987d0..2857493 100644 --- a/src/core/context/frame-database.ts +++ b/src/core/context/frame-database.ts @@ -203,6 +203,16 @@ export class FrameDatabase { values.push(updates.closed_at); } + if (updates.parent_frame_id !== undefined) { + setClauses.push('parent_frame_id = ?'); + values.push(updates.parent_frame_id); + } + + if (updates.depth !== undefined) { + setClauses.push('depth = ?'); + values.push(updates.depth); + } + if (setClauses.length === 0) { return; // No updates to apply } diff --git a/src/core/context/frame-handoff-manager.ts b/src/core/context/frame-handoff-manager.ts index 11f6f90..5e9c8bd 100644 --- a/src/core/context/frame-handoff-manager.ts +++ b/src/core/context/frame-handoff-manager.ts @@ -3,7 +3,7 @@ * Handles frame transfers between individual and team stacks with approval workflows */ -import type { Frame, Event, Anchor } from './frame-manager.js'; +import type { Frame, Event, Anchor } from './frame-types.js'; import { DualStackManager, type StackContext, diff --git a/src/core/context/frame-lifecycle-hooks.ts b/src/core/context/frame-lifecycle-hooks.ts new file mode 100644 index 0000000..4343f62 --- /dev/null +++ b/src/core/context/frame-lifecycle-hooks.ts @@ -0,0 +1,178 @@ +/** + * Frame Lifecycle Hooks + * Allows external modules to subscribe to frame events without coupling to FrameManager + */ + +import { logger } from '../monitoring/logger.js'; +import type { Frame, Event, Anchor } from './frame-types.js'; + +/** + * Data passed to frame close hooks + */ +export interface FrameCloseData { + frame: Frame; + events: Event[]; + anchors: Anchor[]; +} + +/** + * Hook function type for frame close events + */ +export type FrameCloseHook = (data: FrameCloseData) => Promise; + +/** + * Hook function type for frame create events + */ +export type FrameCreateHook = (frame: Frame) => Promise; + +/** + * Registered hook with metadata + */ +interface RegisteredHook { + name: string; + handler: T; + priority: number; +} + +/** + * Frame Lifecycle Hooks Registry + * Singleton that manages all frame lifecycle hooks + */ +class FrameLifecycleHooksRegistry { + private closeHooks: RegisteredHook[] = []; + private createHooks: RegisteredHook[] = []; + + /** + * Register a hook to be called when a frame is closed + * @param name - Unique name for the hook (for logging/debugging) + * @param handler - Async function to call when frame closes + * @param priority - Higher priority hooks run first (default: 0) + */ + onFrameClosed( + name: string, + handler: FrameCloseHook, + priority: number = 0 + ): () => void { + const hook: RegisteredHook = { name, handler, priority }; + this.closeHooks.push(hook); + this.closeHooks.sort((a, b) => b.priority - a.priority); + + logger.debug('Registered frame close hook', { name, priority }); + + // Return unregister function + return () => { + this.closeHooks = this.closeHooks.filter((h) => h !== hook); + logger.debug('Unregistered frame close hook', { name }); + }; + } + + /** + * Register a hook to be called when a frame is created + * @param name - Unique name for the hook (for logging/debugging) + * @param handler - Async function to call when frame is created + * @param priority - Higher priority hooks run first (default: 0) + */ + onFrameCreated( + name: string, + handler: FrameCreateHook, + priority: number = 0 + ): () => void { + const hook: RegisteredHook = { name, handler, priority }; + this.createHooks.push(hook); + this.createHooks.sort((a, b) => b.priority - a.priority); + + logger.debug('Registered frame create hook', { name, priority }); + + // Return unregister function + return () => { + this.createHooks = this.createHooks.filter((h) => h !== hook); + logger.debug('Unregistered frame create hook', { name }); + }; + } + + /** + * Trigger all close hooks (called by FrameManager) + * Hooks are fire-and-forget - errors don't affect frame closure + */ + async triggerClose(data: FrameCloseData): Promise { + if (this.closeHooks.length === 0) return; + + const results = await Promise.allSettled( + this.closeHooks.map(async (hook) => { + try { + await hook.handler(data); + } catch (error) { + logger.warn(`Frame close hook "${hook.name}" failed`, { + error: error instanceof Error ? error.message : String(error), + frameId: data.frame.frame_id, + frameName: data.frame.name, + }); + } + }) + ); + + const failed = results.filter((r) => r.status === 'rejected').length; + if (failed > 0) { + logger.debug('Some frame close hooks failed', { + total: this.closeHooks.length, + failed, + frameId: data.frame.frame_id, + }); + } + } + + /** + * Trigger all create hooks (called by FrameManager) + * Hooks are fire-and-forget - errors don't affect frame creation + */ + async triggerCreate(frame: Frame): Promise { + if (this.createHooks.length === 0) return; + + const results = await Promise.allSettled( + this.createHooks.map(async (hook) => { + try { + await hook.handler(frame); + } catch (error) { + logger.warn(`Frame create hook "${hook.name}" failed`, { + error: error instanceof Error ? error.message : String(error), + frameId: frame.frame_id, + frameName: frame.name, + }); + } + }) + ); + + const failed = results.filter((r) => r.status === 'rejected').length; + if (failed > 0) { + logger.debug('Some frame create hooks failed', { + total: this.createHooks.length, + failed, + frameId: frame.frame_id, + }); + } + } + + /** + * Get count of registered hooks (useful for testing) + */ + getHookCounts(): { close: number; create: number } { + return { + close: this.closeHooks.length, + create: this.createHooks.length, + }; + } + + /** + * Clear all hooks (useful for testing) + */ + clearAll(): void { + this.closeHooks = []; + this.createHooks = []; + logger.debug('Cleared all frame lifecycle hooks'); + } +} + +/** + * Singleton instance of the hooks registry + */ +export const frameLifecycleHooks = new FrameLifecycleHooksRegistry(); diff --git a/src/core/context/frame-manager.ts b/src/core/context/frame-manager.ts deleted file mode 100644 index 3eb6da5..0000000 --- a/src/core/context/frame-manager.ts +++ /dev/null @@ -1,1428 +0,0 @@ -/** - * StackMemory Frame Manager - Call Stack Implementation - * Manages nested frames representing the call stack of work - */ - -import Database from 'better-sqlite3'; -import { v4 as uuidv4 } from 'uuid'; -import { logger } from '../monitoring/logger.js'; -import { trace } from '../trace/index.js'; -import { - DatabaseError, - FrameError, - SystemError, - ErrorCode, - wrapError, - createErrorHandler, -} from '../errors/index.js'; -import { retry, withTimeout } from '../errors/recovery.js'; -import { sessionManager, FrameQueryMode } from '../session/index.js'; -import { contextBridge } from './context-bridge.js'; - -// WhatsApp sync integration - lazy loaded to avoid breaking if module has issues -let whatsappSync: { - onFrameClosed: (data: any) => Promise; - createFrameDigestData: (frame: any, events: any[], anchors: any[]) => any; -} | null = null; - -async function loadWhatsAppSync() { - if (whatsappSync !== null) return whatsappSync; - try { - const mod = await import('../../hooks/whatsapp-sync.js'); - whatsappSync = { - onFrameClosed: mod.onFrameClosed, - createFrameDigestData: mod.createFrameDigestData, - }; - return whatsappSync; - } catch { - // Module not available or has errors - disable integration - whatsappSync = null; - return null; - } -} - -// Constants for frame validation -const MAX_FRAME_DEPTH = 100; // Maximum allowed frame depth -const DEFAULT_MAX_DEPTH = 100; // Default if not configured - -// Type-safe environment variable access -function getEnv(key: string, defaultValue?: string): string { - const value = process.env[key]; - if (value === undefined) { - if (defaultValue !== undefined) return defaultValue; - throw new SystemError( - `Environment variable ${key} is required`, - ErrorCode.CONFIGURATION_ERROR, - { variable: key } - ); - } - return value; -} - -function getOptionalEnv(key: string): string | undefined { - return process.env[key]; -} - -// Frame types based on architecture -export type FrameType = - | 'task' - | 'subtask' - | 'tool_scope' - | 'review' - | 'write' - | 'debug'; -export type FrameState = 'active' | 'closed'; - -export interface Frame { - frame_id: string; - run_id: string; - project_id: string; - parent_frame_id?: string; - depth: number; - type: FrameType; - name: string; - state: FrameState; - inputs: Record; - outputs: Record; - digest_text?: string; - digest_json: Record; - created_at: number; - closed_at?: number; -} - -export interface FrameContext { - frameId: string; - header: { - goal: string; - constraints?: string[]; - definitions?: Record; - }; - anchors: Anchor[]; - recentEvents: Event[]; - activeArtifacts: string[]; -} - -export interface Anchor { - anchor_id: string; - frame_id: string; - type: - | 'FACT' - | 'DECISION' - | 'CONSTRAINT' - | 'INTERFACE_CONTRACT' - | 'TODO' - | 'RISK'; - text: string; - priority: number; - metadata: Record; -} - -export interface Event { - event_id: string; - frame_id: string; - run_id: string; - seq: number; - event_type: - | 'user_message' - | 'assistant_message' - | 'tool_call' - | 'tool_result' - | 'decision' - | 'constraint' - | 'artifact' - | 'observation'; - payload: Record; - ts: number; -} - -export interface FrameManagerOptions { - skipContextBridge?: boolean; - runId?: string; - maxFrameDepth?: number; // Maximum allowed frame depth (default: 100) -} - -export class FrameManager { - private db: Database.Database; - private currentRunId: string; - private sessionId: string; - private projectId: string; - private activeStack: string[] = []; // Stack of active frame IDs - private queryMode: FrameQueryMode = FrameQueryMode.PROJECT_ACTIVE; - private maxFrameDepth: number = DEFAULT_MAX_DEPTH; - - constructor( - db: Database.Database, - projectId: string, - runIdOrOptions?: string | FrameManagerOptions - ) { - this.db = db; - this.projectId = projectId; - - // Handle both legacy string runId and new options object - let runId: string | undefined; - let skipContextBridge = false; - - if (typeof runIdOrOptions === 'string') { - runId = runIdOrOptions; - } else if (runIdOrOptions) { - runId = runIdOrOptions.runId; - skipContextBridge = runIdOrOptions.skipContextBridge || false; - this.maxFrameDepth = runIdOrOptions.maxFrameDepth || DEFAULT_MAX_DEPTH; - } - - // Use session manager for run ID if available - const session = sessionManager.getCurrentSession(); - if (session) { - this.currentRunId = session.runId; - this.sessionId = session.sessionId; - } else { - this.currentRunId = runId || uuidv4(); - this.sessionId = this.currentRunId; // Fallback for legacy behavior - } - - this.initializeSchema(); - this.loadActiveStack(); - - // Initialize context bridge for automatic shared context - // Skip in test environment, when explicitly requested, or for CLI usage - const shouldInitializeBridge = - !skipContextBridge && - process.env['NODE_ENV'] !== 'test' && - !process.env['VITEST'] && - !process.env['STACKMEMORY_CLI']; - - if (shouldInitializeBridge) { - contextBridge - .initialize(this, { - autoSync: true, - syncInterval: 60000, // 1 minute - minFrameScore: 0.5, // Sync frames above 0.5 score - importantTags: ['decision', 'error', 'milestone', 'learning'], - }) - .catch((error) => { - logger.warn('Failed to initialize context bridge', { error }); - }); - } - } - - setQueryMode(mode: FrameQueryMode): void { - this.queryMode = mode; - this.loadActiveStack(); // Reload with new mode - } - - private initializeSchema() { - const errorHandler = createErrorHandler({ - operation: 'initializeSchema', - projectId: this.projectId, - runId: this.currentRunId, - }); - - try { - // Check if database is properly initialized - if (!this.db || typeof this.db.exec !== 'function') { - throw new DatabaseError( - 'Database not properly initialized. Expected SQLite Database instance with exec() method.', - ErrorCode.DB_CONNECTION_FAILED, - { operation: 'initializeSchema' } - ); - } - - // Enforce referential integrity - this.db.pragma('foreign_keys = ON'); - - // Enhanced frames table matching architecture - this.db.exec(` - CREATE TABLE IF NOT EXISTS frames ( - frame_id TEXT PRIMARY KEY, - run_id TEXT NOT NULL, - project_id TEXT NOT NULL, - parent_frame_id TEXT REFERENCES frames(frame_id), - depth INTEGER NOT NULL DEFAULT 0, - type TEXT NOT NULL, - name TEXT NOT NULL, - state TEXT DEFAULT 'active', - inputs TEXT DEFAULT '{}', - outputs TEXT DEFAULT '{}', - digest_text TEXT, - digest_json TEXT DEFAULT '{}', - created_at INTEGER DEFAULT (unixepoch()), - closed_at INTEGER - ); - - CREATE TABLE IF NOT EXISTS events ( - event_id TEXT PRIMARY KEY, - run_id TEXT NOT NULL, - frame_id TEXT NOT NULL, - seq INTEGER NOT NULL, - event_type TEXT NOT NULL, - payload TEXT NOT NULL, - ts INTEGER DEFAULT (unixepoch()), - FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS anchors ( - anchor_id TEXT PRIMARY KEY, - frame_id TEXT NOT NULL, - project_id TEXT NOT NULL, - type TEXT NOT NULL, - text TEXT NOT NULL, - priority INTEGER DEFAULT 0, - created_at INTEGER DEFAULT (unixepoch()), - metadata TEXT DEFAULT '{}', - FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS handoff_requests ( - request_id TEXT PRIMARY KEY, - source_stack_id TEXT NOT NULL, - target_stack_id TEXT NOT NULL, - frame_ids TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - created_at INTEGER DEFAULT (unixepoch()), - expires_at INTEGER, - target_user_id TEXT, - message TEXT - ); - - CREATE INDEX IF NOT EXISTS idx_frames_run ON frames(run_id); - CREATE INDEX IF NOT EXISTS idx_frames_parent ON frames(parent_frame_id); - CREATE INDEX IF NOT EXISTS idx_frames_state ON frames(state); - CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id); - CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq); - CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id); - CREATE INDEX IF NOT EXISTS idx_handoff_requests_status ON handoff_requests(status); - CREATE INDEX IF NOT EXISTS idx_handoff_requests_target ON handoff_requests(target_stack_id); - `); - - // For existing databases, ensure cascade constraints are applied - try { - this.ensureCascadeConstraints(); - } catch (e) { - logger.warn( - 'Failed to ensure cascade constraints (frame-manager)', - e as Error - ); - } - } catch (error: unknown) { - const dbError = errorHandler(error, { - operation: 'initializeSchema', - schema: 'frames', - }); - - if (dbError instanceof DatabaseError) { - throw new DatabaseError( - 'Failed to initialize frame database schema', - ErrorCode.DB_MIGRATION_FAILED, - { - projectId: this.projectId, - operation: 'initializeSchema', - originalError: error, - }, - error instanceof Error ? error : undefined - ); - } - throw dbError; - } - } - - // Ensure ON DELETE CASCADE for events/anchors referencing frames - private ensureCascadeConstraints(): void { - const needsCascade = (table: string): boolean => { - const rows = this.db - .prepare(`PRAGMA foreign_key_list(${table})`) - .all() as any[]; - return rows.some( - (r) => - r.table === 'frames' && - String(r.on_delete).toUpperCase() !== 'CASCADE' - ); - }; - - const migrateTable = (table: 'events' | 'anchors') => { - const createSql = - table === 'events' - ? `CREATE TABLE events_new ( - event_id TEXT PRIMARY KEY, - run_id TEXT NOT NULL, - frame_id TEXT NOT NULL, - seq INTEGER NOT NULL, - event_type TEXT NOT NULL, - payload TEXT NOT NULL, - ts INTEGER DEFAULT (unixepoch()), - FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE - );` - : `CREATE TABLE anchors_new ( - anchor_id TEXT PRIMARY KEY, - frame_id TEXT NOT NULL, - project_id TEXT NOT NULL, - type TEXT NOT NULL, - text TEXT NOT NULL, - priority INTEGER DEFAULT 0, - created_at INTEGER DEFAULT (unixepoch()), - metadata TEXT DEFAULT '{}', - FOREIGN KEY(frame_id) REFERENCES frames(frame_id) ON DELETE CASCADE - );`; - - const cols = - table === 'events' - ? 'event_id, run_id, frame_id, seq, event_type, payload, ts' - : 'anchor_id, frame_id, project_id, type, text, priority, created_at, metadata'; - - const idxSql = - table === 'events' - ? [ - 'CREATE INDEX IF NOT EXISTS idx_events_frame ON events(frame_id);', - 'CREATE INDEX IF NOT EXISTS idx_events_seq ON events(frame_id, seq);', - ] - : [ - 'CREATE INDEX IF NOT EXISTS idx_anchors_frame ON anchors(frame_id);', - ]; - - this.db.exec('PRAGMA foreign_keys = OFF;'); - this.db.exec('BEGIN;'); - this.db.exec(createSql); - this.db - .prepare( - `INSERT INTO ${table === 'events' ? 'events_new' : 'anchors_new'} (${cols}) SELECT ${cols} FROM ${table}` - ) - .run(); - this.db.exec(`DROP TABLE ${table};`); - this.db.exec(`ALTER TABLE ${table}_new RENAME TO ${table};`); - for (const stmt of idxSql) this.db.exec(stmt); - this.db.exec('COMMIT;'); - this.db.exec('PRAGMA foreign_keys = ON;'); - logger.info( - `Migrated ${table} to include ON DELETE CASCADE (frame-manager)` - ); - }; - - if (needsCascade('events')) migrateTable('events'); - if (needsCascade('anchors')) migrateTable('anchors'); - } - - private loadActiveStack() { - const errorHandler = createErrorHandler({ - operation: 'loadActiveStack', - runId: this.currentRunId, - projectId: this.projectId, - }); - - try { - let query: string; - let params: any[]; - - // Build query based on query mode - switch (this.queryMode) { - case FrameQueryMode.ALL_ACTIVE: - query = ` - SELECT frame_id, parent_frame_id, depth - FROM frames - WHERE state = 'active' - ORDER BY created_at DESC, depth ASC - `; - params = []; - break; - - case FrameQueryMode.PROJECT_ACTIVE: - query = ` - SELECT frame_id, parent_frame_id, depth, run_id - FROM frames - WHERE state = 'active' AND project_id = ? - ORDER BY created_at DESC, depth ASC - `; - params = [this.projectId]; - break; - - case FrameQueryMode.HISTORICAL: - query = ` - SELECT frame_id, parent_frame_id, depth - FROM frames - WHERE project_id = ? - ORDER BY created_at DESC, depth ASC - `; - params = [this.projectId]; - break; - - case FrameQueryMode.CURRENT_SESSION: - default: - query = ` - SELECT frame_id, parent_frame_id, depth - FROM frames - WHERE run_id = ? AND state = 'active' - ORDER BY depth ASC - `; - params = [this.currentRunId]; - break; - } - - const activeFrames = this.db.prepare(query).all(...params) as Frame[]; - - // Rebuild stack order - this.activeStack = this.buildStackOrder(activeFrames); - - logger.info('Loaded active stack', { - runId: this.currentRunId, - stackDepth: this.activeStack.length, - activeFrames: this.activeStack, - queryMode: this.queryMode, - }); - } catch (error: unknown) { - const dbError = errorHandler(error, { - query: 'Frame loading query', - runId: this.currentRunId, - queryMode: this.queryMode, - }); - - if (dbError instanceof DatabaseError) { - throw new DatabaseError( - 'Failed to load active frame stack', - ErrorCode.DB_QUERY_FAILED, - { - runId: this.currentRunId, - projectId: this.projectId, - operation: 'loadActiveStack', - }, - error instanceof Error ? error : undefined - ); - } - throw dbError; - } - } - - private buildStackOrder( - frames: Pick[] - ): string[] { - const stack: string[] = []; - - // Find root frame (no parent) - const rootFrame = frames.find((f) => !f.parent_frame_id); - if (!rootFrame) return []; - - // Build stack by following parent-child relationships - let currentFrame = rootFrame; - stack.push(currentFrame.frame_id); - - while (currentFrame) { - const childFrame = frames.find( - (f) => f.parent_frame_id === currentFrame.frame_id - ); - if (!childFrame) break; - stack.push(childFrame.frame_id); - currentFrame = childFrame; - } - - return stack; - } - - /** - * Create a new frame and push to stack - */ - public createFrame(options: { - type: FrameType; - name: string; - inputs?: Record; - parentFrameId?: string; - }): string { - return this._createFrame(options); - } - - private _createFrame(options: { - type: FrameType; - name: string; - inputs?: Record; - parentFrameId?: string; - }): string { - const frameId = uuidv4(); - const parentFrameId = options.parentFrameId || this.getCurrentFrameId(); - const depth = parentFrameId ? this.getFrameDepth(parentFrameId) + 1 : 0; - - // Check for depth limit - if (depth > this.maxFrameDepth) { - throw new FrameError( - `Maximum frame depth exceeded: ${depth} > ${this.maxFrameDepth}`, - ErrorCode.FRAME_STACK_OVERFLOW, - { - currentDepth: depth, - maxDepth: this.maxFrameDepth, - frameId, - parentFrameId, - frameName: options.name, - } - ); - } - - // Check for circular reference before creating frame - if (parentFrameId) { - const cycle = this.detectCycle(frameId, parentFrameId); - if (cycle) { - throw new FrameError( - `Circular reference detected in frame hierarchy`, - ErrorCode.FRAME_CYCLE_DETECTED, - { - frameId, - parentFrameId, - cycle, - frameName: options.name, - } - ); - } - } - - const frame: Omit< - Frame, - 'outputs' | 'digest_text' | 'digest_json' | 'closed_at' - > = { - frame_id: frameId, - run_id: this.currentRunId, - project_id: this.projectId, - parent_frame_id: parentFrameId, - depth, - type: options.type, - name: options.name, - state: 'active', - inputs: options.inputs || {}, - created_at: Math.floor(Date.now() / 1000), - }; - - try { - this.db - .prepare( - ` - INSERT INTO frames ( - frame_id, run_id, project_id, parent_frame_id, depth, type, name, state, inputs, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ` - ) - .run( - frame.frame_id, - frame.run_id, - frame.project_id, - frame.parent_frame_id, - frame.depth, - frame.type, - frame.name, - frame.state, - JSON.stringify(frame.inputs), - frame.created_at - ); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to create frame: ${options.name}`, - ErrorCode.DB_QUERY_FAILED, - { - frameId, - frameType: options.type, - frameName: options.name, - parentFrameId, - depth, - operation: 'createFrame', - }, - error instanceof Error ? error : undefined - ); - } - - // Push to active stack - this.activeStack.push(frameId); - - logger.info('Created frame', { - frameId, - type: options.type, - name: options.name, - depth, - parentFrameId, - stackDepth: this.activeStack.length, - }); - - return frameId; - } - - /** - * Close the current frame and generate digest - */ - public closeFrame(frameId?: string, outputs?: Record): void { - this._closeFrame(frameId, outputs); - } - - private _closeFrame(frameId?: string, outputs?: Record): void { - const targetFrameId = frameId || this.getCurrentFrameId(); - if (!targetFrameId) { - throw new FrameError( - 'No active frame to close', - ErrorCode.FRAME_INVALID_STATE, - { - operation: 'closeFrame', - activeStack: this.activeStack, - stackDepth: this.activeStack.length, - } - ); - } - - // Get frame details - const frame = this.getFrame(targetFrameId); - if (!frame) { - throw new FrameError( - `Frame not found: ${targetFrameId}`, - ErrorCode.FRAME_NOT_FOUND, - { - frameId: targetFrameId, - operation: 'closeFrame', - runId: this.currentRunId, - } - ); - } - - if (frame.state === 'closed') { - logger.warn('Attempted to close already closed frame', { - frameId: targetFrameId, - }); - return; - } - - // Generate digest before closing - const digest = this.generateDigest(targetFrameId); - const finalOutputs = { ...outputs, ...digest.structured }; - - try { - // Update frame to closed state - this.db - .prepare( - ` - UPDATE frames - SET state = 'closed', - outputs = ?, - digest_text = ?, - digest_json = ?, - closed_at = unixepoch() - WHERE frame_id = ? - ` - ) - .run( - JSON.stringify(finalOutputs), - digest.text, - JSON.stringify(digest.structured), - targetFrameId - ); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to close frame: ${targetFrameId}`, - ErrorCode.DB_QUERY_FAILED, - { - frameId: targetFrameId, - frameName: frame.name, - operation: 'closeFrame', - }, - error instanceof Error ? error : undefined - ); - } - - // Remove from active stack - this.activeStack = this.activeStack.filter((id) => id !== targetFrameId); - - // Close all child frames recursively - this.closeChildFrames(targetFrameId); - - // Trigger WhatsApp auto-sync if enabled (fire and forget) - this.triggerWhatsAppSync(frame, targetFrameId).catch(() => { - // Silently ignore errors - sync is non-critical - }); - - logger.info('Closed frame', { - frameId: targetFrameId, - name: frame.name, - duration: Math.floor(Date.now() / 1000) - frame.created_at, - digestLength: digest.text.length, - stackDepth: this.activeStack.length, - }); - } - - /** - * Trigger WhatsApp sync for closed frame (non-blocking) - */ - private async triggerWhatsAppSync( - frame: Frame, - frameId: string - ): Promise { - const sync = await loadWhatsAppSync(); - if (!sync) return; - - try { - const events = this.getFrameEvents(frameId); - const anchors = this.getFrameAnchors(frameId); - const digestData = sync.createFrameDigestData(frame, events, anchors); - await sync.onFrameClosed(digestData); - } catch { - // Silently ignore - WhatsApp sync is optional - } - } - - /** - * Delete a frame completely from the database (used in handoffs) - */ - deleteFrame(frameId: string): void { - try { - // First delete related data - this.db.prepare('DELETE FROM events WHERE frame_id = ?').run(frameId); - this.db.prepare('DELETE FROM anchors WHERE frame_id = ?').run(frameId); - - // Remove from active stack if present - this.activeStack = this.activeStack.filter((id) => id !== frameId); - - // Delete the frame itself - this.db.prepare('DELETE FROM frames WHERE frame_id = ?').run(frameId); - - logger.debug('Deleted frame completely', { frameId }); - } catch (error: unknown) { - logger.error('Failed to delete frame', { frameId, error }); - throw error; - } - } - - private closeChildFrames(parentFrameId: string) { - try { - const children = this.db - .prepare( - ` - SELECT frame_id FROM frames - WHERE parent_frame_id = ? AND state = 'active' - ` - ) - .all(parentFrameId) as { frame_id: string }[]; - - children.forEach((child) => { - try { - this.closeFrame(child.frame_id); - } catch (error: unknown) { - logger.error( - 'Failed to close child frame', - error instanceof Error ? error : new Error(String(error)), - { - parentFrameId, - childFrameId: child.frame_id, - } - ); - } - }); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to close child frames for parent: ${parentFrameId}`, - ErrorCode.DB_QUERY_FAILED, - { - parentFrameId, - operation: 'closeChildFrames', - }, - error instanceof Error ? error : undefined - ); - } - } - - /** - * Generate digest for a frame - */ - private generateDigest(frameId: string): { - text: string; - structured: Record; - } { - const frame = this.getFrame(frameId); - const events = this.getFrameEvents(frameId); - const anchors = this.getFrameAnchors(frameId); - - if (!frame) { - throw new FrameError( - `Cannot generate digest: frame not found ${frameId}`, - ErrorCode.FRAME_NOT_FOUND, - { - frameId, - operation: 'generateDigest', - runId: this.currentRunId, - } - ); - } - - // Extract key information - const decisions = anchors.filter((a) => a.type === 'DECISION'); - const constraints = anchors.filter((a) => a.type === 'CONSTRAINT'); - const risks = anchors.filter((a) => a.type === 'RISK'); - - const toolCalls = events.filter((e) => e.event_type === 'tool_call'); - const artifacts = events.filter((e) => e.event_type === 'artifact'); - - // Generate structured digest - const structured = { - result: frame.name, - decisions: decisions.map((d) => ({ id: d.anchor_id, text: d.text })), - constraints: constraints.map((c) => ({ id: c.anchor_id, text: c.text })), - risks: risks.map((r) => ({ id: r.anchor_id, text: r.text })), - artifacts: artifacts.map((a) => ({ - kind: a.payload.kind || 'unknown', - ref: a.payload.ref, - })), - tool_calls_count: toolCalls.length, - duration_seconds: frame.closed_at - ? frame.closed_at - frame.created_at - : 0, - }; - - // Generate text summary - const text = this.generateDigestText(frame, structured, events.length); - - return { text, structured }; - } - - private generateDigestText( - frame: Frame, - structured: any, - eventCount: number - ): string { - let summary = `Completed: ${frame.name}\n`; - - if (structured.decisions.length > 0) { - summary += `\nDecisions made:\n${structured.decisions.map((d: any) => `- ${d.text}`).join('\n')}`; - } - - if (structured.constraints.length > 0) { - summary += `\nConstraints established:\n${structured.constraints.map((c: any) => `- ${c.text}`).join('\n')}`; - } - - if (structured.risks.length > 0) { - summary += `\nRisks identified:\n${structured.risks.map((r: any) => `- ${r.text}`).join('\n')}`; - } - - summary += `\nActivity: ${eventCount} events, ${structured.tool_calls_count} tool calls`; - - if (structured.duration_seconds > 0) { - summary += `, ${Math.floor(structured.duration_seconds / 60)}m ${structured.duration_seconds % 60}s duration`; - } - - return summary; - } - - /** - * Add event to current frame - */ - public addEvent( - eventType: Event['event_type'], - payload: Record, - frameId?: string - ): string { - const targetFrameId = frameId || this.getCurrentFrameId(); - if (!targetFrameId) { - throw new FrameError( - 'No active frame for event', - ErrorCode.FRAME_INVALID_STATE, - { - operation: 'addEvent', - eventType, - activeStack: this.activeStack, - } - ); - } - - const eventId = uuidv4(); - const seq = this.getNextEventSequence(targetFrameId); - - try { - this.db - .prepare( - ` - INSERT INTO events (event_id, run_id, frame_id, seq, event_type, payload) - VALUES (?, ?, ?, ?, ?, ?) - ` - ) - .run( - eventId, - this.currentRunId, - targetFrameId, - seq, - eventType, - JSON.stringify(payload) - ); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to add event to frame: ${targetFrameId}`, - ErrorCode.DB_QUERY_FAILED, - { - eventId, - frameId: targetFrameId, - eventType, - seq, - operation: 'addEvent', - }, - error instanceof Error ? error : undefined - ); - } - - return eventId; - } - - /** - * Add anchor to frame - */ - public addAnchor( - type: Anchor['type'], - text: string, - priority: number = 0, - metadata: Record = {}, - frameId?: string - ): string { - const targetFrameId = frameId || this.getCurrentFrameId(); - if (!targetFrameId) { - throw new FrameError( - 'No active frame for anchor', - ErrorCode.FRAME_INVALID_STATE, - { - operation: 'addAnchor', - anchorType: type, - text: text.substring(0, 100), - activeStack: this.activeStack, - } - ); - } - - const anchorId = uuidv4(); - - try { - this.db - .prepare( - ` - INSERT INTO anchors (anchor_id, frame_id, project_id, type, text, priority, metadata) - VALUES (?, ?, ?, ?, ?, ?, ?) - ` - ) - .run( - anchorId, - targetFrameId, - this.projectId, - type, - text, - priority, - JSON.stringify(metadata) - ); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to add anchor to frame: ${targetFrameId}`, - ErrorCode.DB_QUERY_FAILED, - { - anchorId, - frameId: targetFrameId, - anchorType: type, - operation: 'addAnchor', - }, - error instanceof Error ? error : undefined - ); - } - - return anchorId; - } - - /** - * Get hot stack context for current active frames - */ - public getHotStackContext(maxEvents: number = 20): FrameContext[] { - return this.activeStack - .map((frameId) => { - const frame = this.getFrame(frameId); - if (!frame) return null; - - return { - frameId, - header: { - goal: frame.name, - constraints: this.extractConstraints(frame.inputs), - definitions: frame.inputs.definitions, - }, - anchors: this.getFrameAnchors(frameId), - recentEvents: this.getFrameEvents(frameId, maxEvents), - activeArtifacts: this.getActiveArtifacts(frameId), - }; - }) - .filter(Boolean) as FrameContext[]; - } - - /** - * Get active frame path (root to current) - */ - public getActiveFramePath(): Frame[] { - return this.activeStack - .map((frameId) => this.getFrame(frameId)) - .filter(Boolean) as Frame[]; - } - - // Utility methods - public getCurrentFrameId(): string | undefined { - return this.activeStack[this.activeStack.length - 1]; - } - - public getStackDepth(): number { - return this.activeStack.length; - } - - /** - * Get recent frames for context sharing - */ - public async getRecentFrames(limit: number = 100): Promise { - try { - const rows = this.db - .prepare( - ` - SELECT * FROM frames - WHERE project_id = ? - ORDER BY created_at DESC - LIMIT ? - ` - ) - .all(this.projectId, limit) as any[]; - - return rows.map((row) => ({ - ...row, - frameId: row.frame_id, - runId: row.run_id, - projectId: row.project_id, - parentFrameId: row.parent_frame_id, - title: row.name, - timestamp: row.created_at, - metadata: { - tags: this.extractTagsFromFrame(row), - importance: this.calculateFrameImportance(row), - }, - data: { - inputs: JSON.parse(row.inputs || '{}'), - outputs: JSON.parse(row.outputs || '{}'), - digest: JSON.parse(row.digest_json || '{}'), - }, - inputs: JSON.parse(row.inputs || '{}'), - outputs: JSON.parse(row.outputs || '{}'), - digest_json: JSON.parse(row.digest_json || '{}'), - })); - } catch (error: unknown) { - logger.error('Failed to get recent frames', error as Error); - return []; - } - } - - /** - * Add context metadata to the current frame - */ - public async addContext(key: string, value: any): Promise { - const currentFrameId = this.getCurrentFrameId(); - if (!currentFrameId) return; - - try { - const frame = this.getFrame(currentFrameId); - if (!frame) return; - - const metadata = frame.outputs || {}; - metadata[key] = value; - - this.db - .prepare(`UPDATE frames SET outputs = ? WHERE frame_id = ?`) - .run(JSON.stringify(metadata), currentFrameId); - } catch (error: unknown) { - logger.warn('Failed to add context to frame', { error, key }); - } - } - - private extractTagsFromFrame(frame: any): string[] { - const tags: string[] = []; - - // Add type as tag - if (frame.type) tags.push(frame.type); - - // Extract tags from name - if (frame.name) { - if (frame.name.toLowerCase().includes('error')) tags.push('error'); - if (frame.name.toLowerCase().includes('fix')) tags.push('resolution'); - if (frame.name.toLowerCase().includes('decision')) tags.push('decision'); - if (frame.name.toLowerCase().includes('milestone')) - tags.push('milestone'); - } - - // Extract from digest - try { - const digest = JSON.parse(frame.digest_json || '{}'); - if (digest.tags) tags.push(...digest.tags); - } catch {} - - return [...new Set(tags)]; - } - - private calculateFrameImportance(frame: any): 'high' | 'medium' | 'low' { - // Milestones and decisions are high importance - if (frame.type === 'milestone' || frame.name?.includes('decision')) - return 'high'; - - // Errors and resolutions are medium importance - if (frame.type === 'error' || frame.type === 'resolution') return 'medium'; - - // Long-running frames are potentially important - if (frame.closed_at && frame.created_at) { - const duration = frame.closed_at - frame.created_at; - if (duration > 300) return 'medium'; // More than 5 minutes - } - - return 'low'; - } - - private getFrameDepth(frameId: string): number { - const frame = this.getFrame(frameId); - return frame?.depth || 0; - } - - public getFrame(frameId: string): Frame | undefined { - try { - const row = this.db - .prepare( - ` - SELECT * FROM frames WHERE frame_id = ? - ` - ) - .get(frameId) as any; - - if (!row) return undefined; - - return { - ...row, - inputs: JSON.parse(row.inputs || '{}'), - outputs: JSON.parse(row.outputs || '{}'), - digest_json: JSON.parse(row.digest_json || '{}'), - }; - } catch (error: unknown) { - // Log the error but return undefined instead of throwing - logger.warn(`Failed to get frame: ${frameId}`, { - error: error instanceof Error ? error.message : String(error), - frameId, - operation: 'getFrame', - }); - return undefined; - } - } - - public getFrameEvents(frameId: string, limit?: number): Event[] { - try { - const query = limit - ? `SELECT * FROM events WHERE frame_id = ? ORDER BY seq DESC LIMIT ?` - : `SELECT * FROM events WHERE frame_id = ? ORDER BY seq ASC`; - - const params = limit ? [frameId, limit] : [frameId]; - const rows = this.db.prepare(query).all(...params) as any[]; - - return rows.map((row) => ({ - ...row, - payload: JSON.parse(row.payload), - })); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to get frame events: ${frameId}`, - ErrorCode.DB_QUERY_FAILED, - { - frameId, - limit, - operation: 'getFrameEvents', - }, - error instanceof Error ? error : undefined - ); - } - } - - private getFrameAnchors(frameId: string): Anchor[] { - try { - const rows = this.db - .prepare( - ` - SELECT * FROM anchors WHERE frame_id = ? ORDER BY priority DESC, created_at ASC - ` - ) - .all(frameId) as any[]; - - return rows.map((row) => ({ - ...row, - metadata: JSON.parse(row.metadata || '{}'), - })); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to get frame anchors: ${frameId}`, - ErrorCode.DB_QUERY_FAILED, - { - frameId, - operation: 'getFrameAnchors', - }, - error instanceof Error ? error : undefined - ); - } - } - - private getNextEventSequence(frameId: string): number { - try { - const result = this.db - .prepare( - ` - SELECT MAX(seq) as max_seq FROM events WHERE frame_id = ? - ` - ) - .get(frameId) as { max_seq: number | null }; - - return (result.max_seq || 0) + 1; - } catch (error: unknown) { - throw new DatabaseError( - `Failed to get next event sequence for frame: ${frameId}`, - ErrorCode.DB_QUERY_FAILED, - { - frameId, - operation: 'getNextEventSequence', - }, - error instanceof Error ? error : undefined - ); - } - } - - private extractConstraints( - inputs: Record - ): string[] | undefined { - return inputs.constraints; - } - - private getActiveArtifacts(frameId: string): string[] { - const artifacts = this.getFrameEvents(frameId) - .filter((e) => e.event_type === 'artifact') - .map((e) => e.payload.ref) - .filter(Boolean); - - return artifacts; - } - - /** - * Detect if setting a parent frame would create a cycle in the frame hierarchy. - * Returns the cycle path if detected, or null if no cycle. - * @param childFrameId - The frame that would be the child - * @param parentFrameId - The proposed parent frame - * @returns Array of frame IDs forming the cycle, or null if no cycle - */ - private detectCycle( - childFrameId: string, - parentFrameId: string - ): string[] | null { - const visited = new Set(); - const path: string[] = []; - - // Start from the proposed parent and traverse up the hierarchy - let currentId: string | undefined = parentFrameId; - - while (currentId) { - // If we've seen this frame before, we have a cycle - if (visited.has(currentId)) { - // Build the cycle path - const cycleStart = path.indexOf(currentId); - return path.slice(cycleStart).concat(currentId); - } - - // If the current frame is the child we're trying to add, it's a cycle - if (currentId === childFrameId) { - return path.concat([currentId, childFrameId]); - } - - visited.add(currentId); - path.push(currentId); - - // Move to the parent of current frame - const frame = this.getFrame(currentId); - if (!frame) { - // Frame not found, no cycle possible through this path - break; - } - currentId = frame.parent_frame_id; - - // Safety check: if we've traversed too many levels, something is wrong - if (path.length > this.maxFrameDepth) { - throw new FrameError( - `Frame hierarchy traversal exceeded maximum depth during cycle detection`, - ErrorCode.FRAME_STACK_OVERFLOW, - { - depth: path.length, - maxDepth: this.maxFrameDepth, - path, - } - ); - } - } - - return null; // No cycle detected - } - - /** - * Update parent frame of an existing frame (with cycle detection) - * @param frameId - The frame to update - * @param newParentFrameId - The new parent frame ID - */ - public updateParentFrame( - frameId: string, - newParentFrameId: string | null - ): void { - // Check if frame exists - const frame = this.getFrame(frameId); - if (!frame) { - throw new FrameError( - `Frame not found: ${frameId}`, - ErrorCode.FRAME_NOT_FOUND, - { frameId } - ); - } - - // If setting a parent, check for cycles - if (newParentFrameId) { - const cycle = this.detectCycle(frameId, newParentFrameId); - if (cycle) { - throw new FrameError( - `Cannot set parent: would create circular reference`, - ErrorCode.FRAME_CYCLE_DETECTED, - { - frameId, - newParentFrameId, - cycle, - currentParentId: frame.parent_frame_id, - } - ); - } - - // Check depth after parent change - const newParentFrame = this.getFrame(newParentFrameId); - if (newParentFrame) { - const newDepth = newParentFrame.depth + 1; - if (newDepth > this.maxFrameDepth) { - throw new FrameError( - `Cannot set parent: would exceed maximum frame depth`, - ErrorCode.FRAME_STACK_OVERFLOW, - { - frameId, - newParentFrameId, - newDepth, - maxDepth: this.maxFrameDepth, - } - ); - } - } - } - - try { - // Update the parent frame - this.db - .prepare(`UPDATE frames SET parent_frame_id = ? WHERE frame_id = ?`) - .run(newParentFrameId, frameId); - - logger.info('Updated parent frame', { - frameId, - oldParentId: frame.parent_frame_id, - newParentId: newParentFrameId, - }); - } catch (error: unknown) { - throw new DatabaseError( - `Failed to update parent frame`, - ErrorCode.DB_UPDATE_FAILED, - { - frameId, - newParentFrameId, - operation: 'updateParentFrame', - }, - error instanceof Error ? error : undefined - ); - } - } -} diff --git a/src/core/context/frame-stack.ts b/src/core/context/frame-stack.ts index 35414aa..da4276c 100644 --- a/src/core/context/frame-stack.ts +++ b/src/core/context/frame-stack.ts @@ -7,9 +7,11 @@ import { Frame, FrameContext, FrameType } from './frame-types.js'; import { FrameDatabase } from './frame-database.js'; import { logger } from '../monitoring/logger.js'; import { FrameError, ErrorCode } from '../errors/index.js'; +import { FrameQueryMode } from '../session/index.js'; export class FrameStack { private activeStack: string[] = []; + private queryMode: FrameQueryMode = FrameQueryMode.PROJECT_ACTIVE; constructor( private frameDb: FrameDatabase, @@ -185,6 +187,39 @@ export class FrameStack { logger.info('Cleared frame stack', { previousDepth }); } + /** + * Set query mode and reinitialize stack + */ + setQueryMode(mode: FrameQueryMode): void { + this.queryMode = mode; + // Reinitialize with new query mode + this.initialize().catch((error) => { + logger.warn('Failed to reinitialize stack with new query mode', { + mode, + error, + }); + }); + } + + /** + * Remove a specific frame from the stack without popping frames above it + */ + removeFrame(frameId: string): boolean { + const index = this.activeStack.indexOf(frameId); + if (index === -1) { + return false; + } + + this.activeStack.splice(index, 1); + + logger.debug('Removed frame from stack', { + frameId, + stackDepth: this.activeStack.length, + }); + + return true; + } + /** * Validate stack consistency */ diff --git a/src/core/context/incremental-gc.ts b/src/core/context/incremental-gc.ts index 0402665..ff8905a 100644 --- a/src/core/context/incremental-gc.ts +++ b/src/core/context/incremental-gc.ts @@ -5,7 +5,8 @@ * with generational aging and priority-based collection. */ -import { Frame, FrameManager } from './frame-manager.js'; +import { FrameManager } from './index.js'; +import type { Frame } from './index.js'; import { Logger } from '../monitoring/logger.js'; interface GCConfig { diff --git a/src/core/context/index.ts b/src/core/context/index.ts index 55fe4e6..9cc6ad6 100644 --- a/src/core/context/index.ts +++ b/src/core/context/index.ts @@ -6,8 +6,8 @@ // Export refactored components as primary export { RefactoredFrameManager as FrameManager } from './refactored-frame-manager.js'; -// Export types -export { +// Export types (type-only, no runtime value) +export type { Frame, FrameContext, Anchor, @@ -24,6 +24,10 @@ export { FrameDatabase } from './frame-database.js'; export { FrameStack } from './frame-stack.js'; export { FrameDigestGenerator } from './frame-digest.js'; -// Re-export from old frame-manager for backwards compatibility -// This allows existing code to continue working without changes -export { FrameManager as LegacyFrameManager } from './frame-manager.js'; +// Export lifecycle hooks for external integrations +export { + frameLifecycleHooks, + type FrameCloseData, + type FrameCloseHook, + type FrameCreateHook, +} from './frame-lifecycle-hooks.js'; diff --git a/src/core/context/refactored-frame-manager.ts b/src/core/context/refactored-frame-manager.ts index 6225f8d..c309308 100644 --- a/src/core/context/refactored-frame-manager.ts +++ b/src/core/context/refactored-frame-manager.ts @@ -16,6 +16,7 @@ import { } from '../errors/index.js'; import { retry, withTimeout } from '../errors/recovery.js'; import { sessionManager, FrameQueryMode } from '../session/index.js'; +import { frameLifecycleHooks } from './frame-lifecycle-hooks.js'; // Constants for frame validation const MAX_FRAME_DEPTH = 100; // Maximum allowed frame depth @@ -308,6 +309,15 @@ export class RefactoredFrameManager { // Close all child frames recursively this.closeChildFrames(targetFrameId); + // Trigger lifecycle hooks (fire and forget) + const events = this.frameDb.getFrameEvents(targetFrameId); + const anchors = this.frameDb.getFrameAnchors(targetFrameId); + frameLifecycleHooks + .triggerClose({ frame: { ...frame, state: 'closed' }, events, anchors }) + .catch(() => { + // Silently ignore errors - hooks are non-critical + }); + logger.info('Closed frame', { frameId: targetFrameId, name: frame.name, @@ -637,8 +647,18 @@ export class RefactoredFrameManager { ); } - // If setting a parent, check for cycles + // If setting a parent, validate and check for cycles if (newParentFrameId) { + // Verify the new parent exists + const newParentFrame = this.frameDb.getFrame(newParentFrameId); + if (!newParentFrame) { + throw new FrameError( + `Parent frame not found: ${newParentFrameId}`, + ErrorCode.FRAME_NOT_FOUND, + { frameId, newParentFrameId } + ); + } + const cycle = this.detectCycle(frameId, newParentFrameId); if (cycle) { throw new FrameError( @@ -654,27 +674,34 @@ export class RefactoredFrameManager { } // Check depth after parent change + const newDepth = newParentFrame.depth + 1; + if (newDepth > this.maxFrameDepth) { + throw new FrameError( + `Cannot set parent: would exceed maximum frame depth`, + ErrorCode.FRAME_STACK_OVERFLOW, + { + frameId, + newParentFrameId, + newDepth, + maxDepth: this.maxFrameDepth, + } + ); + } + } + + // Calculate new depth based on parent + let newDepth = 0; + if (newParentFrameId) { const newParentFrame = this.frameDb.getFrame(newParentFrameId); if (newParentFrame) { - const newDepth = newParentFrame.depth + 1; - if (newDepth > this.maxFrameDepth) { - throw new FrameError( - `Cannot set parent: would exceed maximum frame depth`, - ErrorCode.FRAME_STACK_OVERFLOW, - { - frameId, - newParentFrameId, - newDepth, - maxDepth: this.maxFrameDepth, - } - ); - } + newDepth = newParentFrame.depth + 1; } } - // Update the frame's parent (this would need to be implemented in FrameDatabase) + // Update the frame's parent and depth this.frameDb.updateFrame(frameId, { parent_frame_id: newParentFrameId, + depth: newDepth, }); logger.info('Updated parent frame', { @@ -754,10 +781,145 @@ export class RefactoredFrameManager { warnings, }; } + + /** + * Set query mode for frame retrieval + */ + setQueryMode(mode: FrameQueryMode): void { + this.queryMode = mode; + // Reinitialize stack with new query mode + this.frameStack.setQueryMode(mode); + } + + /** + * Get recent frames for context sharing + */ + async getRecentFrames(limit: number = 100): Promise { + try { + const frames = this.frameDb.getFramesByProject(this.projectId); + + // Sort by created_at descending and limit + return frames + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) + .slice(0, limit) + .map((frame) => ({ + ...frame, + // Add compatibility fields + frameId: frame.frame_id, + runId: frame.run_id, + projectId: frame.project_id, + parentFrameId: frame.parent_frame_id, + title: frame.name, + timestamp: frame.created_at, + metadata: { + tags: this.extractTagsFromFrame(frame), + importance: this.calculateFrameImportance(frame), + }, + data: { + inputs: frame.inputs, + outputs: frame.outputs, + digest: frame.digest_json, + }, + })); + } catch (error: unknown) { + logger.error('Failed to get recent frames', error as Error); + return []; + } + } + + /** + * Add context metadata to the current frame + */ + async addContext(key: string, value: any): Promise { + const currentFrameId = this.frameStack.getCurrentFrameId(); + if (!currentFrameId) return; + + try { + const frame = this.frameDb.getFrame(currentFrameId); + if (!frame) return; + + const metadata = frame.outputs || {}; + metadata[key] = value; + + this.frameDb.updateFrame(currentFrameId, { + outputs: metadata, + }); + } catch (error: unknown) { + logger.warn('Failed to add context to frame', { error, key }); + } + } + + /** + * Delete a frame completely from the database (used in handoffs) + */ + deleteFrame(frameId: string): void { + try { + // Remove from active stack if present + this.frameStack.removeFrame(frameId); + + // Delete the frame and related data (cascades via FrameDatabase) + this.frameDb.deleteFrame(frameId); + + logger.debug('Deleted frame completely', { frameId }); + } catch (error: unknown) { + logger.error('Failed to delete frame', { frameId, error }); + throw error; + } + } + + /** + * Extract tags from frame for categorization + */ + private extractTagsFromFrame(frame: Frame): string[] { + const tags: string[] = []; + + if (frame.type) tags.push(frame.type); + + if (frame.name) { + const nameLower = frame.name.toLowerCase(); + if (nameLower.includes('error')) tags.push('error'); + if (nameLower.includes('fix')) tags.push('resolution'); + if (nameLower.includes('decision')) tags.push('decision'); + if (nameLower.includes('milestone')) tags.push('milestone'); + } + + try { + if (frame.digest_json && typeof frame.digest_json === 'object') { + const digest = frame.digest_json as Record; + if (Array.isArray(digest.tags)) { + tags.push(...(digest.tags as string[])); + } + } + } catch { + // Ignore parse errors + } + + return [...new Set(tags)]; + } + + /** + * Calculate frame importance for prioritization + */ + private calculateFrameImportance(frame: Frame): 'high' | 'medium' | 'low' { + if (frame.type === 'milestone' || frame.name?.includes('decision')) { + return 'high'; + } + + if (frame.type === 'error' || frame.type === 'resolution') { + return 'medium'; + } + + if (frame.closed_at && frame.created_at) { + const duration = frame.closed_at - frame.created_at; + if (duration > 300) return 'medium'; + } + + return 'low'; + } } -// Re-export types for compatibility -export { +// Re-export types for compatibility (type-only, no runtime value) +export type { Frame, FrameContext, Anchor, diff --git a/src/core/context/shared-context-layer.ts b/src/core/context/shared-context-layer.ts index 9b6b0d7..84e4ea9 100644 --- a/src/core/context/shared-context-layer.ts +++ b/src/core/context/shared-context-layer.ts @@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import * as fs from 'fs/promises'; import * as path from 'path'; import { sessionManager } from '../session/session-manager.js'; -import type { Frame } from '../frame-manager/frame-manager.js'; +import type { Frame } from './frame-types.js'; // Type-safe environment variable access diff --git a/src/core/context/stack-merge-resolver.ts b/src/core/context/stack-merge-resolver.ts index ed6bb20..54ef58c 100644 --- a/src/core/context/stack-merge-resolver.ts +++ b/src/core/context/stack-merge-resolver.ts @@ -3,7 +3,7 @@ * Advanced conflict resolution for frame merging between individual and shared stacks */ -import type { Frame, Event, Anchor } from './frame-manager.js'; +import type { Frame, Event, Anchor } from './frame-types.js'; import { DualStackManager, type StackSyncResult, diff --git a/src/core/database/database-adapter.ts b/src/core/database/database-adapter.ts index f6ae62f..a0a4bde 100644 --- a/src/core/database/database-adapter.ts +++ b/src/core/database/database-adapter.ts @@ -4,7 +4,7 @@ * Supports SQLite (current) and ParadeDB (new) with seamless migration */ -import type { Frame, Event, Anchor } from '../context/frame-manager.js'; +import type { Frame, Event, Anchor } from '../context/index.js'; export interface QueryOptions { limit?: number; diff --git a/src/core/database/paradedb-adapter.ts b/src/core/database/paradedb-adapter.ts index d2e7358..6366c10 100644 --- a/src/core/database/paradedb-adapter.ts +++ b/src/core/database/paradedb-adapter.ts @@ -13,7 +13,7 @@ import { BulkOperation, DatabaseStats, } from './database-adapter.js'; -import type { Frame, Event, Anchor } from '../context/frame-manager.js'; +import type { Frame, Event, Anchor } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; import { DatabaseError, ErrorCode, ValidationError } from '../errors/index.js'; diff --git a/src/core/database/query-router.ts b/src/core/database/query-router.ts index 3b39a54..6f7539a 100644 --- a/src/core/database/query-router.ts +++ b/src/core/database/query-router.ts @@ -4,7 +4,7 @@ */ import { DatabaseAdapter } from './database-adapter.js'; -import type { Frame } from '../context/frame-manager.js'; +import type { Frame } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; import { DatabaseError, ErrorCode } from '../errors/index.js'; import { EventEmitter } from 'events'; diff --git a/src/core/database/sqlite-adapter.ts b/src/core/database/sqlite-adapter.ts index 4434f88..b0584d4 100644 --- a/src/core/database/sqlite-adapter.ts +++ b/src/core/database/sqlite-adapter.ts @@ -16,7 +16,7 @@ import { VersionResult, FrameRow, } from './database-adapter.js'; -import type { Frame, Event, Anchor } from '../context/frame-manager.js'; +import type { Frame, Event, Anchor } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; import { DatabaseError, ErrorCode, ValidationError } from '../errors/index.js'; import * as fs from 'fs/promises'; diff --git a/src/core/digest/__tests__/frame-digest-integration.test.ts b/src/core/digest/__tests__/frame-digest-integration.test.ts index 44a6d8f..641c4bd 100644 --- a/src/core/digest/__tests__/frame-digest-integration.test.ts +++ b/src/core/digest/__tests__/frame-digest-integration.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import Database from 'better-sqlite3'; -import { FrameManager } from '../../context/frame-manager.js'; +import { FrameManager } from '../../context/index.js'; import { enhanceFrameManagerWithDigest, FrameDigestIntegration, diff --git a/src/core/digest/frame-digest-integration.ts b/src/core/digest/frame-digest-integration.ts index b8b9e58..9ec67d6 100644 --- a/src/core/digest/frame-digest-integration.ts +++ b/src/core/digest/frame-digest-integration.ts @@ -9,7 +9,7 @@ import { Frame, Event, Anchor, -} from '../context/frame-manager.js'; +} from '../context/index.js'; import { EnhancedHybridDigestGenerator } from './enhanced-hybrid-digest.js'; import { DigestInput, DigestLLMProvider } from './types.js'; import { logger } from '../monitoring/logger.js'; diff --git a/src/core/digest/hybrid-digest-generator.ts b/src/core/digest/hybrid-digest-generator.ts index e95e133..4724932 100644 --- a/src/core/digest/hybrid-digest-generator.ts +++ b/src/core/digest/hybrid-digest-generator.ts @@ -19,7 +19,7 @@ import { ErrorInfo, DEFAULT_DIGEST_CONFIG, } from './types.js'; -import { Frame, Anchor, Event } from '../context/frame-manager.js'; +import { Frame, Anchor, Event } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; /** diff --git a/src/core/digest/types.ts b/src/core/digest/types.ts index ec8a699..7ed11ec 100644 --- a/src/core/digest/types.ts +++ b/src/core/digest/types.ts @@ -3,7 +3,7 @@ * 80% deterministic extraction, 20% AI-generated review/insights */ -import { Frame, Anchor, Event } from '../context/frame-manager.js'; +import { Frame, Anchor, Event } from '../context/index.js'; /** * Deterministic fields extracted directly from frame data (60%) diff --git a/src/core/errors/index.ts b/src/core/errors/index.ts index d86c8c1..5a9ead6 100644 --- a/src/core/errors/index.ts +++ b/src/core/errors/index.ts @@ -4,7 +4,7 @@ */ export enum ErrorCode { - // Database errors (1000-1999) + // Database errors (DB_*) DB_CONNECTION_FAILED = 'DB_001', DB_QUERY_FAILED = 'DB_002', DB_TRANSACTION_FAILED = 'DB_003', @@ -14,8 +14,9 @@ export enum ErrorCode { DB_INSERT_FAILED = 'DB_007', DB_UPDATE_FAILED = 'DB_008', DB_DELETE_FAILED = 'DB_009', + DB_CORRUPTION = 'DB_010', - // Frame errors (2000-2999) + // Frame errors (FRAME_*) FRAME_NOT_FOUND = 'FRAME_001', FRAME_INVALID_STATE = 'FRAME_002', FRAME_PARENT_NOT_FOUND = 'FRAME_003', @@ -25,36 +26,36 @@ export enum ErrorCode { FRAME_INVALID_INPUT = 'FRAME_007', FRAME_STACK_OVERFLOW = 'FRAME_008', - // Task errors (3000-3999) + // Task errors (TASK_*) TASK_NOT_FOUND = 'TASK_001', TASK_INVALID_STATE = 'TASK_002', TASK_DEPENDENCY_CONFLICT = 'TASK_003', TASK_CIRCULAR_DEPENDENCY = 'TASK_004', - // Integration errors (4000-4999) + // Integration errors (LINEAR_*) LINEAR_AUTH_FAILED = 'LINEAR_001', LINEAR_API_ERROR = 'LINEAR_002', LINEAR_SYNC_FAILED = 'LINEAR_003', LINEAR_WEBHOOK_FAILED = 'LINEAR_004', - // MCP errors (5000-5999) + // MCP errors (MCP_*) MCP_TOOL_NOT_FOUND = 'MCP_001', MCP_INVALID_PARAMS = 'MCP_002', MCP_EXECUTION_FAILED = 'MCP_003', MCP_RATE_LIMITED = 'MCP_004', - // Project errors (6000-6999) + // Project errors (PROJECT_*) PROJECT_NOT_FOUND = 'PROJECT_001', PROJECT_INVALID_PATH = 'PROJECT_002', PROJECT_GIT_ERROR = 'PROJECT_003', - // Validation errors (7000-7999) + // Validation errors (VAL_*) VALIDATION_FAILED = 'VAL_001', INVALID_INPUT = 'VAL_002', MISSING_REQUIRED_FIELD = 'VAL_003', TYPE_MISMATCH = 'VAL_004', - // System errors (8000-8999) + // System errors (SYS_*) INITIALIZATION_ERROR = 'SYS_001', NOT_FOUND = 'SYS_002', INTERNAL_ERROR = 'SYS_003', @@ -63,8 +64,28 @@ export enum ErrorCode { RESOURCE_EXHAUSTED = 'SYS_006', SERVICE_UNAVAILABLE = 'SYS_007', SYSTEM_INIT_FAILED = 'SYS_008', + UNKNOWN_ERROR = 'SYS_009', + OPERATION_TIMEOUT = 'SYS_010', - // Collaboration errors (9000-9999) + // Authentication errors (AUTH_*) + AUTH_FAILED = 'AUTH_001', + TOKEN_EXPIRED = 'AUTH_002', + INVALID_CREDENTIALS = 'AUTH_003', + + // File system errors (FS_*) + FILE_NOT_FOUND = 'FS_001', + DISK_FULL = 'FS_002', + + // Git errors (GIT_*) + NOT_GIT_REPO = 'GIT_001', + GIT_COMMAND_FAILED = 'GIT_002', + INVALID_BRANCH = 'GIT_003', + + // Network errors (NET_*) + NETWORK_ERROR = 'NET_001', + API_ERROR = 'NET_002', + + // Collaboration errors (COLLAB_*) STACK_CONTEXT_NOT_FOUND = 'COLLAB_001', HANDOFF_REQUEST_EXPIRED = 'COLLAB_002', MERGE_CONFLICT_UNRESOLVABLE = 'COLLAB_003', @@ -384,3 +405,302 @@ export function createErrorHandler(defaultContext: ErrorContext) { ); }; } + +/** + * User-friendly error messages for each error code + */ +export function getUserFriendlyMessage(code: ErrorCode): string { + switch (code) { + // Auth errors + case ErrorCode.AUTH_FAILED: + return 'Authentication failed. Please check your credentials and try again.'; + case ErrorCode.TOKEN_EXPIRED: + return 'Your session has expired. Please log in again.'; + case ErrorCode.INVALID_CREDENTIALS: + return 'Invalid credentials provided. Please check and try again.'; + + // File system errors + case ErrorCode.FILE_NOT_FOUND: + return 'The requested file or directory was not found.'; + case ErrorCode.PERMISSION_DENIED: + return 'Permission denied. Please check file permissions or run with appropriate privileges.'; + case ErrorCode.DISK_FULL: + return 'Insufficient disk space. Please free up space and try again.'; + + // Git errors + case ErrorCode.NOT_GIT_REPO: + return 'This command requires a git repository. Please run it from within a git repository.'; + case ErrorCode.GIT_COMMAND_FAILED: + return 'Git operation failed. Please ensure your repository is in a valid state.'; + case ErrorCode.INVALID_BRANCH: + return 'Invalid branch specified. Please check the branch name and try again.'; + + // Database errors + case ErrorCode.DB_CONNECTION_FAILED: + return 'Database connection failed. Please try again or contact support if the issue persists.'; + case ErrorCode.DB_QUERY_FAILED: + return 'Database query failed. Please try again.'; + case ErrorCode.DB_CORRUPTION: + return 'Database appears to be corrupted. Please contact support.'; + + // Network errors + case ErrorCode.NETWORK_ERROR: + return 'Network error. Please check your internet connection and try again.'; + case ErrorCode.API_ERROR: + return 'API request failed. Please try again later.'; + case ErrorCode.OPERATION_TIMEOUT: + return 'The operation timed out. Please try again.'; + + // Validation errors + case ErrorCode.INVALID_INPUT: + return 'Invalid input provided. Please check your command and try again.'; + case ErrorCode.VALIDATION_FAILED: + return 'Validation failed. Please check your input and try again.'; + case ErrorCode.MISSING_REQUIRED_FIELD: + return 'A required field is missing. Please provide all required information.'; + + // System errors + case ErrorCode.CONFIGURATION_ERROR: + return 'Configuration error. Please check your settings.'; + case ErrorCode.SERVICE_UNAVAILABLE: + return 'Service is temporarily unavailable. Please try again later.'; + + // Default + default: + return 'An unexpected error occurred. Please try again or contact support.'; + } +} + +/** + * ErrorHandler provides utilities for handling errors in CLI context + */ +export class ErrorHandler { + private static retryMap = new Map(); + private static readonly MAX_RETRIES = 3; + + /** + * Handle an error and exit the process + */ + static handle(error: unknown, operation: string): never { + if (error instanceof StackMemoryError) { + const userMessage = getUserFriendlyMessage(error.code); + console.error(`❌ ${userMessage}`); + + if (error.isRetryable) { + console.error('💡 This error may be recoverable. Please try again.'); + } + + process.exit(1); + } + + if (error instanceof Error) { + let stackMemoryError: StackMemoryError; + + if ('code' in error && typeof error.code === 'string') { + stackMemoryError = ErrorHandler.fromNodeError( + error as NodeJS.ErrnoException, + { operation } + ); + } else { + stackMemoryError = wrapError(error, error.message, ErrorCode.OPERATION_FAILED, { + operation, + }); + } + + const userMessage = getUserFriendlyMessage(stackMemoryError.code); + console.error(`❌ ${userMessage}`); + + if (stackMemoryError.isRetryable) { + console.error('💡 This error may be recoverable. Please try again.'); + } + + process.exit(1); + } + + // Unknown error type + console.error('❌ An unexpected error occurred.'); + process.exit(1); + } + + /** + * Convert Node.js error to StackMemoryError + */ + static fromNodeError( + nodeError: NodeJS.ErrnoException, + context: ErrorContext = {} + ): StackMemoryError { + const code = nodeError.code; + + switch (code) { + case 'ENOENT': + return new SystemError( + `File or directory not found: ${nodeError.path}`, + ErrorCode.FILE_NOT_FOUND, + { ...context, path: nodeError.path }, + nodeError + ); + + case 'EACCES': + case 'EPERM': + return new SystemError( + `Permission denied: ${nodeError.path}`, + ErrorCode.PERMISSION_DENIED, + { ...context, path: nodeError.path }, + nodeError + ); + + case 'ENOSPC': + return new SystemError( + 'No space left on device', + ErrorCode.DISK_FULL, + context, + nodeError + ); + + case 'ETIMEDOUT': + return new SystemError( + 'Operation timed out', + ErrorCode.OPERATION_TIMEOUT, + context, + nodeError + ); + + default: + return new SystemError( + nodeError.message, + ErrorCode.UNKNOWN_ERROR, + { ...context, nodeErrorCode: code }, + nodeError + ); + } + } + + /** + * Safely execute an operation with optional fallback + */ + static async safeExecute( + operation: () => Promise | T, + operationName: string, + fallback?: T + ): Promise { + try { + return await operation(); + } catch (error: unknown) { + if (fallback !== undefined) { + return fallback; + } + ErrorHandler.handle(error, operationName); + } + } + + /** + * Execute with automatic retry and exponential backoff + */ + static async withRetry( + operation: () => Promise | T, + operationName: string, + maxRetries: number = ErrorHandler.MAX_RETRIES + ): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const result = await operation(); + ErrorHandler.retryMap.delete(operationName); + return result; + } catch (error: unknown) { + lastError = error; + + if (error instanceof StackMemoryError && !error.isRetryable) { + ErrorHandler.handle(error, operationName); + } + + if (attempt === maxRetries) { + break; + } + + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + ErrorHandler.handle(lastError, `${operationName} (after ${maxRetries} attempts)`); + } + + /** + * Create a circuit breaker for an operation + */ + static createCircuitBreaker( + operation: () => Promise | T, + operationName: string, + threshold: number = 5 + ) { + let failures = 0; + let lastFailure = 0; + const resetTimeout = 30000; + + return async (): Promise => { + const now = Date.now(); + + if (now - lastFailure > resetTimeout) { + failures = 0; + } + + if (failures >= threshold) { + throw new SystemError( + `Circuit breaker open for '${operationName}'`, + ErrorCode.SERVICE_UNAVAILABLE, + { operationName, failures, threshold } + ); + } + + try { + const result = await operation(); + failures = 0; + return result; + } catch (error: unknown) { + failures++; + lastFailure = now; + throw error; + } + }; + } +} + +/** + * Validation utilities + */ +export const validateInput = ( + value: unknown, + name: string, + validator: (val: unknown) => boolean +): asserts value is NonNullable => { + if (!validator(value)) { + throw new ValidationError( + `Invalid ${name}: ${String(value)}`, + ErrorCode.INVALID_INPUT, + { name, value } + ); + } +}; + +export const validateEmail = (email: string): asserts email is string => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email) || email.length > 254) { + throw new ValidationError( + `Invalid email format: ${email}`, + ErrorCode.INVALID_INPUT, + { email } + ); + } +}; + +export const validatePath = (filePath: string): asserts filePath is string => { + if (!filePath || filePath.includes('..') || filePath.includes('\0')) { + throw new ValidationError( + `Invalid path: ${filePath}`, + ErrorCode.INVALID_INPUT, + { path: filePath } + ); + } +}; diff --git a/src/core/frame/workflow-templates.ts b/src/core/frame/workflow-templates.ts index b5d83c5..1207b65 100644 --- a/src/core/frame/workflow-templates.ts +++ b/src/core/frame/workflow-templates.ts @@ -6,8 +6,7 @@ * and enforces completion gates between transitions */ -import { Frame } from '../types'; -import { FrameManager } from './frame-manager'; +import { Frame, FrameManager } from '../context/index.js'; export interface WorkflowPhase { name: string; diff --git a/src/core/merge/__tests__/conflict-scenarios.test.ts b/src/core/merge/__tests__/conflict-scenarios.test.ts index f1f786d..8f1b619 100644 --- a/src/core/merge/__tests__/conflict-scenarios.test.ts +++ b/src/core/merge/__tests__/conflict-scenarios.test.ts @@ -9,7 +9,7 @@ import { ConflictDetector } from '../conflict-detector.js'; import { StackDiffVisualizer } from '../stack-diff.js'; import { ResolutionEngine, ResolutionContext } from '../resolution-engine.js'; import { FrameStack, MergeConflict, TeamVote } from '../types.js'; -import { Frame, Event } from '../../context/frame-manager.js'; +import { Frame, Event } from '../../context/index.js'; // Test data factories function createMockFrame(overrides?: Partial): Frame { diff --git a/src/core/merge/conflict-detector.ts b/src/core/merge/conflict-detector.ts index fd10663..832f808 100644 --- a/src/core/merge/conflict-detector.ts +++ b/src/core/merge/conflict-detector.ts @@ -10,7 +10,7 @@ import { ParallelSolution, DecisionConflict, } from './types.js'; -import { Frame, Event } from '../context/frame-manager.js'; +import { Frame, Event } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; export class ConflictDetector { diff --git a/src/core/merge/resolution-engine.ts b/src/core/merge/resolution-engine.ts index f8e8608..036ceef 100644 --- a/src/core/merge/resolution-engine.ts +++ b/src/core/merge/resolution-engine.ts @@ -13,7 +13,7 @@ import { MergeResult, NotificationResult, } from './types.js'; -import { Frame } from '../context/frame-manager.js'; +import { Frame } from '../context/index.js'; import { ConflictDetector } from './conflict-detector.js'; import { StackDiffVisualizer } from './stack-diff.js'; import { logger } from '../monitoring/logger.js'; diff --git a/src/core/merge/stack-diff.ts b/src/core/merge/stack-diff.ts index 3c13f78..531d813 100644 --- a/src/core/merge/stack-diff.ts +++ b/src/core/merge/stack-diff.ts @@ -11,7 +11,7 @@ import { DiffEdge, MergeConflict, } from './types.js'; -import { Frame } from '../context/frame-manager.js'; +import { Frame } from '../context/index.js'; import { ConflictDetector } from './conflict-detector.js'; export interface VisualMarker { diff --git a/src/core/merge/types.ts b/src/core/merge/types.ts index 3844e8b..e71fab1 100644 --- a/src/core/merge/types.ts +++ b/src/core/merge/types.ts @@ -3,7 +3,7 @@ * STA-101: Stack Merge Conflict Resolution */ -import { Frame, Event } from '../context/frame-manager.js'; +import { Frame, Event } from '../context/index.js'; export interface MergeConflict { id: string; diff --git a/src/core/monitoring/error-handler.ts b/src/core/monitoring/error-handler.ts index 3169a79..b096160 100644 --- a/src/core/monitoring/error-handler.ts +++ b/src/core/monitoring/error-handler.ts @@ -1,359 +1,42 @@ /** - * Comprehensive error handling for StackMemory CLI + * Error handling re-exports for backwards compatibility + * + * All error handling is now consolidated in src/core/errors/index.ts + * This module re-exports for code that imports from monitoring/error-handler */ -import { logger } from './logger.js'; - -export enum ErrorCode { - // Authentication errors - AUTH_FAILED = 'AUTH_FAILED', - TOKEN_EXPIRED = 'TOKEN_EXPIRED', - INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', - - // File system errors - FILE_NOT_FOUND = 'FILE_NOT_FOUND', - PERMISSION_DENIED = 'PERMISSION_DENIED', - DISK_FULL = 'DISK_FULL', - - // Git operation errors - NOT_GIT_REPO = 'NOT_GIT_REPO', - GIT_COMMAND_FAILED = 'GIT_COMMAND_FAILED', - INVALID_BRANCH = 'INVALID_BRANCH', - - // Database errors - DB_CONNECTION_FAILED = 'DB_CONNECTION_FAILED', - DB_QUERY_FAILED = 'DB_QUERY_FAILED', - DB_CORRUPTION = 'DB_CORRUPTION', - - // Network errors - NETWORK_ERROR = 'NETWORK_ERROR', - API_ERROR = 'API_ERROR', - TIMEOUT = 'TIMEOUT', - - // Validation errors - INVALID_INPUT = 'INVALID_INPUT', - VALIDATION_FAILED = 'VALIDATION_FAILED', - - // General errors - UNKNOWN_ERROR = 'UNKNOWN_ERROR', - OPERATION_FAILED = 'OPERATION_FAILED', - CONFIGURATION_ERROR = 'CONFIGURATION_ERROR', -} - -export class StackMemoryError extends Error { - public readonly code: ErrorCode; - public readonly context: Record; - public readonly userMessage: string; - public readonly recoverable: boolean; - - constructor( - code: ErrorCode, - message: string, - userMessage?: string, - context: Record = {}, - recoverable: boolean = false, - cause?: Error - ) { - super(message); - this.name = 'StackMemoryError'; - this.code = code; - this.context = context; - this.userMessage = userMessage || this.getDefaultUserMessage(code); - this.recoverable = recoverable; - - if (cause && Error.captureStackTrace) { - Error.captureStackTrace(this, StackMemoryError); - } - - // Log the error - logger.error(message, cause, { - code, - context, - recoverable, - userMessage: this.userMessage, - }); - } - - private getDefaultUserMessage(code: ErrorCode): string { - switch (code) { - case ErrorCode.AUTH_FAILED: - return 'Authentication failed. Please check your credentials and try again.'; - case ErrorCode.NOT_GIT_REPO: - return 'This command requires a git repository. Please run it from within a git repository.'; - case ErrorCode.PERMISSION_DENIED: - return 'Permission denied. Please check file permissions or run with appropriate privileges.'; - case ErrorCode.NETWORK_ERROR: - return 'Network error. Please check your internet connection and try again.'; - case ErrorCode.INVALID_INPUT: - return 'Invalid input provided. Please check your command and try again.'; - case ErrorCode.DB_CONNECTION_FAILED: - return 'Database connection failed. Please try again or contact support if the issue persists.'; - case ErrorCode.GIT_COMMAND_FAILED: - return 'Git operation failed. Please ensure your repository is in a valid state.'; - default: - return 'An unexpected error occurred. Please try again or contact support.'; - } - } - - static fromNodeError( - nodeError: NodeJS.ErrnoException, - context: Record = {} - ): StackMemoryError { - const code = nodeError.code; - - switch (code) { - case 'ENOENT': - return new StackMemoryError( - ErrorCode.FILE_NOT_FOUND, - `File or directory not found: ${nodeError.path}`, - 'The requested file or directory was not found.', - { ...context, path: nodeError.path }, - false, - nodeError - ); - - case 'EACCES': - case 'EPERM': - return new StackMemoryError( - ErrorCode.PERMISSION_DENIED, - `Permission denied: ${nodeError.path}`, - 'Permission denied. Please check file permissions.', - { ...context, path: nodeError.path }, - true, - nodeError - ); - - case 'ENOSPC': - return new StackMemoryError( - ErrorCode.DISK_FULL, - 'No space left on device', - 'Insufficient disk space. Please free up space and try again.', - context, - true, - nodeError - ); - - case 'ETIMEDOUT': - return new StackMemoryError( - ErrorCode.TIMEOUT, - 'Operation timed out', - 'The operation timed out. Please try again.', - context, - true, - nodeError - ); - - default: - return new StackMemoryError( - ErrorCode.UNKNOWN_ERROR, - nodeError.message, - 'An unexpected system error occurred.', - { ...context, nodeErrorCode: code }, - false, - nodeError - ); - } - } -} - -export class ErrorHandler { - private static retryMap = new Map(); - private static readonly MAX_RETRIES = 3; - - static handle(error: unknown, operation: string): never { - if (error instanceof StackMemoryError) { - // Already a well-formed StackMemory error - console.error(`❌ ${error.userMessage}`); - - if (error.recoverable) { - console.error('💡 This error may be recoverable. Please try again.'); - } - - process.exit(1); - } - - if (error instanceof Error) { - // Convert Node.js error to StackMemoryError - let stackMemoryError: StackMemoryError; - - if ('code' in error && typeof error.code === 'string') { - stackMemoryError = StackMemoryError.fromNodeError( - error as NodeJS.ErrnoException, - { operation } - ); - } else { - stackMemoryError = new StackMemoryError( - ErrorCode.OPERATION_FAILED, - `Operation '${operation}' failed: ${error.message}`, - `Operation failed: ${error.message}`, - { operation }, - false, - error - ); - } - - console.error(`❌ ${stackMemoryError.userMessage}`); - if (stackMemoryError.recoverable) { - console.error('💡 This error may be recoverable. Please try again.'); - } - - process.exit(1); - } - - // Unknown error type - const unknownError = new StackMemoryError( - ErrorCode.UNKNOWN_ERROR, - `Unknown error in operation '${operation}': ${String(error)}`, - 'An unexpected error occurred.', - { operation, errorType: typeof error }, - false - ); - - console.error(`❌ ${unknownError.userMessage}`); - process.exit(1); - } - - static async safeExecute( - operation: () => Promise | T, - operationName: string, - fallback?: T - ): Promise { - try { - return await operation(); - } catch (error: unknown) { - if (fallback !== undefined) { - logger.warn(`Operation '${operationName}' failed, using fallback`, { - error: String(error), - }); - return fallback; - } - - ErrorHandler.handle(error, operationName); - } - } - - static async withRetry( - operation: () => Promise | T, - operationName: string, - maxRetries: number = ErrorHandler.MAX_RETRIES - ): Promise { - let lastError: unknown; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const result = await operation(); - // Clear retry count on success - ErrorHandler.retryMap.delete(operationName); - return result; - } catch (error: unknown) { - lastError = error; - - if (error instanceof StackMemoryError && !error.recoverable) { - // Don't retry non-recoverable errors - ErrorHandler.handle(error, operationName); - } - - if (attempt === maxRetries) { - break; - } - - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Exponential backoff - logger.warn( - `Attempt ${attempt}/${maxRetries} failed for '${operationName}', retrying in ${delay}ms`, - { - error: String(error), - } - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - - ErrorHandler.handle( - lastError, - `${operationName} (after ${maxRetries} attempts)` - ); - } - - static createCircuitBreaker( - operation: () => Promise | T, - operationName: string, - threshold: number = 5 - ) { - let failures = 0; - let lastFailure = 0; - const resetTimeout = 30000; // 30 seconds - - return async (): Promise => { - const now = Date.now(); - - // Reset circuit breaker after timeout - if (now - lastFailure > resetTimeout) { - failures = 0; - } - - // Circuit is open (too many failures) - if (failures >= threshold) { - throw new StackMemoryError( - ErrorCode.OPERATION_FAILED, - `Circuit breaker open for '${operationName}'`, - `Operation temporarily unavailable. Please try again later.`, - { operationName, failures, threshold }, - true - ); - } - - try { - const result = await operation(); - failures = 0; // Reset on success - return result; - } catch (error: unknown) { - failures++; - lastFailure = now; - throw error; - } - }; - } -} - -// Utility functions for common error scenarios -export const validateInput = ( - value: unknown, - name: string, - validator: (val: unknown) => boolean -): asserts value is NonNullable => { - if (!validator(value)) { - throw new StackMemoryError( - ErrorCode.INVALID_INPUT, - `Invalid ${name}: ${String(value)}`, - `Please provide a valid ${name}.`, - { name, value }, - true - ); - } -}; - -export const validateEmail = (email: string): asserts email is string => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email) || email.length > 254) { - throw new StackMemoryError( - ErrorCode.INVALID_INPUT, - `Invalid email format: ${email}`, - 'Please provide a valid email address.', - { email }, - true - ); - } -}; - -export const validatePath = (filePath: string): asserts filePath is string => { - if (!filePath || filePath.includes('..') || filePath.includes('\0')) { - throw new StackMemoryError( - ErrorCode.INVALID_INPUT, - `Invalid path: ${filePath}`, - 'Invalid file path provided.', - { path: filePath }, - true - ); - } -}; +export { + // Error codes + ErrorCode, + + // Error classes + StackMemoryError, + DatabaseError, + FrameError, + TaskError, + IntegrationError, + MCPError, + ValidationError, + ProjectError, + SystemError, + + // Error handler + ErrorHandler, + + // Utilities + getUserFriendlyMessage, + isRetryableError, + getErrorMessage, + wrapError, + isStackMemoryError, + createErrorHandler, + + // Validators + validateInput, + validateEmail, + validatePath, + + // Types + type ErrorContext, + type StackMemoryErrorOptions, +} from '../errors/index.js'; diff --git a/src/core/monitoring/session-monitor.ts b/src/core/monitoring/session-monitor.ts index d01fd59..f8ed8cd 100644 --- a/src/core/monitoring/session-monitor.ts +++ b/src/core/monitoring/session-monitor.ts @@ -6,7 +6,7 @@ import { EventEmitter } from 'events'; import { ClearSurvival } from '../session/clear-survival.js'; import { HandoffGenerator } from '../session/handoff-generator.js'; -import { FrameManager } from '../frame/frame-manager.js'; +import { FrameManager } from '../context/index.js'; import { DatabaseManager } from '../storage/database-manager.js'; import * as fs from 'fs/promises'; import * as path from 'path'; diff --git a/src/core/performance/lazy-context-loader.ts b/src/core/performance/lazy-context-loader.ts index 40136b0..8b7a53b 100644 --- a/src/core/performance/lazy-context-loader.ts +++ b/src/core/performance/lazy-context-loader.ts @@ -4,7 +4,7 @@ */ import Database from 'better-sqlite3'; -import { Frame, Anchor, Event } from '../context/frame-manager.js'; +import { Frame, Anchor, Event } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; export interface LazyLoadOptions { diff --git a/src/core/performance/optimized-frame-context.ts b/src/core/performance/optimized-frame-context.ts index de6b7dc..1958023 100644 --- a/src/core/performance/optimized-frame-context.ts +++ b/src/core/performance/optimized-frame-context.ts @@ -11,7 +11,7 @@ import { FrameContext, Anchor, Event, -} from '../context/frame-manager.js'; +} from '../context/index.js'; export interface ContextAssemblyOptions { maxEvents?: number; diff --git a/src/core/retrieval/context-retriever.ts b/src/core/retrieval/context-retriever.ts index 9a0cf83..61f3726 100644 --- a/src/core/retrieval/context-retriever.ts +++ b/src/core/retrieval/context-retriever.ts @@ -7,7 +7,7 @@ import { DatabaseAdapter, SearchOptions, } from '../database/database-adapter.js'; -import { Frame } from '../context/frame-manager.js'; +import { Frame } from '../context/index.js'; import { logger } from '../monitoring/logger.js'; export interface ContextQuery { diff --git a/src/core/retrieval/graph-retrieval.ts b/src/core/retrieval/graph-retrieval.ts index 6846052..f5567e1 100644 --- a/src/core/retrieval/graph-retrieval.ts +++ b/src/core/retrieval/graph-retrieval.ts @@ -9,7 +9,7 @@ import Database from 'better-sqlite3'; import { logger } from '../monitoring/logger.js'; import { Trace, CompressedTrace } from '../trace/types.js'; -import { Frame, Anchor } from '../context/frame-manager.js'; +import { Frame, Anchor } from '../context/index.js'; import crypto from 'crypto'; export type NodeType = diff --git a/src/core/retrieval/hierarchical-retrieval.ts b/src/core/retrieval/hierarchical-retrieval.ts index 21ea5e6..6ec3805 100644 --- a/src/core/retrieval/hierarchical-retrieval.ts +++ b/src/core/retrieval/hierarchical-retrieval.ts @@ -9,7 +9,7 @@ import Database from 'better-sqlite3'; import { logger } from '../monitoring/logger.js'; import { Trace, CompressedTrace } from '../trace/types.js'; -import { Frame, Anchor, Event } from '../context/frame-manager.js'; +import { Frame, Anchor, Event } from '../context/index.js'; import * as zlib from 'zlib'; import { promisify } from 'util'; import crypto from 'crypto'; diff --git a/src/core/retrieval/llm-context-retrieval.ts b/src/core/retrieval/llm-context-retrieval.ts index 80dc61a..3855a48 100644 --- a/src/core/retrieval/llm-context-retrieval.ts +++ b/src/core/retrieval/llm-context-retrieval.ts @@ -9,7 +9,7 @@ import { Frame, Anchor, Event, -} from '../context/frame-manager.js'; +} from '../context/index.js'; import { QueryParser, StackMemoryQuery } from '../query/query-parser.js'; import { CompressedSummaryGenerator } from './summary-generator.js'; import { diff --git a/src/core/retrieval/retrieval-benchmarks.ts b/src/core/retrieval/retrieval-benchmarks.ts index 55f367c..c0cfa86 100644 --- a/src/core/retrieval/retrieval-benchmarks.ts +++ b/src/core/retrieval/retrieval-benchmarks.ts @@ -15,7 +15,7 @@ import { Trace } from '../trace/types.js'; import { LLMContextRetrieval } from './llm-context-retrieval.js'; import { HierarchicalRetrieval } from './hierarchical-retrieval.js'; import { GraphRetrieval } from './graph-retrieval.js'; -import { FrameManager } from '../context/frame-manager.js'; +import { FrameManager } from '../context/index.js'; export interface BenchmarkQuery { query: string; diff --git a/src/core/retrieval/summary-generator.ts b/src/core/retrieval/summary-generator.ts index 227906c..242b8bb 100644 --- a/src/core/retrieval/summary-generator.ts +++ b/src/core/retrieval/summary-generator.ts @@ -9,7 +9,7 @@ import { Frame, Anchor, Event, -} from '../context/frame-manager.js'; +} from '../context/index.js'; import { TraceDetector } from '../trace/trace-detector.js'; import { CompressedSummary, diff --git a/src/core/retrieval/types.ts b/src/core/retrieval/types.ts index 0c5fa51..fe2eaaa 100644 --- a/src/core/retrieval/types.ts +++ b/src/core/retrieval/types.ts @@ -3,7 +3,7 @@ * Implements intelligent context selection based on compressed summaries */ -import { Frame, Anchor, Event } from '../context/frame-manager.js'; +import { Frame, Anchor, Event } from '../context/index.js'; import { StackMemoryQuery } from '../query/query-parser.js'; /** diff --git a/src/core/storage/__tests__/two-tier-storage.test.ts b/src/core/storage/__tests__/two-tier-storage.test.ts index f1918d4..16f92fd 100644 --- a/src/core/storage/__tests__/two-tier-storage.test.ts +++ b/src/core/storage/__tests__/two-tier-storage.test.ts @@ -13,7 +13,7 @@ import { type TwoTierConfig, type TierConfig } from '../two-tier-storage.js'; -import type { Frame, Event, Anchor } from '../../context/frame-manager.js'; +import type { Frame, Event, Anchor } from '../../context/index.js'; describe('TwoTierStorageSystem', () => { let storage: TwoTierStorageSystem; diff --git a/src/core/storage/chromadb-adapter.ts b/src/core/storage/chromadb-adapter.ts index 0f137cd..ff1ec3f 100644 --- a/src/core/storage/chromadb-adapter.ts +++ b/src/core/storage/chromadb-adapter.ts @@ -7,7 +7,7 @@ import { CloudClient, Collection } from 'chromadb'; import { v4 as uuidv4 } from 'uuid'; -import { Frame } from '../context/frame-manager.js'; +import { Frame } from '../context/index.js'; import { Logger } from '../monitoring/logger.js'; interface ChromaDocument { diff --git a/src/core/storage/infinite-storage.ts b/src/core/storage/infinite-storage.ts index db62b37..3ca8444 100644 --- a/src/core/storage/infinite-storage.ts +++ b/src/core/storage/infinite-storage.ts @@ -18,7 +18,7 @@ import { import { createClient as createRedisClient } from 'redis'; import { Pool } from 'pg'; import { Logger } from '../monitoring/logger.js'; -import { Frame } from '../context/frame-manager.js'; +import { Frame } from '../context/index.js'; import { v4 as uuidv4 } from 'uuid'; import { compress, decompress } from '../utils/compression.js'; diff --git a/src/core/storage/two-tier-storage.ts b/src/core/storage/two-tier-storage.ts index 862e455..dccdc21 100644 --- a/src/core/storage/two-tier-storage.ts +++ b/src/core/storage/two-tier-storage.ts @@ -26,7 +26,7 @@ import * as zlib from 'zlib'; import { promisify } from 'util'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from '../monitoring/logger.js'; -import type { Frame, Event, Anchor } from '../context/frame-manager.js'; +import type { Frame, Event, Anchor } from '../context/index.js'; // LZ4 would be installed separately: npm install lz4 // For now we'll use a placeholder diff --git a/src/features/tasks/task-aware-context.ts b/src/features/tasks/task-aware-context.ts index da7ea2c..672fc79 100644 --- a/src/features/tasks/task-aware-context.ts +++ b/src/features/tasks/task-aware-context.ts @@ -9,7 +9,7 @@ import { Anchor, Event, FrameManager, -} from '../../core/context/frame-manager.js'; +} from '../../core/context/index.js'; import { logger } from '../../core/monitoring/logger.js'; export type TaskStatus = diff --git a/src/features/web/server/index.ts b/src/features/web/server/index.ts index 5ddd852..e823617 100644 --- a/src/features/web/server/index.ts +++ b/src/features/web/server/index.ts @@ -9,7 +9,7 @@ import { Server as SocketServer } from 'socket.io'; import cors from 'cors'; import { LinearTaskReader } from '../../tui/services/linear-task-reader.js'; import { SessionManager } from '../../../core/session/session-manager.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import Database from 'better-sqlite3'; import { existsSync } from 'fs'; import { join } from 'path'; diff --git a/src/hooks/whatsapp-sync.ts b/src/hooks/whatsapp-sync.ts index 7df170d..4734928 100644 --- a/src/hooks/whatsapp-sync.ts +++ b/src/hooks/whatsapp-sync.ts @@ -1,6 +1,9 @@ /** * WhatsApp Context Sync Engine * Push frame digests and context updates to WhatsApp + * + * Uses the frame lifecycle hooks system to receive frame close events. + * Call `registerWhatsAppSyncHook()` to enable automatic sync on frame close. */ import { existsSync, readFileSync } from 'fs'; @@ -14,6 +17,10 @@ import { } from './sms-notify.js'; import { writeFileSecure, ensureSecureDir } from './secure-fs.js'; import { SyncOptionsSchema, parseConfigSafe } from './schemas.js'; +import { + frameLifecycleHooks, + type FrameCloseData, +} from '../core/context/frame-lifecycle-hooks.js'; export interface SyncOptions { autoSyncOnClose: boolean; @@ -454,6 +461,55 @@ export async function onFrameClosed( return syncFrameData(frameData, options); } +/** + * Internal hook handler that receives FrameCloseData from lifecycle hooks + */ +async function handleFrameCloseHook(data: FrameCloseData): Promise { + const digestData = createFrameDigestData( + data.frame, + data.events, + data.anchors + ); + await onFrameClosed(digestData); +} + +// Track if hook is registered to avoid duplicates +let hookUnregister: (() => void) | null = null; + +/** + * Register WhatsApp sync as a frame lifecycle hook + * This enables automatic sync when frames are closed + * Call this during app initialization to enable the integration + * + * @returns Unregister function to disable the hook + */ +export function registerWhatsAppSyncHook(): () => void { + // Avoid duplicate registration + if (hookUnregister) { + return hookUnregister; + } + + hookUnregister = frameLifecycleHooks.onFrameClosed( + 'whatsapp-sync', + handleFrameCloseHook, + -10 // Low priority - run after other hooks + ); + + return () => { + if (hookUnregister) { + hookUnregister(); + hookUnregister = null; + } + }; +} + +/** + * Check if the WhatsApp sync hook is currently registered + */ +export function isHookRegistered(): boolean { + return hookUnregister !== null; +} + /** * Create frame digest data from raw frame info * Helper for integration with frame-manager diff --git a/src/index.ts b/src/index.ts index 171a254..9eebad3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ export { FrameManager, type FrameType, type FrameState, -} from './core/context/frame-manager.js'; +} from './core/context/index.js'; export { logger, Logger, LogLevel } from './core/monitoring/logger.js'; export { StackMemoryError, diff --git a/src/integrations/mcp/handlers/context-handlers.ts b/src/integrations/mcp/handlers/context-handlers.ts index f428082..5928fb5 100644 --- a/src/integrations/mcp/handlers/context-handlers.ts +++ b/src/integrations/mcp/handlers/context-handlers.ts @@ -3,7 +3,7 @@ * Handles frame management and context retrieval */ -import { FrameManager, FrameType } from '../../../core/context/frame-manager.js'; +import { FrameManager, FrameType } from '../../../core/context/index.js'; import { LLMContextRetrieval } from '../../../core/retrieval/index.js'; import { logger } from '../../../core/monitoring/logger.js'; diff --git a/src/integrations/mcp/handlers/discovery-handlers.ts b/src/integrations/mcp/handlers/discovery-handlers.ts index 337a361..ca372e7 100644 --- a/src/integrations/mcp/handlers/discovery-handlers.ts +++ b/src/integrations/mcp/handlers/discovery-handlers.ts @@ -3,7 +3,7 @@ * Intelligently discovers relevant files based on current context */ -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { LLMContextRetrieval } from '../../../core/retrieval/index.js'; import { logger } from '../../../core/monitoring/logger.js'; import { execSync } from 'child_process'; diff --git a/src/integrations/mcp/server.ts b/src/integrations/mcp/server.ts index 552e596..79e7de4 100644 --- a/src/integrations/mcp/server.ts +++ b/src/integrations/mcp/server.ts @@ -21,7 +21,7 @@ import { import { readFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { execSync } from 'child_process'; -import { FrameManager, FrameType } from '../../core/context/frame-manager.js'; +import { FrameManager, FrameType } from '../../core/context/index.js'; import { LinearTaskManager, TaskPriority, diff --git a/src/integrations/ralph/bridge/ralph-stackmemory-bridge.ts b/src/integrations/ralph/bridge/ralph-stackmemory-bridge.ts index 0a56247..f8d8c5e 100644 --- a/src/integrations/ralph/bridge/ralph-stackmemory-bridge.ts +++ b/src/integrations/ralph/bridge/ralph-stackmemory-bridge.ts @@ -8,7 +8,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { execSync } from 'child_process'; import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { SessionManager } from '../../../core/session/session-manager.js'; import { SQLiteAdapter } from '../../../core/database/sqlite-adapter.js'; import { ContextBudgetManager } from '../context/context-budget-manager.js'; diff --git a/src/integrations/ralph/context/stackmemory-context-loader.ts b/src/integrations/ralph/context/stackmemory-context-loader.ts index f4a9e4e..11c977f 100644 --- a/src/integrations/ralph/context/stackmemory-context-loader.ts +++ b/src/integrations/ralph/context/stackmemory-context-loader.ts @@ -4,7 +4,7 @@ */ import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { sharedContextLayer } from '../../../core/context/shared-context-layer.js'; import { ContextRetriever } from '../../../core/retrieval/context-retriever.js'; import { sessionManager } from '../../../core/session/index.js'; diff --git a/src/integrations/ralph/learning/pattern-learner.ts b/src/integrations/ralph/learning/pattern-learner.ts index 823d85d..5e228c2 100644 --- a/src/integrations/ralph/learning/pattern-learner.ts +++ b/src/integrations/ralph/learning/pattern-learner.ts @@ -4,7 +4,7 @@ */ import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { sharedContextLayer } from '../../../core/context/shared-context-layer.js'; import { sessionManager } from '../../../core/session/index.js'; import { diff --git a/src/integrations/ralph/orchestration/multi-loop-orchestrator.ts b/src/integrations/ralph/orchestration/multi-loop-orchestrator.ts index a078f20..3350716 100644 --- a/src/integrations/ralph/orchestration/multi-loop-orchestrator.ts +++ b/src/integrations/ralph/orchestration/multi-loop-orchestrator.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { sessionManager } from '../../../core/session/index.js'; import { RalphStackMemoryBridge } from '../bridge/ralph-stackmemory-bridge.js'; import { diff --git a/src/integrations/ralph/swarm/swarm-coordinator.ts b/src/integrations/ralph/swarm/swarm-coordinator.ts index 4ad24cb..0a6f519 100644 --- a/src/integrations/ralph/swarm/swarm-coordinator.ts +++ b/src/integrations/ralph/swarm/swarm-coordinator.ts @@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid'; import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { sessionManager } from '../../../core/session/index.js'; import { sharedContextLayer } from '../../../core/context/shared-context-layer.js'; import { RalphStackMemoryBridge } from '../bridge/ralph-stackmemory-bridge.js'; diff --git a/src/integrations/ralph/types.ts b/src/integrations/ralph/types.ts index 226ad66..e3f90d3 100644 --- a/src/integrations/ralph/types.ts +++ b/src/integrations/ralph/types.ts @@ -3,7 +3,7 @@ * Includes swarm coordination, pattern learning, and orchestration types */ -import { Frame, FrameType } from '../../core/context/frame-manager.js'; +import { Frame, FrameType } from '../../core/context/index.js'; import { Session } from '../../core/session/session-manager.js'; // Ralph Loop types diff --git a/src/integrations/ralph/visualization/ralph-debugger.ts b/src/integrations/ralph/visualization/ralph-debugger.ts index 5e00864..e62c71d 100644 --- a/src/integrations/ralph/visualization/ralph-debugger.ts +++ b/src/integrations/ralph/visualization/ralph-debugger.ts @@ -6,7 +6,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { logger } from '../../../core/monitoring/logger.js'; -import { FrameManager } from '../../../core/context/frame-manager.js'; +import { FrameManager } from '../../../core/context/index.js'; import { sessionManager } from '../../../core/session/index.js'; import { DebugSession, diff --git a/src/mcp/stackmemory-mcp-server.ts b/src/mcp/stackmemory-mcp-server.ts index 6959a65..38d63e0 100644 --- a/src/mcp/stackmemory-mcp-server.ts +++ b/src/mcp/stackmemory-mcp-server.ts @@ -20,7 +20,7 @@ import { LinearTaskManager, TaskPriority, } from '../features/tasks/linear-task-manager.js'; -import { FrameManager } from '../core/context/frame-manager.js'; +import { FrameManager } from '../core/context/index.js'; import { AgentTaskManager } from '../agents/core/agent-task-manager.js'; import { logger } from '../core/monitoring/logger.js'; @@ -40,7 +40,7 @@ const frameManager = new FrameManager(db, PROJECT_ROOT, undefined); const agentTaskManager = new AgentTaskManager(taskStore, frameManager); // Track active Claude session -// eslint-disable-next-line @typescript-eslint/no-unused-vars + let _claudeSessionId: string | null = null; let claudeFrameId: string | null = null; diff --git a/src/skills/claude-skills.ts b/src/skills/claude-skills.ts index b0e420c..338430d 100644 --- a/src/skills/claude-skills.ts +++ b/src/skills/claude-skills.ts @@ -10,7 +10,7 @@ import { import { DualStackManager } from '../core/context/dual-stack-manager.js'; import { SQLiteAdapter } from '../core/database/sqlite-adapter.js'; import { ContextRetriever } from '../core/retrieval/context-retriever.js'; -import type { FrameManager } from '../core/context/frame-manager.js'; +import type { FrameManager } from '../core/context/index.js'; import { logger } from '../core/monitoring/logger.js'; import { RepoIngestionSkill, @@ -25,7 +25,7 @@ import { getAPISkill, type APISkill } from './api-skill.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import type { Frame } from '../core/context/frame-manager.js'; +import type { Frame } from '../core/context/index.js'; // Type definitions for Dig skill interface Pattern { diff --git a/src/skills/recursive-agent-orchestrator.ts b/src/skills/recursive-agent-orchestrator.ts index c757fb4..3aaecb1 100644 --- a/src/skills/recursive-agent-orchestrator.ts +++ b/src/skills/recursive-agent-orchestrator.ts @@ -13,7 +13,7 @@ */ import { logger } from '../core/monitoring/logger.js'; -import { FrameManager } from '../core/context/frame-manager.js'; +import { FrameManager } from '../core/context/index.js'; import { DualStackManager } from '../core/context/dual-stack-manager.js'; import { ContextRetriever } from '../core/retrieval/context-retriever.js'; import { LinearTaskManager } from '../features/tasks/linear-task-manager.js'; diff --git a/src/skills/unified-rlm-orchestrator.ts b/src/skills/unified-rlm-orchestrator.ts index 214a1c8..b252157 100644 --- a/src/skills/unified-rlm-orchestrator.ts +++ b/src/skills/unified-rlm-orchestrator.ts @@ -20,7 +20,7 @@ import { import { logger } from '../core/monitoring/logger.js'; import type { DualStackManager } from '../core/context/dual-stack-manager.js'; import type { ContextRetriever } from '../core/retrieval/context-retriever.js'; -import type { FrameManager } from '../core/context/frame-manager.js'; +import type { FrameManager } from '../core/context/index.js'; import type { LinearTaskManager } from '../features/tasks/linear-task-manager.js'; // Skill to RLM mapping configuration