specmem-hardwicksoftware 3.7.11 → 3.7.13

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.
package/bootstrap.cjs CHANGED
@@ -357,8 +357,10 @@ function killStaleBootstraps() {
357
357
  const currentPid = process.pid;
358
358
  const projectPath = process.env.SPECMEM_PROJECT_PATH;
359
359
 
360
+ if (!projectPath) return; // Can't scope without a project path
361
+
360
362
  try {
361
- // Find all node processes running bootstrap.cjs from this same directory
363
+ // Find all node processes running bootstrap.cjs
362
364
  const result = execSync(
363
365
  `ps aux | grep -E "node.*bootstrap\\.cjs" | grep -v grep | awk '{print $2}'`,
364
366
  { encoding: 'utf8', timeout: 5000 }
@@ -371,37 +373,55 @@ function killStaleBootstraps() {
371
373
  const pid = parseInt(pidStr.trim(), 10);
372
374
  if (isNaN(pid) || pid === currentPid) continue;
373
375
 
374
- // Check if this process is from the same project directory
376
+ // Check if this process serves the SAME project by reading its
377
+ // SPECMEM_PROJECT_PATH env var — this is the definitive project identity,
378
+ // not cwd which can differ from the actual project being served.
375
379
  try {
376
- const cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`);
377
- if (cwdLink === projectPath || cwdLink.startsWith(projectPath + '/')) {
378
- // CRITICAL: Only kill processes older than 30 seconds to avoid race conditions
379
- // when spawns multiple MCP server attempts in quick succession
380
- try {
381
- const stat = fs.statSync(`/proc/${pid}`);
382
- const processAge = Date.now() - stat.ctimeMs;
383
- if (processAge < 30000) {
384
- startupLog(`CLEANUP: Skipping young process ${pid} (age: ${Math.round(processAge/1000)}s < 30s)`);
385
- continue;
380
+ let otherProjectPath = null;
381
+ try {
382
+ const environ = fs.readFileSync(`/proc/${pid}/environ`, 'utf8');
383
+ const envVars = environ.split('\0');
384
+ for (const v of envVars) {
385
+ if (v.startsWith('SPECMEM_PROJECT_PATH=')) {
386
+ otherProjectPath = v.substring('SPECMEM_PROJECT_PATH='.length);
387
+ break;
386
388
  }
387
- } catch (e) {
388
- // Can't determine age, skip to be safe
389
- continue;
390
389
  }
390
+ } catch (e) {
391
+ // Can't read environ (permissions), fall back to cwd check
392
+ try {
393
+ otherProjectPath = fs.readlinkSync(`/proc/${pid}/cwd`);
394
+ } catch { continue; } // Can't determine project — skip to be safe
395
+ }
391
396
 
392
- startupLog(`CLEANUP: Killing stale bootstrap process ${pid} (cwd: ${cwdLink})`);
393
- process.kill(pid, 'SIGTERM');
394
- // Give it a moment to die gracefully, then force kill
395
- setTimeout(() => {
396
- try { process.kill(pid, 'SIGKILL'); } catch (e) { /* already dead */ }
397
- }, 1000);
397
+ if (!otherProjectPath) continue; // Can't determine skip
398
+
399
+ // STRICT: Only kill if EXACT same project path
400
+ if (otherProjectPath !== projectPath) continue;
401
+
402
+ // Only kill processes older than 30 seconds to avoid race conditions
403
+ try {
404
+ const stat = fs.statSync(`/proc/${pid}`);
405
+ const processAge = Date.now() - stat.ctimeMs;
406
+ if (processAge < 30000) {
407
+ startupLog(`CLEANUP: Skipping young process ${pid} (age: ${Math.round(processAge/1000)}s < 30s)`);
408
+ continue;
409
+ }
410
+ } catch (e) {
411
+ // Can't determine age, skip to be safe
412
+ continue;
398
413
  }
414
+
415
+ startupLog(`CLEANUP: Killing stale bootstrap for same project ${pid} (project: ${otherProjectPath})`);
416
+ process.kill(pid, 'SIGTERM');
417
+ setTimeout(() => {
418
+ try { process.kill(pid, 'SIGKILL'); } catch (e) { /* already dead */ }
419
+ }, 1000);
399
420
  } catch (e) {
400
- // Process might have died or we can't read its cwd - ignore
421
+ // Process might have died or we can't read its info - skip
401
422
  }
402
423
  }
403
424
  } catch (e) {
404
- // Cleanup is best-effort - don't fail startup if it doesn't work
405
425
  startupLog(`CLEANUP: Could not check for stale processes: ${e.message}`);
406
426
  }
407
427
  }
@@ -189,6 +189,13 @@ PROJECT_PATH = os.environ.get('SPECMEM_PROJECT_PATH', 'default')
189
189
 
190
190
  SPECMEM_HOME = os.environ.get('SPECMEM_HOME', os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
191
191
  SPECMEM_RUN_DIR = os.environ.get('SPECMEM_RUN_DIR', os.path.join(SPECMEM_HOME, 'run'))
192
+
193
+ # Bundled model: shipped with the npm package, no download needed
194
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
195
+ _BUNDLED_MODEL_DIR = os.path.join(_SCRIPT_DIR, 'models', 'all-MiniLM-L6-v2')
196
+ BUNDLED_MODEL_PATH = _BUNDLED_MODEL_DIR if os.path.isfile(os.path.join(_BUNDLED_MODEL_DIR, 'onnx', 'model_quint8_avx2.onnx')) else None
197
+ if BUNDLED_MODEL_PATH:
198
+ print(f"📦 Bundled model found: {BUNDLED_MODEL_PATH}", file=sys.stderr)
192
199
  # Socket directory: {PROJECT}/specmem/sockets/ - matches config.ts expectations
193
200
  # This is the ONLY location config.ts checks for per-project sockets
194
201
  def _get_socket_dir():
@@ -821,7 +828,8 @@ class LayerOffloadingTransformer:
821
828
  """
822
829
 
823
830
  def __init__(self, model_name: str, cache_dir: Path):
824
- self.model_name = model_name
831
+ # Use bundled model if available
832
+ self.model_name = BUNDLED_MODEL_PATH if BUNDLED_MODEL_PATH else model_name
825
833
  self.cache_dir = cache_dir
826
834
  self.tokenizer = None
827
835
  self.model_config = None
@@ -2026,7 +2034,8 @@ class FrankensteinEmbeddings:
2026
2034
  self.ram_guard = RAMGuard()
2027
2035
 
2028
2036
  # Store model name for lazy-loading
2029
- self.base_model = base_model
2037
+ # Use bundled model path if available (no network download needed)
2038
+ self.base_model = BUNDLED_MODEL_PATH if BUNDLED_MODEL_PATH else base_model
2030
2039
 
2031
2040
  # Track request time for idle cleanup
2032
2041
  self.last_request_time = time.time()
@@ -2049,10 +2058,10 @@ class FrankensteinEmbeddings:
2049
2058
  self.dim_config.native_dims = 384 # MiniLM-L6-v2 is always 384
2050
2059
  else:
2051
2060
  # EAGER MODE: Load model immediately (for high-RAM or heavyOps)
2052
- print(f"Loading model: {base_model} ({_BEST_ONNX_FILE})", file=sys.stderr)
2061
+ print(f"Loading model: {self.base_model} ({_BEST_ONNX_FILE})", file=sys.stderr)
2053
2062
  # NOTE: backend='onnx' is REQUIRED for model_kwargs file_name to work
2054
2063
  self.model = SentenceTransformer(
2055
- base_model,
2064
+ self.base_model,
2056
2065
  device='cpu',
2057
2066
  backend='onnx',
2058
2067
  cache_folder=str(self.cache_dir),
@@ -0,0 +1,7 @@
1
+ {
2
+ "word_embedding_dimension": 384,
3
+ "pooling_mode_cls_token": false,
4
+ "pooling_mode_mean_tokens": true,
5
+ "pooling_mode_max_tokens": false,
6
+ "pooling_mode_mean_sqrt_len_tokens": false
7
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "_name_or_path": "nreimers/MiniLM-L6-H384-uncased",
3
+ "architectures": [
4
+ "BertModel"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.1,
7
+ "gradient_checkpointing": false,
8
+ "hidden_act": "gelu",
9
+ "hidden_dropout_prob": 0.1,
10
+ "hidden_size": 384,
11
+ "initializer_range": 0.02,
12
+ "intermediate_size": 1536,
13
+ "layer_norm_eps": 1e-12,
14
+ "max_position_embeddings": 512,
15
+ "model_type": "bert",
16
+ "num_attention_heads": 12,
17
+ "num_hidden_layers": 6,
18
+ "pad_token_id": 0,
19
+ "position_embedding_type": "absolute",
20
+ "transformers_version": "4.8.2",
21
+ "type_vocab_size": 2,
22
+ "use_cache": true,
23
+ "vocab_size": 30522
24
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "__version__": {
3
+ "sentence_transformers": "2.0.0",
4
+ "transformers": "4.6.1",
5
+ "pytorch": "1.8.1"
6
+ }
7
+ }
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.models.Transformer"
7
+ },
8
+ {
9
+ "idx": 1,
10
+ "name": "1",
11
+ "path": "1_Pooling",
12
+ "type": "sentence_transformers.models.Pooling"
13
+ },
14
+ {
15
+ "idx": 2,
16
+ "name": "2",
17
+ "path": "2_Normalize",
18
+ "type": "sentence_transformers.models.Normalize"
19
+ }
20
+ ]
@@ -0,0 +1,4 @@
1
+ {
2
+ "max_seq_length": 256,
3
+ "do_lower_case": false
4
+ }
@@ -0,0 +1 @@
1
+ {"unk_token": "[UNK]", "sep_token": "[SEP]", "pad_token": "[PAD]", "cls_token": "[CLS]", "mask_token": "[MASK]"}