OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / session-start.js

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

/**
 * session-start.js — Start CogServer and ingest data at session begin
 *
 * Called from SessionStart hook. Async — does not block session.
 * Starts CogServer if not running, then ingests codetree + ratchet data.
 * Fails silently if CogServer binary not found.
 */

const { execSync, spawn } = require('child_process')
const { isCogServerUp, evalScheme } = require('./cogserver')
const { ingestAll } = require('./ingest')

const COGSERVER_PORT = 17001
const STARTUP_WAIT = 2000

async function startCogServerIfNeeded () {
  if (await isCogServerUp(COGSERVER_PORT)) {
    return true
  }

  // Check if cogserver binary exists
  try {
    execSync('which cogserver', { encoding: 'utf8', stdio: 'pipe' })
  } catch {
    return false // no cogserver binary
  }

  // Start CogServer in background
  try {
    const child = spawn('cogserver', [], {
      detached: true,
      stdio: 'ignore'
    })
    child.unref()

    // Wait for it to come up
    const deadline = Date.now() + STARTUP_WAIT
    while (Date.now() < deadline) {
      if (await isCogServerUp(COGSERVER_PORT)) return true
      await new Promise(r => setTimeout(r, 200))
    }
    return await isCogServerUp(COGSERVER_PORT)
  } catch {
    return false
  }
}

/**
 * Create a session atom tagged with a UUID so streaming hooks can link to it.
 * Writes the session UUID to /tmp/claude-atomspace-session so hooks can read it.
 */
async function createSessionAtom () {
  const crypto = require('crypto')
  const fs = require('fs')
  const sessionId = crypto.randomBytes(8).toString('hex')
  const startTime = Math.floor(Date.now() / 1000)
  const cwd = process.cwd()

  const sessionNode = `(ConceptNode "session:${sessionId}")`
  const exprs = [
    sessionNode,
    `(cog-set-value! ${sessionNode} (Predicate "start-time") (FloatValue ${startTime}))`,
    `(cog-set-value! ${sessionNode} (Predicate "cwd") (StringValue "${cwd.replace(/"/g, '\\"')}"))`,
    `(cog-set-value! ${sessionNode} (Predicate "turn") (FloatValue 0))`,
    `(cog-set-value! ${sessionNode} (Predicate "correction-count") (FloatValue 0))`
  ]

  await evalScheme('(begin ' + exprs.join(' ') + ')')

  // Write session ID to a known path so hooks can find it
  fs.writeFileSync('/tmp/claude-atomspace-session', sessionId)
  return sessionId
}

async function main () {
  const started = await startCogServerIfNeeded()
  if (!started) {
    // CogServer not available — silent exit
    process.exit(0)
  }

  // Run ingest
  try {
    const result = await ingestAll()
    if (result.ok) {
      const atoms = (result.codetree ? result.codetree.sent || 0 : 0) +
                    (result.ratchet ? result.ratchet.sent || 0 : 0)
      process.stderr.write(`[AtomSpace] Loaded ${atoms} atoms (${result.elapsed}ms)\n`)
    }
  } catch (err) {
    process.stderr.write(`[AtomSpace] Ingest error: ${err.message}\n`)
  }

  // Create session atom for streaming observability
  try {
    const sessionId = await createSessionAtom()
    process.stderr.write(`[AtomSpace] Session: ${sessionId}\n`)
  } catch (err) {
    process.stderr.write(`[AtomSpace] Session init error: ${err.message}\n`)
  }

  // Phase 3.2: Continuation pre-loading
  // Detect if this session continues a prior one and inject context
  try {
    await preloadContinuationContext()
  } catch (err) {
    // Non-fatal
  }

  process.exit(0)
}

/**
 * If recent sessions (within 7 days) share the current cwd, surface
 * context from them at session start: unresolved loops, dominant topics,
 * last session's outcome.
 */
async function preloadContinuationContext () {
  const fs = require('fs')
  const path = require('path')
  const cwd = process.cwd()

  // Read recent session data from fused-scores
  const fusedFile = path.join(__dirname, '..', 'data', 'fused-scores.json')
  if (!fs.existsSync(fusedFile)) return
  const fused = JSON.parse(fs.readFileSync(fusedFile, 'utf8'))

  // Find recent sessions in same cwd within 7 days
  const now = Date.now()
  const sevenDaysMs = 7 * 24 * 3600 * 1000
  const recent = (fused.sessions || []).filter(s => {
    if (s.cwd !== cwd) return false
    const ts = new Date(s.timestamp || s.date).getTime()
    return (now - ts) <= sevenDaysMs
  }).sort((a, b) =>
    new Date(b.timestamp || b.date).getTime() - new Date(a.timestamp || a.date).getTime()
  )

  if (recent.length === 0) return

  // Pull loops from same cwd
  const loopsFile = path.join(__dirname, '..', 'data', 'loops.json')
  let unresolvedLoops = []
  if (fs.existsSync(loopsFile)) {
    const loops = JSON.parse(fs.readFileSync(loopsFile, 'utf8'))
    const priorSessionIds = new Set(recent.map(s => s.sessionId))
    unresolvedLoops = (loops.loops || []).filter(l =>
      priorSessionIds.has(l.sessionId) && !l.resolved
    )
  }

  // Build briefing
  const lines = []
  lines.push('─── PRIOR CONTEXT (last 7 days, same cwd) ───')
  lines.push(`${recent.length} prior session(s) on this cwd`)

  // Most recent session summary
  const last = recent[0]
  const hoursAgo = Math.round((now - new Date(last.timestamp || last.date).getTime()) / 1000 / 3600)
  lines.push(`Last session: ${hoursAgo}h ago, ${last.turns} turns, ${last.corrections} corrections`)
  if (last.trustedScore != null) lines.push(`  trusted-score: ${last.trustedScore}`)

  // Unresolved loops
  if (unresolvedLoops.length > 0) {
    lines.push(`Unresolved from prior sessions: ${unresolvedLoops.length}`)
    for (const l of unresolvedLoops.slice(0, 3)) {
      const firstCorr = (l.corrections && l.corrections[0] || '').slice(0, 80)
      lines.push(`  ✗ [${l.type}] "${firstCorr}..."`)
    }
  }

  // Top files across recent sessions (likely still relevant)
  const fileCount = {}
  for (const s of recent) {
    // We don't have per-session editedFiles here but loop filesTried is a proxy
    const sLoops = (unresolvedLoops.length > 0)
      ? [] // skip if we already reported
      : []
  }

  process.stderr.write(lines.join('\n') + '\n')
}

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