git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/gof-memento-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-memento-pattern-20937e && rm -rf "$T"
agents/skills/codex/gof-memento-pattern/SKILL.mdGOF Memento Pattern
Capture and restore object state using mementos for undo history and time-travel.
When to Use
- You need to implement undo/redo without exposing the object's internals
- You want to save and restore checkpoints of an object's state
- You're building a text editor, drawing app, form wizard, or game save system
- You need time-travel debugging or state snapshots for comparison
Instructions
Classic memento with Originator and Caretaker:
// Memento — stores a snapshot of state class EditorMemento { constructor( private readonly content: string, private readonly cursorPosition: number, private readonly timestamp: Date ) {} getContent(): string { return this.content; } getCursorPosition(): number { return this.cursorPosition; } getTimestamp(): Date { return this.timestamp; } describe(): string { return `[${this.timestamp.toISOString()}] ${this.content.slice(0, 30)}...`; } } // Originator — creates and restores from mementos class TextEditor { private content = ''; private cursorPosition = 0; type(text: string): void { this.content = this.content.slice(0, this.cursorPosition) + text + this.content.slice(this.cursorPosition); this.cursorPosition += text.length; } moveCursor(position: number): void { this.cursorPosition = Math.max(0, Math.min(position, this.content.length)); } delete(count: number): void { this.content = this.content.slice(0, this.cursorPosition - count) + this.content.slice(this.cursorPosition); this.cursorPosition = Math.max(0, this.cursorPosition - count); } // Save state to memento save(): EditorMemento { return new EditorMemento(this.content, this.cursorPosition, new Date()); } // Restore state from memento restore(memento: EditorMemento): void { this.content = memento.getContent(); this.cursorPosition = memento.getCursorPosition(); } getState(): { content: string; cursor: number } { return { content: this.content, cursor: this.cursorPosition }; } } // Caretaker — manages the history of mementos class EditorHistory { private history: EditorMemento[] = []; private future: EditorMemento[] = []; save(editor: TextEditor): void { this.history.push(editor.save()); this.future = []; // clear redo history } undo(editor: TextEditor): boolean { if (this.history.length === 0) return false; this.future.push(editor.save()); editor.restore(this.history.pop()!); return true; } redo(editor: TextEditor): boolean { if (this.future.length === 0) return false; this.history.push(editor.save()); editor.restore(this.future.pop()!); return true; } getHistoryDescriptions(): string[] { return this.history.map((m) => m.describe()); } } // Usage const editor = new TextEditor(); const history = new EditorHistory(); history.save(editor); editor.type('Hello, world!'); history.save(editor); editor.type(' How are you?'); history.save(editor); editor.delete(4); console.log(editor.getState()); // { content: 'Hello, world! How are', cursor: 21 } history.undo(editor); console.log(editor.getState()); // { content: 'Hello, world! How are you?', cursor: 26 } history.undo(editor); console.log(editor.getState()); // { content: 'Hello, world!', cursor: 13 } history.redo(editor); console.log(editor.getState()); // { content: 'Hello, world! How are you?', cursor: 26 }
Lightweight memento using plain objects (TypeScript idiomatic):
type FormState = { firstName: string; lastName: string; email: string; step: number; }; class MultiStepForm { private state: FormState = { firstName: '', lastName: '', email: '', step: 1 }; private snapshots: FormState[] = []; updateField<K extends keyof FormState>(field: K, value: FormState[K]): void { this.state = { ...this.state, [field]: value }; } checkpoint(): void { this.snapshots.push({ ...this.state }); // shallow copy sufficient for flat state } rollback(): boolean { const snapshot = this.snapshots.pop(); if (!snapshot) return false; this.state = snapshot; return true; } getState(): Readonly<FormState> { return this.state; } }
Details
Encapsulation is key: The Originator creates and restores mementos. The Caretaker stores them but must NOT access their internal data. In TypeScript, enforce this with private constructors or closures.
Memory management: Unlimited undo history can exhaust memory. Implement a fixed-size ring buffer or time-limited history:
class BoundedHistory { private history: Memento[] = []; constructor(private readonly maxSize: number) {} push(memento: Memento): void { this.history.push(memento); if (this.history.length > this.maxSize) { this.history.shift(); // drop oldest } } }
Anti-patterns:
- Memento that exposes mutable references to internal state — snapshots must be copies
- Caretaker that inspects memento contents — violates encapsulation
- Saving mementos too frequently (e.g., on every keystroke without debounce) — throttle or batch snapshots
Memento vs. Command: Command stores the operation needed to undo an action. Memento stores a complete state snapshot. Command is more memory-efficient for simple state changes; Memento is simpler to implement when state is complex and hard to invert.
Source
refactoring.guru/design-patterns/memento
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.