Skip to content

Commit c46ca2b

Browse files
committed
refactor(frame-manager): add lifecycle hooks and missing methods to RefactoredFrameManager
- Add frameLifecycleHooks integration to RefactoredFrameManager.closeFrame() - Add missing methods for feature parity with legacy FrameManager: - setQueryMode() - change query mode for frame retrieval - getRecentFrames() - get recent frames with metadata - addContext() - add context metadata to current frame - deleteFrame() - delete frame completely (for handoffs) - extractTagsFromFrame() - extract tags for categorization - calculateFrameImportance() - calculate frame importance - Add setQueryMode() and removeFrame() to FrameStack This brings RefactoredFrameManager to feature parity with legacy FrameManager, enabling eventual removal of the legacy implementation. https://claude.ai/code/session_014ojs76858uGzWb6Hrbs4VG
1 parent 8fede91 commit c46ca2b

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

src/core/context/frame-stack.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { Frame, FrameContext, FrameType } from './frame-types.js';
77
import { FrameDatabase } from './frame-database.js';
88
import { logger } from '../monitoring/logger.js';
99
import { FrameError, ErrorCode } from '../errors/index.js';
10+
import { FrameQueryMode } from '../session/index.js';
1011

1112
export class FrameStack {
1213
private activeStack: string[] = [];
14+
private queryMode: FrameQueryMode = FrameQueryMode.PROJECT_ACTIVE;
1315

1416
constructor(
1517
private frameDb: FrameDatabase,
@@ -185,6 +187,39 @@ export class FrameStack {
185187
logger.info('Cleared frame stack', { previousDepth });
186188
}
187189

190+
/**
191+
* Set query mode and reinitialize stack
192+
*/
193+
setQueryMode(mode: FrameQueryMode): void {
194+
this.queryMode = mode;
195+
// Reinitialize with new query mode
196+
this.initialize().catch((error) => {
197+
logger.warn('Failed to reinitialize stack with new query mode', {
198+
mode,
199+
error,
200+
});
201+
});
202+
}
203+
204+
/**
205+
* Remove a specific frame from the stack without popping frames above it
206+
*/
207+
removeFrame(frameId: string): boolean {
208+
const index = this.activeStack.indexOf(frameId);
209+
if (index === -1) {
210+
return false;
211+
}
212+
213+
this.activeStack.splice(index, 1);
214+
215+
logger.debug('Removed frame from stack', {
216+
frameId,
217+
stackDepth: this.activeStack.length,
218+
});
219+
220+
return true;
221+
}
222+
188223
/**
189224
* Validate stack consistency
190225
*/

src/core/context/refactored-frame-manager.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../errors/index.js';
1717
import { retry, withTimeout } from '../errors/recovery.js';
1818
import { sessionManager, FrameQueryMode } from '../session/index.js';
19+
import { frameLifecycleHooks } from './frame-lifecycle-hooks.js';
1920

2021
// Constants for frame validation
2122
const MAX_FRAME_DEPTH = 100; // Maximum allowed frame depth
@@ -308,6 +309,15 @@ export class RefactoredFrameManager {
308309
// Close all child frames recursively
309310
this.closeChildFrames(targetFrameId);
310311

312+
// Trigger lifecycle hooks (fire and forget)
313+
const events = this.frameDb.getFrameEvents(targetFrameId);
314+
const anchors = this.frameDb.getFrameAnchors(targetFrameId);
315+
frameLifecycleHooks
316+
.triggerClose({ frame: { ...frame, state: 'closed' }, events, anchors })
317+
.catch(() => {
318+
// Silently ignore errors - hooks are non-critical
319+
});
320+
311321
logger.info('Closed frame', {
312322
frameId: targetFrameId,
313323
name: frame.name,
@@ -754,6 +764,141 @@ export class RefactoredFrameManager {
754764
warnings,
755765
};
756766
}
767+
768+
/**
769+
* Set query mode for frame retrieval
770+
*/
771+
setQueryMode(mode: FrameQueryMode): void {
772+
this.queryMode = mode;
773+
// Reinitialize stack with new query mode
774+
this.frameStack.setQueryMode(mode);
775+
}
776+
777+
/**
778+
* Get recent frames for context sharing
779+
*/
780+
async getRecentFrames(limit: number = 100): Promise<Frame[]> {
781+
try {
782+
const frames = this.frameDb.getFramesByProject(this.projectId);
783+
784+
// Sort by created_at descending and limit
785+
return frames
786+
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
787+
.slice(0, limit)
788+
.map((frame) => ({
789+
...frame,
790+
// Add compatibility fields
791+
frameId: frame.frame_id,
792+
runId: frame.run_id,
793+
projectId: frame.project_id,
794+
parentFrameId: frame.parent_frame_id,
795+
title: frame.name,
796+
timestamp: frame.created_at,
797+
metadata: {
798+
tags: this.extractTagsFromFrame(frame),
799+
importance: this.calculateFrameImportance(frame),
800+
},
801+
data: {
802+
inputs: frame.inputs,
803+
outputs: frame.outputs,
804+
digest: frame.digest_json,
805+
},
806+
}));
807+
} catch (error: unknown) {
808+
logger.error('Failed to get recent frames', error as Error);
809+
return [];
810+
}
811+
}
812+
813+
/**
814+
* Add context metadata to the current frame
815+
*/
816+
async addContext(key: string, value: any): Promise<void> {
817+
const currentFrameId = this.frameStack.getCurrentFrameId();
818+
if (!currentFrameId) return;
819+
820+
try {
821+
const frame = this.frameDb.getFrame(currentFrameId);
822+
if (!frame) return;
823+
824+
const metadata = frame.outputs || {};
825+
metadata[key] = value;
826+
827+
this.frameDb.updateFrame(currentFrameId, {
828+
outputs: metadata,
829+
});
830+
} catch (error: unknown) {
831+
logger.warn('Failed to add context to frame', { error, key });
832+
}
833+
}
834+
835+
/**
836+
* Delete a frame completely from the database (used in handoffs)
837+
*/
838+
deleteFrame(frameId: string): void {
839+
try {
840+
// Remove from active stack if present
841+
this.frameStack.removeFrame(frameId);
842+
843+
// Delete the frame and related data (cascades via FrameDatabase)
844+
this.frameDb.deleteFrame(frameId);
845+
846+
logger.debug('Deleted frame completely', { frameId });
847+
} catch (error: unknown) {
848+
logger.error('Failed to delete frame', { frameId, error });
849+
throw error;
850+
}
851+
}
852+
853+
/**
854+
* Extract tags from frame for categorization
855+
*/
856+
private extractTagsFromFrame(frame: Frame): string[] {
857+
const tags: string[] = [];
858+
859+
if (frame.type) tags.push(frame.type);
860+
861+
if (frame.name) {
862+
const nameLower = frame.name.toLowerCase();
863+
if (nameLower.includes('error')) tags.push('error');
864+
if (nameLower.includes('fix')) tags.push('resolution');
865+
if (nameLower.includes('decision')) tags.push('decision');
866+
if (nameLower.includes('milestone')) tags.push('milestone');
867+
}
868+
869+
try {
870+
if (frame.digest_json && typeof frame.digest_json === 'object') {
871+
const digest = frame.digest_json as Record<string, unknown>;
872+
if (Array.isArray(digest.tags)) {
873+
tags.push(...(digest.tags as string[]));
874+
}
875+
}
876+
} catch {
877+
// Ignore parse errors
878+
}
879+
880+
return [...new Set(tags)];
881+
}
882+
883+
/**
884+
* Calculate frame importance for prioritization
885+
*/
886+
private calculateFrameImportance(frame: Frame): 'high' | 'medium' | 'low' {
887+
if (frame.type === 'milestone' || frame.name?.includes('decision')) {
888+
return 'high';
889+
}
890+
891+
if (frame.type === 'error' || frame.type === 'resolution') {
892+
return 'medium';
893+
}
894+
895+
if (frame.closed_at && frame.created_at) {
896+
const duration = frame.closed_at - frame.created_at;
897+
if (duration > 300) return 'medium';
898+
}
899+
900+
return 'low';
901+
}
757902
}
758903

759904
// Re-export types for compatibility

0 commit comments

Comments
 (0)