OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / success-memory.js

223 lines · javascript ref:
#!/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 }