Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 1x 1x 1x 1x 1x 1x 1x 1x 14x 14x 10x 6x 6x 6x 1x 4x 3x 3x 3x 1x 5x 5x 5x 3x 3x 1x 6x 6x 5x 5x 7x 106x 5x 4x 4x 4x 4x 4x 4x 10x 10x 9x 9x 4x 4x 4x | /**
* correction-memory.ts
*
* Reads and writes the per-workspace attempt history at
* ~/.pisces/attempt-history.json.
*
* Provides:
* - appendAttempt() — validated write with silent drop on policy violation
* - getRecentGaps() — compact gap summary for injection into calibration context
*/
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { validateWrite, validateInject } from "./memory-policy";
// ─── Types ─────────────────────────────────────────────────────────────────
export interface AttemptRecord {
/** Short ID — timestamp-based base36 for easy sorting */
id: string;
/** ISO 8601 timestamp */
timestamp: string;
/** Skill that triggered the attempt (e.g. "attempt") */
skillName: string;
/** Classified attempt type (e.g. "code", "essay", "generic") */
attemptType: string;
/** What the learner was trying to achieve (truncated to 200 chars for storage) */
goal?: string;
/** Specific gaps identified — populated after model grades (initially empty) */
gaps: string[];
/** Strengths identified — populated after model grades (initially empty) */
strengths: string[];
/** 0–100 composite score — set after model grades (optional at capture time) */
score?: number;
}
// ─── Storage ───────────────────────────────────────────────────────────────
const HISTORY_DIR = path.join(os.homedir(), ".pisces");
const HISTORY_FILE = path.join(HISTORY_DIR, "attempt-history.json");
// Keep bounded to avoid unbounded growth
const MAX_RECORDS = 200;
// Gap summary budget (proxy for ≈150 tokens at ~4 chars/token)
const DEFAULT_MAX_CHARS = 600;
function load(): AttemptRecord[] {
try {
if (!fs.existsSync(HISTORY_FILE)) return [];
return JSON.parse(fs.readFileSync(HISTORY_FILE, "utf-8")) as AttemptRecord[];
} catch {
return [];
}
}
function save(records: AttemptRecord[]): void {
try {
fs.mkdirSync(HISTORY_DIR, { recursive: true });
fs.writeFileSync(HISTORY_FILE, JSON.stringify(records.slice(-MAX_RECORDS), null, 2));
} catch {
// Non-fatal — history is valuable but never critical
}
}
// ─── Public API ────────────────────────────────────────────────────────────
/**
* Appends an attempt record to history after policy validation.
* Silently drops the record if the policy check fails.
*/
export function appendAttempt(record: AttemptRecord): void {
if (!validateWrite(record).allowed) return;
const records = load();
records.push(record);
save(records);
}
/**
* Updates gaps, strengths, and score on an existing record by ID.
* Called by the turn_end write-back path after the model grades the attempt.
* No-ops silently if the record is not found.
*/
export function updateAttemptRecord(
id: string,
updates: { gaps?: string[]; strengths?: string[]; score?: number }
): void {
const records = load();
const idx = records.findIndex((r) => r.id === id);
if (idx === -1) return;
records[idx] = { ...records[idx], ...updates };
save(records);
}
/**
* Returns a compact, deduplicated summary of unresolved gaps from recent
* attempts, truncated to `maxChars` to fit within a context window budget.
* Returns an empty string if there are no gaps or the summary fails policy.
*/
export function getRecentGaps(maxChars = DEFAULT_MAX_CHARS): string {
const records = load();
if (records.length === 0) return "";
// Collect all gaps newest-first, then deduplicate
const gaps: string[] = [];
for (let i = records.length - 1; i >= 0; i--) {
for (const gap of records[i].gaps) {
gaps.push(gap);
}
}
if (gaps.length === 0) return "";
const unique = [...new Set(gaps)];
// Build summary within budget
const lines: string[] = [];
let used = 0;
const header = "Recent gaps to address:\n";
used += header.length;
for (const gap of unique) {
const line = `- ${gap}`;
if (used + line.length + 1 > maxChars) break;
lines.push(line);
used += line.length + 1;
}
Iif (lines.length === 0) return "";
const summary = header + lines.join("\n");
return validateInject(summary).allowed ? summary : "";
}
|