UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

779 lines (778 loc) 27 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; import { ValidationError, DatabaseError, ErrorCode } from "../errors/index.js"; import { validateInput, InitiateHandoffSchema, HandoffApprovalSchema } from "./validation.js"; class FrameHandoffManager { dualStackManager; activeHandoffs = /* @__PURE__ */ new Map(); pendingApprovals = /* @__PURE__ */ new Map(); notifications = /* @__PURE__ */ new Map(); constructor(dualStackManager) { this.dualStackManager = dualStackManager; } /** * Initiate a frame handoff with rich metadata and approval workflow */ async initiateHandoff(targetStackId, frameIds, metadata, targetUserId, message) { const input = validateInput(InitiateHandoffSchema, { targetStackId, frameIds, handoffRequest: metadata, reviewerId: targetUserId, description: message }); try { await this.dualStackManager.getPermissionManager().enforcePermission( this.dualStackManager.getPermissionManager().createContext( input.handoffRequest.initiatorId, "handoff", "handoff", input.targetStackId ) ); await this.validateFramesForHandoff(input.frameIds); const requestId = await this.dualStackManager.initiateHandoff( input.targetStackId, input.frameIds, input.reviewerId, input.description ); const progress = { requestId, status: "pending_review", transferredFrames: 0, totalFrames: input.frameIds.length, currentStep: "Awaiting approval", errors: [] }; this.activeHandoffs.set(requestId, progress); await this.createHandoffNotifications(requestId, metadata, targetUserId); await this.scheduleHandoffReminders(requestId, metadata); logger.info(`Initiated enhanced handoff: ${requestId}`, { frameCount: frameIds.length, priority: metadata.businessContext?.priority, targetUser: targetUserId }); return requestId; } catch (error) { throw new DatabaseError( "Failed to initiate handoff", ErrorCode.OPERATION_FAILED, { targetStackId, frameIds }, error instanceof Error ? error : void 0 ); } } /** * Submit approval/rejection for handoff request */ async submitHandoffApproval(requestId, approval) { const input = validateInput(HandoffApprovalSchema, { ...approval, reviewerId: approval.reviewerId }); const progress = this.activeHandoffs.get(requestId); if (!progress) { throw new ValidationError( `Handoff request not found: ${requestId}`, ErrorCode.HANDOFF_REQUEST_EXPIRED ); } const fullApproval = { ...input, requestId, reviewedAt: /* @__PURE__ */ new Date() }; const existingApprovals = this.pendingApprovals.get(requestId) || []; existingApprovals.push(fullApproval); this.pendingApprovals.set(requestId, existingApprovals); if (input.decision === "approved") { progress.status = "approved"; progress.currentStep = "Ready for transfer"; await this.executeHandoffTransfer(requestId); } else if (input.decision === "rejected") { progress.status = "failed"; progress.currentStep = "Rejected by reviewer"; progress.errors.push({ step: "approval", error: input.feedback || "Request rejected", timestamp: /* @__PURE__ */ new Date() }); } else if (input.decision === "needs_changes") { progress.status = "pending_review"; progress.currentStep = "Changes requested"; await this.notifyChangesRequested(requestId, approval); } this.activeHandoffs.set(requestId, progress); logger.info(`Handoff approval submitted: ${requestId}`, { decision: approval.decision, reviewer: approval.reviewerId }); } /** * Execute the actual frame transfer after approval */ async executeHandoffTransfer(requestId) { logger.debug("executeHandoffTransfer called", { requestId, availableHandoffs: Array.from(this.activeHandoffs.keys()) }); const progress = this.activeHandoffs.get(requestId); if (!progress) { logger.error("Handoff progress not found", { requestId, availableHandoffs: Array.from(this.activeHandoffs.keys()) }); throw new DatabaseError( `Handoff progress not found: ${requestId}`, ErrorCode.INVALID_STATE ); } try { logger.debug("Setting progress status to in_transfer", { requestId }); progress.status = "in_transfer"; progress.currentStep = "Transferring frames"; progress.estimatedCompletion = new Date(Date.now() + 5 * 60 * 1e3); logger.debug("About to call acceptHandoff", { requestId }); const result = await this.dualStackManager.acceptHandoff(requestId); logger.debug("acceptHandoff returned", { requestId, success: result.success }); if (result.success) { progress.status = "completed"; progress.currentStep = "Transfer completed"; progress.transferredFrames = result.mergedFrames.length; await this.notifyHandoffCompletion(requestId, result); logger.info(`Handoff transfer completed: ${requestId}`, { transferredFrames: progress.transferredFrames, conflicts: result.conflictFrames.length }); } else { progress.status = "failed"; progress.currentStep = "Transfer failed"; result.errors.forEach((error) => { progress.errors.push({ step: "transfer", error: `Frame ${error.frameId}: ${error.error}`, timestamp: /* @__PURE__ */ new Date() }); }); throw new DatabaseError( "Handoff transfer failed", ErrorCode.OPERATION_FAILED, { errors: result.errors } ); } } catch (error) { progress.status = "failed"; progress.currentStep = "Transfer error"; progress.errors.push({ step: "transfer", error: error instanceof Error ? error.message : String(error), timestamp: /* @__PURE__ */ new Date() }); logger.error(`Handoff transfer failed: ${requestId}`, error); throw error; } finally { this.activeHandoffs.set(requestId, progress); } } /** * Get handoff progress and status */ async getHandoffProgress(requestId) { return this.activeHandoffs.get(requestId) || null; } /** * Cancel a pending handoff request */ async cancelHandoff(requestId, reason) { const progress = this.activeHandoffs.get(requestId); if (!progress) { throw new DatabaseError( `Handoff request not found: ${requestId}`, ErrorCode.RESOURCE_NOT_FOUND ); } if (progress.status === "in_transfer") { throw new DatabaseError( "Cannot cancel handoff that is currently transferring", ErrorCode.INVALID_STATE ); } progress.status = "cancelled"; progress.currentStep = "Cancelled by user"; progress.errors.push({ step: "cancellation", error: reason, timestamp: /* @__PURE__ */ new Date() }); this.activeHandoffs.set(requestId, progress); await this.notifyHandoffCancellation(requestId, reason); logger.info(`Handoff cancelled: ${requestId}`, { reason }); } /** * Get all active handoffs for a user or team */ async getActiveHandoffs(userId, teamId) { const handoffs = Array.from(this.activeHandoffs.values()); if (userId || teamId) { return handoffs.filter( (handoff) => handoff.status === "pending_review" || handoff.status === "approved" || handoff.status === "in_transfer" ); } return handoffs; } /** * Get notifications for a user */ async getUserNotifications(userId) { return this.notifications.get(userId) || []; } /** * Mark notification as read */ async markNotificationRead(notificationId, userId) { const userNotifications = this.notifications.get(userId) || []; const updatedNotifications = userNotifications.filter( (n) => n.id !== notificationId ); this.notifications.set(userId, updatedNotifications); } /** * Validate frames are suitable for handoff */ async validateFramesForHandoff(frameIds) { const activeStack = this.dualStackManager.getActiveStack(); for (const frameId of frameIds) { const frame = await activeStack.getFrame(frameId); if (!frame) { throw new DatabaseError( `Frame not found: ${frameId}`, ErrorCode.RESOURCE_NOT_FOUND ); } if (frame.state === "active") { logger.warn(`Transferring active frame: ${frameId}`, { frameName: frame.name }); } } } /** * Create notifications for handoff stakeholders */ async createHandoffNotifications(requestId, metadata, targetUserId) { const notifications = []; if (targetUserId) { notifications.push({ id: `${requestId}-target`, type: "request", requestId, recipientId: targetUserId, title: "Frame Handoff Request", message: `${metadata.initiatorId} wants to transfer ${metadata.frameContext.totalFrames} frames to you`, actionRequired: true, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3), createdAt: /* @__PURE__ */ new Date() }); } if (metadata.businessContext?.stakeholders) { for (const stakeholderId of metadata.businessContext.stakeholders) { notifications.push({ id: `${requestId}-stakeholder-${stakeholderId}`, type: "request", requestId, recipientId: stakeholderId, title: "Frame Handoff Notification", message: `Frame transfer initiated for ${metadata.businessContext?.milestone || "project milestone"}`, actionRequired: false, createdAt: /* @__PURE__ */ new Date() }); } } for (const notification of notifications) { const userNotifications = this.notifications.get(notification.recipientId) || []; userNotifications.push(notification); this.notifications.set(notification.recipientId, userNotifications); } } /** * Schedule reminder notifications */ async scheduleHandoffReminders(requestId, metadata) { if (metadata.businessContext?.priority === "high" || metadata.businessContext?.priority === "critical") { setTimeout( async () => { const progress = this.activeHandoffs.get(requestId); if (progress && progress.status === "pending_review") { await this.sendHandoffReminder(requestId, metadata); } }, 4 * 60 * 60 * 1e3 ); } } /** * Send handoff reminder */ async sendHandoffReminder(requestId, metadata) { const progress = this.activeHandoffs.get(requestId); if (!progress || progress.status !== "pending_review") { return; } const reminderNotification = { id: `${requestId}-reminder-${Date.now()}`, type: "reminder", requestId, recipientId: metadata.targetUserId || "unknown", title: "\u23F0 Handoff Request Reminder", message: `Reminder: ${metadata.initiatorId} is waiting for approval on ${metadata.frameContext.totalFrames} frames. Priority: ${metadata.businessContext?.priority || "medium"}`, actionRequired: true, expiresAt: new Date(Date.now() + 12 * 60 * 60 * 1e3), // 12 hours createdAt: /* @__PURE__ */ new Date() }; if (metadata.targetUserId) { const userNotifications = this.notifications.get(metadata.targetUserId) || []; userNotifications.push(reminderNotification); this.notifications.set(metadata.targetUserId, userNotifications); logger.info(`Sent handoff reminder: ${requestId}`, { priority: metadata.businessContext?.priority, recipient: metadata.targetUserId }); } if (metadata.businessContext?.stakeholders) { for (const stakeholderId of metadata.businessContext.stakeholders) { const stakeholderNotification = { ...reminderNotification, id: `${requestId}-reminder-stakeholder-${stakeholderId}-${Date.now()}`, recipientId: stakeholderId, title: "\u{1F4CB} Handoff Status Update", message: `Pending handoff approval: ${metadata.businessContext?.milestone || "development work"} requires attention`, actionRequired: false }; const stakeholderNotifications = this.notifications.get(stakeholderId) || []; stakeholderNotifications.push(stakeholderNotification); this.notifications.set(stakeholderId, stakeholderNotifications); } } } /** * Notify when changes are requested */ async notifyChangesRequested(requestId, approval) { const progress = this.activeHandoffs.get(requestId); if (!progress) return; const changeRequestNotification = { id: `${requestId}-changes-${Date.now()}`, type: "request", requestId, recipientId: "requester", // TODO: Get actual requester from handoff metadata title: "Changes Requested for Handoff", message: `${approval.reviewerId} has requested changes: ${approval.feedback || "See detailed suggestions"}`, actionRequired: true, expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1e3), // 48 hours createdAt: /* @__PURE__ */ new Date() }; const notifications = this.notifications.get("requester") || []; notifications.push(changeRequestNotification); this.notifications.set("requester", notifications); logger.info(`Changes requested for handoff: ${requestId}`, { reviewer: approval.reviewerId, feedback: approval.feedback, suggestedChangesCount: approval.suggestedChanges?.length || 0 }); if (approval.suggestedChanges && approval.suggestedChanges.length > 0) { logger.info(`Detailed change suggestions:`, { requestId, suggestions: approval.suggestedChanges.map((change) => ({ frameId: change.frameId, suggestion: change.suggestion, reason: change.reason })) }); } } /** * Notify handoff completion */ async notifyHandoffCompletion(requestId, result) { const progress = this.activeHandoffs.get(requestId); if (!progress) return; const completionNotification = { id: `${requestId}-completion-${Date.now()}`, type: "completion", requestId, recipientId: "all", // Will be distributed to all stakeholders title: "Handoff Completed Successfully", message: `Frame transfer completed: ${result.mergedFrames.length} frames transferred${result.conflictFrames.length > 0 ? `, ${result.conflictFrames.length} conflicts resolved` : ""}`, actionRequired: false, createdAt: /* @__PURE__ */ new Date() }; const allUsers = Array.from(this.notifications.keys()); for (const userId of allUsers) { const userSpecificNotification = { ...completionNotification, id: `${requestId}-completion-${userId}-${Date.now()}`, recipientId: userId }; const userNotifications = this.notifications.get(userId) || []; userNotifications.push(userSpecificNotification); this.notifications.set(userId, userNotifications); } logger.info(`Handoff completed: ${requestId}`, { mergedFrames: result.mergedFrames.length, conflicts: result.conflictFrames.length, notifiedUsers: allUsers.length }); if (result.conflictFrames.length > 0) { logger.info(`Handoff completion details:`, { requestId, transferredFrames: result.mergedFrames.map( (f) => f.frameId || f.id ), conflictFrames: result.conflictFrames.map( (f) => f.frameId || f.id ) }); } } /** * Notify handoff cancellation */ async notifyHandoffCancellation(requestId, reason) { const cancellationNotification = { id: `${requestId}-cancellation-${Date.now()}`, type: "request", // Using 'request' type as it's informational requestId, recipientId: "all", // Will be distributed to all stakeholders title: "Handoff Cancelled", message: `Handoff request has been cancelled. Reason: ${reason}`, actionRequired: false, createdAt: /* @__PURE__ */ new Date() }; const allUsers = Array.from(this.notifications.keys()); for (const userId of allUsers) { const userSpecificNotification = { ...cancellationNotification, id: `${requestId}-cancellation-${userId}-${Date.now()}`, recipientId: userId }; const userNotifications = this.notifications.get(userId) || []; userNotifications.push(userSpecificNotification); this.notifications.set(userId, userNotifications); } logger.info(`Handoff cancelled: ${requestId}`, { reason, notifiedUsers: allUsers.length }); } /** * Get handoff analytics and metrics */ async getHandoffMetrics(timeRange) { const handoffs = Array.from(this.activeHandoffs.values()); const filteredHandoffs = timeRange ? handoffs.filter((h) => { return true; }) : handoffs; const completedHandoffs = filteredHandoffs.filter( (h) => h.status === "completed" ); return { totalHandoffs: filteredHandoffs.length, completedHandoffs: completedHandoffs.length, averageProcessingTime: this.calculateAverageProcessingTime(completedHandoffs), topFrameTypes: this.analyzeFrameTypes(filteredHandoffs), collaborationPatterns: this.analyzeCollaborationPatterns(filteredHandoffs) }; } calculateAverageProcessingTime(handoffs) { if (handoffs.length === 0) return 0; let totalProcessingTime = 0; let validHandoffs = 0; for (const handoff of handoffs) { if (handoff.status === "completed" && handoff.estimatedCompletion) { const frameComplexity = handoff.totalFrames * 0.5; const errorPenalty = handoff.errors.length * 2; const processingTime = Math.max(1, frameComplexity + errorPenalty); totalProcessingTime += processingTime; validHandoffs++; } } return validHandoffs > 0 ? Math.round(totalProcessingTime / validHandoffs) : 0; } analyzeFrameTypes(handoffs) { const frameTypeCount = /* @__PURE__ */ new Map(); for (const handoff of handoffs) { const estimatedTypes = this.estimateFrameTypes(handoff); for (const type of estimatedTypes) { frameTypeCount.set(type, (frameTypeCount.get(type) || 0) + 1); } } return Array.from(frameTypeCount.entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count).slice(0, 10); } estimateFrameTypes(handoff) { const types = []; if (handoff.totalFrames > 10) { types.push("bulk_transfer"); } if (handoff.errors.length > 0) { types.push("complex_handoff"); } if (handoff.transferredFrames === handoff.totalFrames) { types.push("complete_transfer"); } else { types.push("partial_transfer"); } types.push("development", "collaboration"); return types; } analyzeCollaborationPatterns(handoffs) { const collaborationCount = /* @__PURE__ */ new Map(); for (const handoff of handoffs) { const pattern = this.extractCollaborationPattern(handoff); if (pattern) { const key = `${pattern.sourceUser}->${pattern.targetUser}`; collaborationCount.set(key, (collaborationCount.get(key) || 0) + 1); } } return Array.from(collaborationCount.entries()).map(([pattern, count]) => { const [sourceUser, targetUser] = pattern.split("->"); return { sourceUser, targetUser, count }; }).sort((a, b) => b.count - a.count).slice(0, 20); } extractCollaborationPattern(handoff) { if (handoff.status === "completed") { return { sourceUser: "developer", targetUser: "reviewer" }; } else if (handoff.status === "failed") { return { sourceUser: "developer", targetUser: "lead" }; } return null; } /** * Real-time collaboration features */ /** * Get real-time handoff status updates */ async getHandoffStatusStream(requestId) { const progress = this.activeHandoffs.get(requestId); if (!progress) { throw new DatabaseError( `Handoff request not found: ${requestId}`, ErrorCode.RESOURCE_NOT_FOUND ); } const self = this; return { async *[Symbol.asyncIterator]() { let lastStatus = progress.status; while (lastStatus !== "completed" && lastStatus !== "failed" && lastStatus !== "cancelled") { const currentProgress = self.activeHandoffs.get(requestId); if (currentProgress && currentProgress.status !== lastStatus) { lastStatus = currentProgress.status; yield currentProgress; } await new Promise((resolve) => setTimeout(resolve, 1e3)); } } }; } /** * Update handoff progress in real-time */ async updateHandoffProgress(requestId, update) { let progress = this.activeHandoffs.get(requestId); if (!progress && update.requestId && update.status && update.totalFrames !== void 0) { progress = { requestId: update.requestId, status: update.status, transferredFrames: 0, totalFrames: update.totalFrames, currentStep: "Initialized", errors: [], ...update }; } else if (!progress) { throw new DatabaseError( `Handoff request not found: ${requestId}`, ErrorCode.RESOURCE_NOT_FOUND ); } else { progress = { ...progress, ...update }; } this.activeHandoffs.set(requestId, progress); logger.info(`Handoff progress updated: ${requestId}`, { status: progress.status, currentStep: progress.currentStep, transferredFrames: progress.transferredFrames }); await this.notifyProgressUpdate(requestId, progress); } /** * Notify stakeholders of progress updates */ async notifyProgressUpdate(requestId, progress) { const updateNotification = { id: `${requestId}-progress-${Date.now()}`, type: "request", requestId, recipientId: "all", title: "Handoff Progress Update", message: `Status: ${progress.status} | Step: ${progress.currentStep} | Progress: ${progress.transferredFrames}/${progress.totalFrames} frames`, actionRequired: false, createdAt: /* @__PURE__ */ new Date() }; const allUsers = Array.from(this.notifications.keys()); for (const userId of allUsers) { const userNotifications = this.notifications.get(userId) || []; userNotifications.push({ ...updateNotification, id: `${requestId}-progress-${userId}-${Date.now()}`, recipientId: userId }); this.notifications.set(userId, userNotifications); } } /** * Get active handoffs with real-time filtering */ async getActiveHandoffsRealTime(filters) { let handoffs = Array.from(this.activeHandoffs.values()); if (filters?.status) { handoffs = handoffs.filter((h) => h.status === filters.status); } if (filters?.userId) { handoffs = handoffs.filter( (h) => h.requestId.includes(filters.userId || "") ); } if (filters?.priority) { handoffs = handoffs.filter((h) => { const estimatedPriority = this.estimateHandoffPriority(h); return estimatedPriority === filters.priority; }); } return handoffs.sort((a, b) => { const statusPriority = { in_transfer: 4, approved: 3, pending_review: 2, completed: 1, failed: 1, cancelled: 0 }; return (statusPriority[b.status] || 0) - (statusPriority[a.status] || 0); }); } estimateHandoffPriority(handoff) { if (handoff.errors.length > 2 || handoff.totalFrames > 50) return "critical"; if (handoff.errors.length > 0 || handoff.totalFrames > 20) return "high"; if (handoff.totalFrames > 5) return "medium"; return "low"; } /** * Bulk handoff operations for team collaboration */ async bulkHandoffOperation(operation) { const results = { successful: [], failed: [] }; for (const requestId of operation.requestIds) { try { switch (operation.action) { case "approve": await this.submitHandoffApproval(requestId, { reviewerId: operation.reviewerId, decision: "approved", feedback: operation.feedback }); results.successful.push(requestId); break; case "reject": await this.submitHandoffApproval(requestId, { reviewerId: operation.reviewerId, decision: "rejected", feedback: operation.feedback || "Bulk rejection" }); results.successful.push(requestId); break; case "cancel": await this.cancelHandoff( requestId, operation.feedback || "Bulk cancellation" ); results.successful.push(requestId); break; } } catch (error) { results.failed.push({ requestId, error: error instanceof Error ? error.message : String(error) }); } } logger.info(`Bulk handoff operation completed`, { action: operation.action, successful: results.successful.length, failed: results.failed.length, reviewerId: operation.reviewerId }); return results; } /** * Enhanced notification management with cleanup */ async cleanupExpiredNotifications(userId) { let cleanedCount = 0; const now = /* @__PURE__ */ new Date(); const userIds = userId ? [userId] : Array.from(this.notifications.keys()); for (const uid of userIds) { const userNotifications = this.notifications.get(uid) || []; const activeNotifications = userNotifications.filter((notification) => { if (notification.expiresAt && notification.expiresAt < now) { cleanedCount++; return false; } return true; }); this.notifications.set(uid, activeNotifications); } if (cleanedCount > 0) { logger.info(`Cleaned up expired notifications`, { count: cleanedCount, userId: userId || "all" }); } return cleanedCount; } } export { FrameHandoffManager }; //# sourceMappingURL=frame-handoff-manager.js.map