#!/usr/bin/env node
/**
* success-memory.js — Capture successful approaches, not just failures
*
* Shifts ratchet from failure-only → failure + success memory.
* For each approval in a session, snapshot the 5 turns before it:
* - Files edited
* - Tool pattern (sequence of tool names)
* - Dominant sensor values
* - Whether preceded by corrections (resolution) or clean (validation)
*
* Creates success: atoms with bonds to files and topics.
* Attention-router can query these to inject "past approach that worked".
*/
const fs = require('fs')
const path = require('path')
const { evalScheme, isCogServerUp } = require('./cogserver')
const DATA_DIR = path.join(__dirname, '..', 'data')
const LOOKBACK = 5 // turns before approval to capture
const MIN_TOOL_CALLS = 3 // need some actual activity to count
function esc (s) {
return String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ')
}
function readJSON (name) {
const file = path.join(DATA_DIR, name)
if (!fs.existsSync(file)) return null
return JSON.parse(fs.readFileSync(file, 'utf8'))
}
/**
* Extract success patterns from a session.
* A "success" = an approval turn preceded by N turns of actual work.
*
* Returns array of success records:
* { sessionId, approvalTurn, windowTurns, files, tools, correctedFirst, score }
*/
function extractSuccesses (session) {
const turns = session.turns || []
const successes = []
for (let i = 0; i < turns.length; i++) {
if (!turns[i].isApproval) continue
// Look back LOOKBACK turns
const start = Math.max(0, i - LOOKBACK)
const window = turns.slice(start, i)
if (window.length === 0) continue
// Must have actual activity
const totalToolCalls = window.reduce((s, t) => s + (t.toolCalls || 0), 0)
if (totalToolCalls < MIN_TOOL_CALLS) continue
// Files edited in the window
const files = new Set()
for (const t of window) {
for (const f of (t.editFiles || [])) files.add(f)
}
// Was this success preceded by a correction? (resolution vs clean validation)
const correctedFirst = window.some(t => t.isCorrection)
// Dominant sensor averages over window
const sensorAggs = {}
const trustedKeys = ['correction', 'approval', 'userProvidesAnswer', 'buildErrors', 'editSurvival']
for (const t of window) {
if (!t.sensors) continue
for (const k of trustedKeys) {
if (t.sensors[k] != null) {
if (!sensorAggs[k]) sensorAggs[k] = []
sensorAggs[k].push(t.sensors[k])
}
}
}
const sensors = {}
for (const [k, vals] of Object.entries(sensorAggs)) {
sensors[k] = Math.round((vals.reduce((a, b) => a + b, 0) / vals.length) * 100) / 100
}
// Average per-turn score in the window
const scores = window.map(t => t.score || 0.5)
const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length
successes.push({
sessionId: session.sessionId,
date: session.date,
cwd: session.cwd,
approvalTurn: i,
windowSize: window.length,
files: [...files],
totalToolCalls,
correctedFirst,
sensors,
avgScore: Math.round(avgScore * 1000) / 1000,
approvalText: (turns[i].text || '').slice(0, 150)
})
}
return successes
}
/**
* Classify an approval's "shape" — what kind of success was it?
* resolution: preceded by correction (we fixed something)
* validation: clean path (approach worked first try)
* partial: mid-session checkpoint (not session-end)
*/
function classifySuccess (s) {
if (s.correctedFirst) return 'resolution'
return 'validation'
}
/**
* Generate Scheme to ingest a success record.
*/
function successToScheme (success) {
const exprs = []
const sid = success.sessionId
const turn = success.approvalTurn
const id = `success:${sid}:${turn}`
const node = `(ConceptNode "${esc(id)}")`
const shape = classifySuccess(success)
exprs.push(node)
exprs.push(`(cog-set-value! ${node} (Predicate "kind") (StringValue "${shape}"))`)
exprs.push(`(cog-set-value! ${node} (Predicate "date") (StringValue "${esc(success.date)}"))`)
exprs.push(`(cog-set-value! ${node} (Predicate "turn") (FloatValue ${turn}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "window-size") (FloatValue ${success.windowSize}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "tool-calls") (FloatValue ${success.totalToolCalls}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "avg-score") (FloatValue ${success.avgScore}))`)
exprs.push(`(cog-set-value! ${node} (Predicate "approval-text") (StringValue "${esc(success.approvalText)}"))`)
// Link to session
exprs.push(`(EdgeLink (BondNode "success-in-session") (ListLink ${node} (ConceptNode "session-archive:${esc(sid)}")))`)
// Link to each file edited in the window (preferred approach)
for (const f of success.files.slice(0, 10)) {
const normalPath = f
.replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
.replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
.replace(/^\/storage\/projects\/code\//, '')
if (!normalPath) continue
const fileNode = `(ConceptNode "file:${esc(normalPath)}")`
exprs.push(fileNode)
exprs.push(`(EdgeLink (BondNode "success-touched") (ListLink ${node} ${fileNode}))`)
}
return exprs
}
async function main () {
const dryRun = process.argv.includes('--dry-run')
const detail = readJSON('fused-turns-detail.json')
if (!detail) { console.log('No fused-turns-detail'); process.exit(1) }
const sessions = detail.sessions || detail
const allSuccesses = []
for (const s of sessions) {
allSuccesses.push(...extractSuccesses(s))
}
// Distribution
const byShape = {}
const byFile = {}
let totalFiles = 0
for (const s of allSuccesses) {
const shape = classifySuccess(s)
byShape[shape] = (byShape[shape] || 0) + 1
for (const f of s.files) {
byFile[f] = (byFile[f] || 0) + 1
totalFiles++
}
}
console.log(`Extracted ${allSuccesses.length} success patterns from ${sessions.length} sessions`)
console.log(` Unique files involved in successes: ${Object.keys(byFile).length}`)
console.log('')
console.log('By shape:')
for (const [shape, count] of Object.entries(byShape).sort((a, b) => b[1] - a[1])) {
console.log(` ${shape.padEnd(12)} ${count}`)
}
console.log('\nTop files in successes (top 10):')
const topFiles = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 10)
for (const [f, c] of topFiles) {
const short = f.replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
console.log(` ${String(c).padStart(3)} ${short}`)
}
if (dryRun) {
console.log('\n--dry-run: not ingesting')
return
}
if (!await isCogServerUp()) {
console.log('\nCogServer not running, skipping ingest')
return
}
// Ingest
const allExprs = allSuccesses.flatMap(successToScheme)
console.log(`\nIngesting ${allExprs.length} Scheme expressions...`)
const BATCH = 50
let sent = 0
for (let i = 0; i < allExprs.length; i += BATCH) {
const batch = allExprs.slice(i, i + BATCH)
await evalScheme('(begin ' + batch.join(' ') + ')')
sent += batch.length
}
console.log(`Stored: ${sent} atoms/edges for ${allSuccesses.length} successes`)
}
if (require.main === module) {
main().catch(err => { console.error(err.message); process.exit(1) })
}
module.exports = { extractSuccesses, classifySuccess }