refactor: rename inner-loop-tracker to ralph-tracker with API improvements

- Rename inner-loop-tracker.ts → ralph-tracker.ts throughout codebase
- Add ralph-config.ts for parsing .claude/ralph-loop.local.md config
- Standardize API error responses using createErrorResponse()
- Add input validation for auto-compact/auto-clear thresholds
- Update UI labels to "Ralph / Todo Tracker" consistently
- Add 46 integration tests for Ralph tracking functionality
- Update test badge to 438 total tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 22:36:45 +01:00
co-authored by Claude Opus 4.5
parent 5be0babf01
commit 0d1ca37b6e
21 changed files with 2151 additions and 451 deletions
+46 -46
View File
@@ -8,7 +8,7 @@
* - `state.json`: Main app state (sessions, tasks, config)
* - `state-inner.json`: Inner loop state (todos, Ralph loop state per session)
*
* The separation reduces write frequency since inner loop state changes rapidly
* The separation reduces write frequency since Ralph state changes rapidly
* during Ralph Wiggum loops.
*
* @module state-store
@@ -17,7 +17,7 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { AppState, createInitialState, InnerSessionState, createInitialInnerSessionState } from './types.js';
import { AppState, createInitialState, RalphSessionState, createInitialRalphSessionState } from './types.js';
/** Debounce delay for batching state writes (ms) */
const SAVE_DEBOUNCE_MS = 500;
@@ -50,17 +50,17 @@ export class StateStore {
private dirty: boolean = false;
// Inner state storage (separate from main state to reduce write frequency)
private innerStates: Map<string, InnerSessionState> = new Map();
private innerStatePath: string;
private innerStateSaveTimeout: NodeJS.Timeout | null = null;
private innerStateDirty: boolean = false;
private ralphStates: Map<string, RalphSessionState> = new Map();
private ralphStatePath: string;
private ralphStateSaveTimeout: NodeJS.Timeout | null = null;
private ralphStateDirty: boolean = false;
constructor(filePath?: string) {
this.filePath = filePath || join(homedir(), '.claudeman', 'state.json');
this.innerStatePath = this.filePath.replace('.json', '-inner.json');
this.ralphStatePath = this.filePath.replace('.json', '-inner.json');
this.state = this.load();
this.state.config.stateFilePath = this.filePath;
this.loadInnerStates();
this.loadRalphStates();
}
private ensureDir(): void {
@@ -203,20 +203,20 @@ export class StateStore {
reset(): void {
this.state = createInitialState();
this.state.config.stateFilePath = this.filePath;
this.innerStates.clear();
this.ralphStates.clear();
this.saveNow(); // Immediate save for reset operations
this.saveInnerStatesNow();
this.saveRalphStatesNow();
}
// ========== Inner State Methods (Ralph Loop tracking) ==========
private loadInnerStates(): void {
private loadRalphStates(): void {
try {
if (existsSync(this.innerStatePath)) {
const data = readFileSync(this.innerStatePath, 'utf-8');
const parsed = JSON.parse(data) as Record<string, InnerSessionState>;
if (existsSync(this.ralphStatePath)) {
const data = readFileSync(this.ralphStatePath, 'utf-8');
const parsed = JSON.parse(data) as Record<string, RalphSessionState>;
for (const [sessionId, state] of Object.entries(parsed)) {
this.innerStates.set(sessionId, state);
this.ralphStates.set(sessionId, state);
}
}
} catch (err) {
@@ -225,43 +225,43 @@ export class StateStore {
}
// Debounced save for inner states
private saveInnerStates(): void {
this.innerStateDirty = true;
if (this.innerStateSaveTimeout) {
private saveRalphStates(): void {
this.ralphStateDirty = true;
if (this.ralphStateSaveTimeout) {
return; // Already scheduled
}
this.innerStateSaveTimeout = setTimeout(() => {
this.saveInnerStatesNow();
this.ralphStateSaveTimeout = setTimeout(() => {
this.saveRalphStatesNow();
}, SAVE_DEBOUNCE_MS);
}
// Immediate save for inner states
private saveInnerStatesNow(): void {
if (this.innerStateSaveTimeout) {
clearTimeout(this.innerStateSaveTimeout);
this.innerStateSaveTimeout = null;
private saveRalphStatesNow(): void {
if (this.ralphStateSaveTimeout) {
clearTimeout(this.ralphStateSaveTimeout);
this.ralphStateSaveTimeout = null;
}
if (!this.innerStateDirty) {
if (!this.ralphStateDirty) {
return;
}
this.innerStateDirty = false;
this.ralphStateDirty = false;
this.ensureDir();
const data: Record<string, InnerSessionState> = {};
for (const [sessionId, state] of this.innerStates) {
const data: Record<string, RalphSessionState> = {};
for (const [sessionId, state] of this.ralphStates) {
data[sessionId] = state;
}
writeFileSync(this.innerStatePath, JSON.stringify(data, null, 2), 'utf-8');
writeFileSync(this.ralphStatePath, JSON.stringify(data, null, 2), 'utf-8');
}
/** Returns inner state for a session, or null if not found. */
getInnerState(sessionId: string): InnerSessionState | null {
return this.innerStates.get(sessionId) || null;
getRalphState(sessionId: string): RalphSessionState | null {
return this.ralphStates.get(sessionId) || null;
}
/** Sets inner state for a session and triggers a debounced save. */
setInnerState(sessionId: string, state: InnerSessionState): void {
this.innerStates.set(sessionId, state);
this.saveInnerStates();
setRalphState(sessionId: string, state: RalphSessionState): void {
this.ralphStates.set(sessionId, state);
this.saveRalphStates();
}
/**
@@ -269,34 +269,34 @@ export class StateStore {
* Creates initial state if none exists.
* @returns The updated inner state.
*/
updateInnerState(sessionId: string, updates: Partial<InnerSessionState>): InnerSessionState {
let state = this.innerStates.get(sessionId);
updateRalphState(sessionId: string, updates: Partial<RalphSessionState>): RalphSessionState {
let state = this.ralphStates.get(sessionId);
if (!state) {
state = createInitialInnerSessionState(sessionId);
state = createInitialRalphSessionState(sessionId);
}
state = { ...state, ...updates, lastUpdated: Date.now() };
this.innerStates.set(sessionId, state);
this.saveInnerStates();
this.ralphStates.set(sessionId, state);
this.saveRalphStates();
return state;
}
/** Removes inner state for a session and triggers a debounced save. */
removeInnerState(sessionId: string): void {
if (this.innerStates.has(sessionId)) {
this.innerStates.delete(sessionId);
this.saveInnerStates();
removeRalphState(sessionId: string): void {
if (this.ralphStates.has(sessionId)) {
this.ralphStates.delete(sessionId);
this.saveRalphStates();
}
}
/** Returns a copy of all inner states as a Map. */
getAllInnerStates(): Map<string, InnerSessionState> {
return new Map(this.innerStates);
getAllRalphStates(): Map<string, RalphSessionState> {
return new Map(this.ralphStates);
}
/** Flushes all pending saves (main and inner state). Call before shutdown. */
flushAll(): void {
this.saveNow();
this.saveInnerStatesNow();
this.saveRalphStatesNow();
}
}