specmem-hardwicksoftware 3.7.29 → 3.7.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1254,25 +1254,57 @@ async function handleRequest(req, res) {
1254
1254
 
1255
1255
  pushEvent('info', `POST /v1/messages model=${body.model || '?'} msgs=${messageCount} size=${(originalSize / 1024).toFixed(0)}KB`);
1256
1256
 
1257
+ const isCompaction = isCompactionRequest(body);
1258
+ const isPassthrough = !isCompaction && (dontCompress || messageCount <= liveConfig.PRESERVE_RECENT_MESSAGES);
1259
+ let sysPromptModified = false;
1260
+
1257
1261
  // === SYSTEM PROMPT COMPRESSION ===
1262
+ // Always compress system prompt if not dontCompress — cache makes repeat calls free.
1263
+ // Cache-miss: fire-and-forget on passthrough (don't block forwarding), await on compaction/live paths.
1258
1264
  if (!dontCompress && body.system) {
1259
- try {
1260
- const sysResult = await compressSystemPrompt(body.system);
1261
- if (sysResult.charsSaved > 0) {
1262
- body.system = sysResult.system;
1263
- stats.sysPromptCharsSaved += sysResult.charsSaved;
1265
+ // Build hash to check cache without calling async function
1266
+ const _sysKey = typeof body.system === 'string' ? body.system
1267
+ : Array.isArray(body.system) ? body.system.map(b => typeof b === 'string' ? b : (b?.text || '')).join('')
1268
+ : JSON.stringify(body.system);
1269
+ const _sysHash = require('crypto').createHash('md5').update(_sysKey).digest('hex');
1270
+ const _sysCached = _sysPromptCache.get(_sysHash);
1271
+
1272
+ if (_sysCached) {
1273
+ // Cache hit — zero latency, always apply
1274
+ if (_sysCached.charsSaved > 0) {
1275
+ body.system = _sysCached.system;
1276
+ sysPromptModified = true;
1277
+ stats.sysPromptCharsSaved += _sysCached.charsSaved;
1264
1278
  stats.sysPromptCompressed++;
1265
- stats.tokensStripped += Math.floor(sysResult.charsSaved / 4);
1266
- stats.bytesStripped += sysResult.charsSaved;
1267
- log('compress', `SYSPROMPT: ${sysResult.charsSaved} chars saved`);
1268
- pushEvent('compress', `System prompt: -${sysResult.charsSaved} chars`);
1279
+ stats.tokensStripped += Math.floor(_sysCached.charsSaved / 4);
1280
+ stats.bytesStripped += _sysCached.charsSaved;
1281
+ log('compress', `SYSPROMPT (cache hit): ${_sysCached.charsSaved} chars saved`);
1282
+ pushEvent('compress', `System prompt (cached): -${_sysCached.charsSaved} chars`);
1283
+ }
1284
+ } else if (isPassthrough) {
1285
+ // Cache miss + passthrough: fire-and-forget on new thread — populates cache for next request
1286
+ compressSystemPrompt(body.system).catch(() => {});
1287
+ } else {
1288
+ // Cache miss + compaction/live: must await (need compressed body)
1289
+ try {
1290
+ const sysResult = await compressSystemPrompt(body.system);
1291
+ if (sysResult.charsSaved > 0) {
1292
+ body.system = sysResult.system;
1293
+ sysPromptModified = true;
1294
+ stats.sysPromptCharsSaved += sysResult.charsSaved;
1295
+ stats.sysPromptCompressed++;
1296
+ stats.tokensStripped += Math.floor(sysResult.charsSaved / 4);
1297
+ stats.bytesStripped += sysResult.charsSaved;
1298
+ log('compress', `SYSPROMPT: ${sysResult.charsSaved} chars saved`);
1299
+ pushEvent('compress', `System prompt: -${sysResult.charsSaved} chars`);
1300
+ }
1301
+ } catch (e) {
1302
+ log('warn', `System prompt compression failed: ${e.message}`);
1269
1303
  }
1270
- } catch (e) {
1271
- log('warn', `System prompt compression failed: ${e.message}`);
1272
1304
  }
1273
1305
  }
1274
1306
 
1275
- if (isCompactionRequest(body)) {
1307
+ if (isCompaction) {
1276
1308
  // === COMPACTION DETECTED — strip tool bodies ===
1277
1309
  stats.compactionRequests++;
1278
1310
  stats.lastCompaction = new Date().toISOString();
@@ -1284,7 +1316,7 @@ async function handleRequest(req, res) {
1284
1316
  const { strippedMessages, strippingStats } = stripMessages(body.messages);
1285
1317
  body.messages = strippedMessages;
1286
1318
 
1287
- // Also apply steno+MT compression on compaction requests
1319
+ // Run steno+MT compression in parallel (independent of strip)
1288
1320
  if (!dontCompress) {
1289
1321
  const { messages: compressed, blocksCompressed, charsCompressed, verifiedCount = 0, stenoOnlyCount = 0, tmHits: hits = 0, samples: compSamples = [] } = await compressMessagesLive(body.messages);
1290
1322
  body.messages = compressed;
@@ -1294,7 +1326,6 @@ async function handleRequest(req, res) {
1294
1326
  stats.zhRejected += stenoOnlyCount;
1295
1327
  stats.stenoOnly += (blocksCompressed - verifiedCount - stenoOnlyCount);
1296
1328
  stats.tmHits += hits;
1297
- // Store translation samples for preview
1298
1329
  if (compSamples.length > 0) stats._lastSamples = compSamples;
1299
1330
  if (blocksCompressed > 0) {
1300
1331
  pushEvent('compress', `${blocksCompressed} blocks, ${charsCompressed} chars (${verifiedCount} zh, ${stenoOnlyCount} steno, ${hits} TM)`);
@@ -1318,11 +1349,15 @@ async function handleRequest(req, res) {
1318
1349
  return;
1319
1350
  }
1320
1351
 
1321
- // === NON-COMPACTION — strip old tool_results + live MT compression ===
1322
- if (dontCompress || messageCount <= liveConfig.PRESERVE_RECENT_MESSAGES) {
1352
+ // === NON-COMPACTION — passthrough if below threshold ===
1353
+ if (isPassthrough) {
1323
1354
  stats.passthrough++;
1324
1355
  pushEvent('pass', `msgs=${messageCount} (below threshold ${liveConfig.PRESERVE_RECENT_MESSAGES})`);
1325
- forwardRequest(req, res, rawBody);
1356
+ // Use modified body if sys prompt was compressed (cache hit), else rawBody
1357
+ const passthroughBody = sysPromptModified
1358
+ ? Buffer.from(JSON.stringify(body), 'utf8')
1359
+ : rawBody;
1360
+ forwardRequest(req, res, passthroughBody);
1326
1361
  return;
1327
1362
  }
1328
1363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.29",
3
+ "version": "3.7.30",
4
4
  "type": "module",
5
5
  "description": "Your Claude Code sessions don't have to start from scratch anymore — SpecMem gives your AI real memory. It won't forget your conversations, your code, or your architecture decisions between sessions. That's the whole point. Semantic code indexing that actually works: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, HTML and more. It doesn't just track functions — it gets classes, methods, fields, constants, enums, macros, imports, structs, the whole codebase graph. There's chat memory too, powered by pgvector embeddings. You've also got token compression, team coordination, multi-agent comms, and file watching built in. 74+ MCP tools. Runs on PostgreSQL + Docker. It's kind of a big deal. justcalljon.pro",
6
6
  "main": "dist/index.js",
@@ -3202,11 +3202,11 @@ const TIER_CONFIG = {
3202
3202
  const MAX_EMBED_CHARS = 8000;
3203
3203
 
3204
3204
  // ============================================================================
3205
- // STAGE 1: PROJECT ANALYSIS
3205
+ // STAGE 2: PROJECT ANALYSIS
3206
3206
  // ============================================================================
3207
3207
 
3208
3208
  async function analyzeProject(projectPath, ui) {
3209
- ui.setStage(1, 'PROJECT ANALYSIS');
3209
+ ui.setStage(2, 'PROJECT ANALYSIS');
3210
3210
 
3211
3211
  const results = {
3212
3212
  tier: 'small',
@@ -3386,11 +3386,11 @@ async function analyzeProject(projectPath, ui) {
3386
3386
  }
3387
3387
 
3388
3388
  // ============================================================================
3389
- // STAGE 2: SCORCHED EARTH - Wipe everything and rebuild fresh
3389
+ // STAGE 3: SCORCHED EARTH - Wipe everything and rebuild fresh
3390
3390
  // ============================================================================
3391
3391
 
3392
3392
  async function scorchedEarth(projectPath, ui) {
3393
- ui.setStage(2, 'CLEANUP');
3393
+ ui.setStage(3, 'CLEANUP');
3394
3394
 
3395
3395
  const specmemDir = path.join(projectPath, 'specmem');
3396
3396
  const projectDir = path.join(projectPath, '.claude');
@@ -3539,11 +3539,11 @@ async function scorchedEarth(projectPath, ui) {
3539
3539
  }
3540
3540
 
3541
3541
  // ============================================================================
3542
- // STAGE 5: MODEL OPTIMIZATION
3542
+ // STAGE 4: MODEL OPTIMIZATION
3543
3543
  // ============================================================================
3544
3544
 
3545
3545
  async function optimizeModel(projectPath, analysis, ui) {
3546
- ui.setStage(3, 'BLAST OFF 🚀');
3546
+ ui.setStage(4, 'BLAST OFF 🚀');
3547
3547
 
3548
3548
  const recommended = TIER_CONFIG[analysis.tier];
3549
3549
 
@@ -3750,7 +3750,7 @@ async function optimizeModel(projectPath, analysis, ui) {
3750
3750
  * This ensures the overflow queue is pre-populated before launches.
3751
3751
  */
3752
3752
  async function coldStartEmbeddingDocker(projectPath, modelConfig, ui, codebaseResult) {
3753
- ui.setStage(4, 'EMBEDDING DOCKER');
3753
+ ui.setStage(5, 'EMBEDDING DOCKER');
3754
3754
 
3755
3755
  // ═══════════════════════════════════════════════════════════════════════════
3756
3756
  // 🔒 PRE-FLIGHT ACK CHECK - Verify optimizations before starting embedding 🔒
@@ -4129,7 +4129,7 @@ async function waitForEmbeddingReady(sockPath, opts = {}) {
4129
4129
  }
4130
4130
 
4131
4131
  async function indexCodebase(projectPath, ui, embeddingResult) {
4132
- ui.setStage(5, 'CODEBASE INDEXING');
4132
+ ui.setStage(6, 'CODEBASE INDEXING');
4133
4133
 
4134
4134
  const { Pool } = require('pg');
4135
4135
  const crypto = require('crypto');
@@ -5982,11 +5982,11 @@ async function extractDefinitions(content, filePath, language, fileId) {
5982
5982
  }
5983
5983
 
5984
5984
  // ============================================================================
5985
- // STAGE 6: TOKEN COMPRESSION
5985
+ // STAGE 7: TOKEN COMPRESSION
5986
5986
  // ============================================================================
5987
5987
 
5988
5988
  async function compressTokens(projectPath, ui) {
5989
- ui.setStage(7, 'TOKEN COMPRESSION');
5989
+ ui.setStage(8, 'TOKEN COMPRESSION');
5990
5990
 
5991
5991
  // Import compressor inline
5992
5992
  let compress;
@@ -6085,11 +6085,11 @@ async function compressTokens(projectPath, ui) {
6085
6085
 
6086
6086
 
6087
6087
  // ============================================================================
6088
- // STAGE 7: COMMAND DEPLOYMENT
6088
+ // STAGE 8: COMMAND DEPLOYMENT
6089
6089
  // ============================================================================
6090
6090
 
6091
6091
  async function deployCommands(projectPath, ui) {
6092
- ui.setStage(8, 'COMMAND DEPLOYMENT');
6092
+ ui.setStage(9, 'COMMAND DEPLOYMENT');
6093
6093
 
6094
6094
  const globalCmdsDir = path.join(os.homedir(), '.claude', 'commands');
6095
6095
  const projectCmdsDir = path.join(projectPath, '.claude', 'commands');
@@ -6167,7 +6167,7 @@ async function deployCommands(projectPath, ui) {
6167
6167
  * so they're searchable via find_memory from the start.
6168
6168
  */
6169
6169
  async function extractSessions(projectPath, ui, embeddingResult = null) {
6170
- ui.setStage(6, 'SESSION EXTRACTION');
6170
+ ui.setStage(7, 'SESSION EXTRACTION');
6171
6171
 
6172
6172
  const claudeDir = path.join(os.homedir(), '.claude');
6173
6173
  const projectsDir = path.join(claudeDir, 'projects');
@@ -6812,11 +6812,11 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
6812
6812
  }
6813
6813
 
6814
6814
  // ============================================================================
6815
- // STAGE 9: FINAL VERIFICATION
6815
+ // STAGE 10: FINAL VERIFICATION
6816
6816
  // ============================================================================
6817
6817
 
6818
6818
  async function finalVerification(projectPath, analysis, modelConfig, ui) {
6819
- ui.setStage(9, 'FINAL VERIFICATION');
6819
+ ui.setStage(10, 'FINAL VERIFICATION');
6820
6820
 
6821
6821
  const checks = {
6822
6822
  modelConfig: false,
@@ -8955,32 +8955,44 @@ function registerProjectInRegistry(projectPath, globalDir) {
8955
8955
  }
8956
8956
 
8957
8957
  // ============================================================================
8958
- // MODEL DOWNLOAD — ensures ML models exist before init proceeds
8958
+ // MODEL DOWNLOAD — Stage 1: ensures all ML models exist before init proceeds
8959
8959
  // ============================================================================
8960
8960
 
8961
8961
  /**
8962
- * Checks if ML models are present in the install directory.
8963
- * If missing (npm strips them), downloads from GitHub release.
8964
- * Nohup mode: auto-accepts without prompting.
8962
+ * Stage 1: MODEL DOWNLOAD
8963
+ * Checks for all required ML models. If any are missing, auto-downloads
8964
+ * the combined models tarball from GitHub release — no prompting.
8965
+ *
8966
+ * Models bundled in npm (always present after install):
8967
+ * - all-MiniLM-L6-v2 (embedding)
8968
+ * - minisbd (sentence boundary)
8969
+ *
8970
+ * Models NOT in npm (LFS-only, downloaded via release tarball):
8971
+ * - pythia-410m-onnx-quant (mini-COT)
8972
+ * - argos-translate (translation en↔zh/zt)
8965
8973
  */
8966
- async function ensureModels() {
8974
+ async function ensureModels(ui) {
8967
8975
  const specmemRoot = path.resolve(__dirname, '..');
8968
- const sentinelFile = path.join(specmemRoot, 'embedding-sandbox', 'models', 'all-MiniLM-L6-v2', 'onnx', 'model_quint8_avx2.onnx');
8976
+ const modelsDir = path.join(specmemRoot, 'embedding-sandbox', 'models');
8977
+ const version = SPECMEM_VERSION;
8969
8978
 
8970
- if (fs.existsSync(sentinelFile)) {
8971
- initLog('[MODELS] Models present skipping download');
8972
- console.log(` ${c.green}✓${c.reset} ${c.dim}ML models OK${c.reset}`);
8979
+ const sentinels = [
8980
+ { name: 'Mini-COT (Pythia)', path: path.join(modelsDir, 'pythia-410m-onnx-quant', 'model_quantized.onnx') },
8981
+ { name: 'Translation (Argos)', path: path.join(modelsDir, 'argos-translate', 'translate-en_zh-1_9', 'model', 'model.bin') },
8982
+ ];
8983
+
8984
+ const missing = sentinels.filter(s => !fs.existsSync(s.path));
8985
+
8986
+ if (missing.length === 0) {
8987
+ initLog('[MODELS] All models present — skipping download');
8988
+ if (ui) ui.setSubStatus('✓ All ML models present');
8973
8989
  return;
8974
8990
  }
8975
8991
 
8976
- initLog('[MODELS] Sentinel file missing: ' + sentinelFile);
8977
-
8978
- const nohup = process.argv.includes('--nohup') || process.env.SPECMEM_NOHUP === '1';
8979
- const modelsDir = path.join(specmemRoot, 'embedding-sandbox', 'models');
8980
- const version = SPECMEM_VERSION;
8981
- const releaseUrl = `https://github.com/jonhardwick-spec/specmem/releases/download/v${version}/specmem-models-${version}.tar.gz`;
8992
+ initLog(`[MODELS] Missing: ${missing.map(s => s.name).join(', ')}`);
8993
+ if (ui) ui.setSubStatus(`↓ Downloading ML models (~570MB)...`);
8982
8994
 
8983
- // Check if install path is globally owned (needs sudo for extraction)
8995
+ // Check write perms
8984
8996
  let needsSudo = false;
8985
8997
  try {
8986
8998
  fs.accessSync(path.join(specmemRoot, 'embedding-sandbox'), fs.constants.W_OK);
@@ -8988,102 +9000,62 @@ async function ensureModels() {
8988
9000
  needsSudo = true;
8989
9001
  }
8990
9002
 
8991
- if (!nohup) {
8992
- console.log('');
8993
- console.log(drawBox([
8994
- `${c.bold}${c.yellow}ML Models Required${c.reset}`,
8995
- '',
8996
- `SpecMem needs to download ML models (~500MB).`,
8997
- `These are stripped from npm to keep installs fast.`,
8998
- '',
8999
- `${c.dim}Source: GitHub release v${version}${c.reset}`,
9000
- needsSudo ? `${c.yellow}Note: Install path requires sudo for extraction${c.reset}` : '',
9001
- ].filter(Boolean), { borderColor: c.yellow }));
9002
-
9003
- const readline = require('readline');
9004
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
9005
-
9006
- const answer = await new Promise((resolve) => {
9007
- rl.question(` ${c.bold}Download models?${c.reset} [Y/n] `, (ans) => {
9008
- rl.close();
9009
- resolve(ans.trim().toLowerCase());
9010
- });
9011
- });
9012
-
9013
- if (answer === 'n' || answer === 'no') {
9014
- console.log(` ${c.yellow}⚠${c.reset} Skipped model download — embedding features will not work.`);
9015
- initLog('[MODELS] User declined model download');
9016
- return;
9017
- }
9018
- } else {
9019
- console.log(` ${c.dim}[nohup] Auto-downloading ML models (~500MB)...${c.reset}`);
9020
- }
9021
-
9022
- // Download
9023
- const tmpTarball = path.join(os.tmpdir(), `specmem-models-${version}.tar.gz`);
9024
-
9025
- // Prefer curl, fall back to wget
9026
9003
  const hasCurl = (() => { try { execSync('which curl', { stdio: 'pipe' }); return true; } catch { return false; } })();
9027
9004
  const hasWget = (() => { try { execSync('which wget', { stdio: 'pipe' }); return true; } catch { return false; } })();
9028
9005
 
9029
9006
  if (!hasCurl && !hasWget) {
9030
- console.log(` ${c.red}✗${c.reset} Neither curl nor wget found — cannot download models.`);
9031
- console.log(` ${c.dim}Install curl or wget and re-run specmem init.${c.reset}`);
9032
9007
  initLog('[MODELS] ERROR: no curl or wget');
9008
+ if (ui) ui.setSubStatus('⚠ curl/wget not found — ML models unavailable (mini-COT/translation disabled)');
9033
9009
  return;
9034
9010
  }
9035
9011
 
9036
- console.log(` ${c.cyan}↓${c.reset} Downloading models from GitHub release v${version}...`);
9037
- initLog('[MODELS] Downloading: ' + releaseUrl);
9012
+ const releaseUrl = `https://github.com/jonhardwick-spec/specmem/releases/download/v${version}/specmem-models-${version}.tar.gz`;
9013
+ const tmpTarball = path.join(os.tmpdir(), `specmem-models-${version}.tar.gz`);
9014
+
9015
+ initLog(`[MODELS] Downloading: ${releaseUrl}`);
9038
9016
 
9039
9017
  try {
9040
9018
  const dlCmd = hasCurl
9041
9019
  ? `curl -fSL --progress-bar -o "${tmpTarball}" "${releaseUrl}"`
9042
9020
  : `wget --progress=bar:force -O "${tmpTarball}" "${releaseUrl}"`;
9043
-
9044
- execSync(dlCmd, { stdio: 'inherit', timeout: 600000 });
9021
+ execSync(dlCmd, { stdio: 'pipe', timeout: 900000 }); // 15 min timeout for ~570MB
9045
9022
  } catch (e) {
9046
- console.log(` ${c.red}✗${c.reset} Download failed: ${e.message}`);
9047
- initLog('[MODELS] Download failed: ' + e.message);
9023
+ initLog(`[MODELS] Download failed: ${e.message}`);
9024
+ if (ui) ui.setSubStatus(`⚠ Model download failed mini-COT/translation disabled`);
9048
9025
  try { fs.unlinkSync(tmpTarball); } catch {}
9049
9026
  return;
9050
9027
  }
9051
9028
 
9052
- // Extract
9053
- console.log(` ${c.cyan}⤳${c.reset} Extracting models...`);
9054
- initLog('[MODELS] Extracting to: ' + modelsDir);
9029
+ if (ui) ui.setSubStatus('⤳ Extracting ML models...');
9030
+ initLog(`[MODELS] Extracting to: ${modelsDir}`);
9055
9031
 
9056
9032
  try {
9057
- // Ensure models directory exists
9058
9033
  const mkdirCmd = `mkdir -p "${modelsDir}"`;
9059
9034
  const extractCmd = `tar xzf "${tmpTarball}" -C "${modelsDir}"`;
9060
-
9061
9035
  if (needsSudo) {
9062
9036
  execSync(`sudo ${mkdirCmd}`, { stdio: 'pipe' });
9063
- execSync(`sudo ${extractCmd}`, { stdio: 'inherit', timeout: 120000 });
9064
- // Fix ownership so specmem can read
9037
+ execSync(`sudo ${extractCmd}`, { stdio: 'pipe', timeout: 180000 });
9065
9038
  execSync(`sudo chmod -R a+rX "${modelsDir}"`, { stdio: 'pipe' });
9066
9039
  } else {
9067
9040
  execSync(mkdirCmd, { stdio: 'pipe' });
9068
- execSync(extractCmd, { stdio: 'inherit', timeout: 120000 });
9041
+ execSync(extractCmd, { stdio: 'pipe', timeout: 180000 });
9069
9042
  }
9070
9043
  } catch (e) {
9071
- console.log(` ${c.red}✗${c.reset} Extraction failed: ${e.message}`);
9072
- initLog('[MODELS] Extraction failed: ' + e.message);
9044
+ initLog(`[MODELS] Extraction failed: ${e.message}`);
9045
+ if (ui) ui.setSubStatus(' Model extraction failed');
9073
9046
  try { fs.unlinkSync(tmpTarball); } catch {}
9074
9047
  return;
9075
9048
  }
9076
9049
 
9077
- // Cleanup tarball
9078
9050
  try { fs.unlinkSync(tmpTarball); } catch {}
9079
9051
 
9080
- // Verify
9081
- if (fs.existsSync(sentinelFile)) {
9082
- console.log(` ${c.green}✓${c.reset} Models downloaded and extracted successfully`);
9083
- initLog('[MODELS] Success sentinel verified');
9052
+ const stillMissing = sentinels.filter(s => !fs.existsSync(s.path));
9053
+ if (stillMissing.length === 0) {
9054
+ initLog('[MODELS] All models OK after download');
9055
+ if (ui) ui.setSubStatus(' ML models downloaded and ready');
9084
9056
  } else {
9085
- console.log(` ${c.yellow}⚠${c.reset} Extraction completed but sentinel file not found models may be incomplete`);
9086
- initLog('[MODELS] WARNING: sentinel missing after extraction');
9057
+ initLog(`[MODELS] Still missing after download: ${stillMissing.map(s => s.name).join(', ')}`);
9058
+ if (ui) ui.setSubStatus(`⚠ Still missing: ${stillMissing.map(s => s.name).join(', ')}`);
9087
9059
  }
9088
9060
  }
9089
9061
 
@@ -9193,9 +9165,6 @@ async function main() {
9193
9165
  }
9194
9166
  }
9195
9167
 
9196
- // ========== ENSURE ML MODELS ==========
9197
- await ensureModels();
9198
-
9199
9168
  // Animated banner with sliding red highlight (screen already cleared at startup)
9200
9169
  const _dbg = (m) => { try { fs.appendFileSync('/tmp/init-trace.log', `${Date.now()} ${m}\n`); } catch {} };
9201
9170
  _dbg('PRE-BANNER');
@@ -9858,16 +9827,22 @@ ${lastOutput}
9858
9827
 
9859
9828
  // Adjust total stages based on mode
9860
9829
  if (!launchScreens) {
9861
- ui.totalStages = 9; // No screen sessions stage
9830
+ ui.totalStages = 10; // No screen sessions stage
9862
9831
  } else if (skipScorchedEarth) {
9863
- ui.totalStages = 2; // Quick mode: just analyze + screens
9832
+ ui.totalStages = 3; // Quick mode: models + analyze + screens
9864
9833
  } else {
9865
- ui.totalStages = 10; // Full mode with screen sessions
9834
+ ui.totalStages = 11; // Full mode with screen sessions
9866
9835
  }
9867
9836
 
9868
9837
  ui.start();
9869
9838
  _dbg('POST-UI-START');
9870
9839
 
9840
+ // ==========================================================================
9841
+ // STAGE 1: MODEL DOWNLOAD — auto-fetch any missing ML models
9842
+ // ==========================================================================
9843
+ ui.setStage(1, 'MODEL DOWNLOAD');
9844
+ await ensureModels(ui);
9845
+
9871
9846
  // ==========================================================================
9872
9847
  // CONTAINER FAST-PATH: If podman/docker is available, use container mode
9873
9848
  // This replaces stages 2-9 with: pull image → start container → deploy hooks → verify
@@ -9953,14 +9928,14 @@ ${lastOutput}
9953
9928
  }
9954
9929
 
9955
9930
  if (useContainerMode && !skipScorchedEarth) {
9956
- // Container mode: 5 stages (+ optional codebase indexing + screens)
9957
- ui.totalStages = launchScreens ? 7 : 6;
9931
+ // Container mode: Stage 1 = models, then container stages
9932
+ ui.totalStages = launchScreens ? 8 : 7;
9958
9933
 
9959
- // Stage 1: Analyze project
9934
+ // Stage 2: Analyze project
9960
9935
  const analysis = await analyzeProject(projectPath, ui);
9961
9936
 
9962
- // Stage 2: Container engine — pull image if needed
9963
- ui.setStage(2, 'CONTAINER ENGINE');
9937
+ // Stage 3: Container engine — pull image if needed
9938
+ ui.setStage(3, 'CONTAINER ENGINE');
9964
9939
  ui.setStatus('Pulling specmem-brain image...');
9965
9940
  const containerImage = process.env.SPECMEM_CONTAINER_IMAGE || 'ghcr.io/hardwicksoftware/specmem-brain:latest';
9966
9941
  const rtCmd = (() => {
@@ -10012,8 +9987,8 @@ ${lastOutput}
10012
9987
  console.log('');
10013
9988
  process.exit(1);
10014
9989
  } else {
10015
- // Stage 3: Start container
10016
- ui.setStage(3, 'CONTAINER START');
9990
+ // Stage 4: Start container
9991
+ ui.setStage(4, 'CONTAINER START');
10017
9992
  ui.setStatus('Starting specmem-brain...');
10018
9993
  const dirName = path.basename(projectPath).replace(/[^a-zA-Z0-9_-]/g, '_');
10019
9994
  const containerName = `specmem-brain-${dirName}`;
@@ -10412,8 +10387,8 @@ priority=999
10412
10387
  process.env.SPECMEM_TRANSLATE_SOCKET = path.join(runDir, 'translate.sock');
10413
10388
  process.env.SPECMEM_MINICOT_SOCKET = path.join(runDir, 'minicot.sock');
10414
10389
 
10415
- // Stage 4: PostgreSQL client tools (psql) — hooks shell out to psql for DB queries
10416
- ui.setStage(4, 'PSQL CLIENT');
10390
+ // Stage 5: PostgreSQL client tools (psql) — hooks shell out to psql for DB queries
10391
+ ui.setStage(5, 'PSQL CLIENT');
10417
10392
  ui.setStatus('Checking psql...');
10418
10393
 
10419
10394
  let hasPsqlClient = false;
@@ -10512,8 +10487,8 @@ priority=999
10512
10487
  console.log(` ${c.dim}Hooks that query the DB will fail without it.${c.reset}`);
10513
10488
  }
10514
10489
 
10515
- // Stage 5: Deploy hooks + MCP config + verify
10516
- ui.setStage(5, 'HOOKS & CONFIG');
10490
+ // Stage 6: Deploy hooks + MCP config + verify
10491
+ ui.setStage(6, 'HOOKS & CONFIG');
10517
10492
  ui.setStatus('Deploying Claude hooks...');
10518
10493
  syncHooksAndSettings();
10519
10494
  ui.setStatus('Hooks deployed');
@@ -10619,9 +10594,9 @@ priority=999
10619
10594
  }, null, 2));
10620
10595
  } catch {}
10621
10596
 
10622
- // Stage 6: CODEBASE INDEXING
10623
- _dbg('STAGE-6-CODEBASE-INDEXING');
10624
- ui.setStage(6, 'CODEBASE INDEXING');
10597
+ // Stage 7: CODEBASE INDEXING
10598
+ _dbg('STAGE-7-CODEBASE-INDEXING');
10599
+ ui.setStage(7, 'CODEBASE INDEXING');
10625
10600
  ui.enableFileFeed(true);
10626
10601
  ui.setStatus('Waiting for container services...');
10627
10602
 
@@ -1,6 +1,6 @@
1
1
  ; ============================================
2
2
  ; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
3
- ; Generated by specmem-init at 2026-02-22T16:24:03.901Z
3
+ ; Generated by specmem-init at 2026-02-22T17:39:56.598Z
4
4
  ; Thread counts from model-config.json resourcePool
5
5
  ; ============================================
6
6