@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
JavaScript
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