#!/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))