OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / stream-correction.js

200 lines · javascript ref:
#!/usr/bin/env node

/**
 * stream-correction.js — Stream corrections into AtomSpace in real-time
 *
 * Runs parallel to detect-correction.js as an async UserPromptSubmit hook.
 * On detected correction, creates a live atom linked to current session.
 *
 * Silent if:
 *   - CogServer not running
 *   - No session atom (session-start hook didn't fire)
 *   - No correction pattern matched
 */

const fs = require('fs')
const path = require('path')
const { isCogServerUp, evalScheme } = require('./cogserver')

const SESSION_FILE = '/tmp/claude-atomspace-session'
const SESSION_STATE_FILE = '/tmp/claude-session-score.json'

const CORRECTION_PATTERNS = [
  /\b(no|wrong|not that|that's not|incorrect|stop doing)\b/i,
  /\b(i said|i meant|i asked for|that's wrong)\b/i,
  /\b(it's actually|you should use|the function is|look at|use .+ instead)\b/i,
  /\b(again|already told you|how many times|we discussed this)\b/i,
  /\b(don't ever|never do|stop|please don't|do not)\b/i
]

const APPROVAL_PATTERNS = [
  /\b(perfect|exactly|great|yes|lgtm|looks good|nice|thanks)\b/i
]

function esc (s) {
  return String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ')
}

function readSessionId () {
  try { return fs.readFileSync(SESSION_FILE, 'utf8').trim() } catch { return null }
}

function readCurrentTurn () {
  try {
    const state = JSON.parse(fs.readFileSync(SESSION_STATE_FILE, 'utf8'))
    return state.turns || 0
  } catch { return 0 }
}

function readRecentEdits () {
  try {
    const state = JSON.parse(fs.readFileSync(SESSION_STATE_FILE, 'utf8'))
    return state.editedFiles || state._lastTurnEdits || []
  } catch { return [] }
}

/**
 * Query AtomSpace for files edited in this session (live source of truth).
 */
async function getSessionEditedFiles (sessionId) {
  try {
    const result = await evalScheme(
      `(cog-execute! (Meet (TypedVariable (Variable "$f") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "session-edited") (ListLink (ConceptNode "session:${sessionId}") (Variable "$f"))))))`)
    const matches = (result || '').match(/(?:Concept|ConceptNode) "file:([^"]+)"/g) || []
    return matches.map(m => m.match(/"file:([^"]+)"/)[1])
  } catch { return [] }
}

async function main () {
  // Read prompt
  let input = ''
  for await (const chunk of process.stdin) input += chunk

  let data
  try { data = JSON.parse(input) } catch { process.exit(0) }

  const message = data.prompt || data.user_prompt || data.message || ''
  if (!message || message.length < 3) process.exit(0)

  // Skip analyzer prompts (prevent feedback loop)
  if (message.includes('You are analyzing a user') ||
      message.includes('REGEX CLASSIFIED AS') ||
      message.includes('respond with EXACTLY one line')) {
    process.exit(0)
  }

  const isCorrection = CORRECTION_PATTERNS.some(p => p.test(message))
  const isApproval = APPROVAL_PATTERNS.some(p => p.test(message))
  if (!isCorrection && !isApproval) process.exit(0)

  // Check if CogServer + session are ready
  if (!await isCogServerUp()) process.exit(0)
  const sessionId = readSessionId()
  if (!sessionId) process.exit(0)

  const turn = readCurrentTurn()
  const timestamp = Math.floor(Date.now() / 1000)
  const type = isCorrection ? 'correction' : 'approval'
  const truncated = message.slice(0, 300)

  // Create live correction/approval atom
  const eventId = `live:${type}:${sessionId}:${timestamp}`
  const eventNode = `(ConceptNode "${esc(eventId)}")`
  const sessionNode = `(ConceptNode "session:${sessionId}")`

  const exprs = [
    eventNode,
    `(cog-set-value! ${eventNode} (Predicate "kind") (StringValue "${type}"))`,
    `(cog-set-value! ${eventNode} (Predicate "text") (StringValue "${esc(truncated)}"))`,
    `(cog-set-value! ${eventNode} (Predicate "turn") (FloatValue ${turn}))`,
    `(cog-set-value! ${eventNode} (Predicate "timestamp") (FloatValue ${timestamp}))`,
    // Link event → session
    `(EdgeLink (BondNode "${type}-in-session") (ListLink ${eventNode} ${sessionNode}))`
  ]

  // Link event → recently edited files (from live AtomSpace session state)
  // Falls back to ratchet session state file if query fails
  let recentFiles = await getSessionEditedFiles(sessionId)
  if (recentFiles.length === 0) {
    recentFiles = readRecentEdits().map(f => f
      .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
      .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
      .replace(/^\/storage\/projects\/code\//, ''))
  }
  for (const normalPath of recentFiles.slice(0, 5)) {
    if (!normalPath) continue
    const fileNode = `(ConceptNode "file:${esc(normalPath)}")`
    exprs.push(`(EdgeLink (BondNode "${type}-near-edit") (ListLink ${eventNode} ${fileNode}))`)
  }

  // Phase 3.3: Active rule proposal — when a correction happens, propose a
  // candidate rule based on recent edits + correction text. Written to
  // data/rule-candidates.jsonl for interactive review (never auto-applied).
  if (isCorrection && recentFiles.length > 0) {
    try {
      const path = require('path')
      const candidateFile = path.join(__dirname, '..', 'data', 'rule-candidates.jsonl')
      const candidate = {
        timestamp: new Date().toISOString(),
        session: sessionId,
        symptom: truncated,
        triggerFiles: recentFiles.slice(0, 5),
        suggestedRule: null,  // filled in by review script or LLM later
        status: 'pending-review'
      }
      // Infer a rough rule suggestion
      const sampleFile = recentFiles[0]
      if (sampleFile) {
        candidate.suggestedRule =
          `When editing ${sampleFile}, watch for: "${truncated.slice(0, 80)}"`
      }
      fs.appendFileSync(candidateFile, JSON.stringify(candidate) + '\n')
    } catch {}
  }

  // Utility tracking: if this is a correction, find rules for recently-edited
  // files and increment their utility-contradictions counter.
  // Rules that were likely "shown" (file was edited) but didn't prevent the
  // correction get a signal that they may be less useful in this context.
  // NO DELETION — just a counter update.
  if (isCorrection) {
    const rulesToPenalize = new Set()
    for (const normalPath of recentFiles.slice(0, 5)) {
      if (!normalPath) continue
      // Find rule atoms for this file
      const ruleQuery = `(cog-execute! (Meet (TypedVariable (Variable "$r") (Type (quote ConceptNode))) (Present (EdgeLink (BondNode "rule-for-file") (ListLink (Variable "$r") (ConceptNode "file:${esc(normalPath)}"))))))`
      try {
        const result = await evalScheme(ruleQuery)
        const matches = (result || '').match(/(?:Concept|ConceptNode) "(rule:[^"]+)"/g) || []
        for (const m of matches) {
          const ruleName = m.match(/"(rule:[^"]+)"/)[1]
          rulesToPenalize.add(ruleName)
        }
      } catch {}
    }

    // Increment utility-contradictions for each penalized rule
    for (const ruleName of rulesToPenalize) {
      const ruleAtom = `(ConceptNode "${esc(ruleName)}")`
      exprs.push(
        `(cog-set-value! ${ruleAtom} (Predicate "utility-contradictions") ` +
        `(FloatValue (+ 1 (let ((v (cog-value ${ruleAtom} (Predicate "utility-contradictions")))) ` +
        `(if v (cog-value-ref v 0) 0)))))`
      )
    }
  }

  // Update session-level counters
  if (isCorrection) {
    exprs.push(`(cog-set-value! ${sessionNode} (Predicate "correction-count") (FloatValue ${turn}))`)
  }

  try {
    await evalScheme('(begin ' + exprs.join(' ') + ')')
  } catch {}

  process.exit(0)
}

main().catch(() => process.exit(0))