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