@alucard_24/pi-codeindex 1.0.0 → 1.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ ### Novità
6
+
7
+ - Modello di embedding in un servizio locale condiviso tra processi pi e CLI:
8
+ una sola pipeline per endpoint, avvio automatico, inferenze serializzate e
9
+ coda limitata. Nessun fallback in-process in caso di errore del servizio.
10
+ - Rilascio del modello dopo inattività (default 5 minuti), configurabile con
11
+ `serviceIdleMs` / `CODEINDEX_SERVICE_IDLE_MS`.
12
+ - `/codeindex service status` e `/codeindex service stop`, disponibili anche
13
+ dalla CLI. Autenticazione locale, verifica del profilo dopo riconnessione e
14
+ rifiuto dei conflitti di configurazione senza creare un secondo modello.
15
+ - Indicizzazione e sincronizzazione anche nelle cartelle senza repository Git.
16
+ Home e radice del disco richiedono consenso esplicito in ogni sessione.
17
+ - Diagnostica, configurazione e documentazione aggiornate; test di concorrenza,
18
+ crash, shutdown e prove opt-in con modello reale.
19
+
20
+ ### Aggiornamento
21
+
22
+ Chiudere e riaprire i vecchi processi pi dopo l'aggiornamento: le copie del
23
+ modello caricate dal vecchio runtime in-process non vengono liberate dal
24
+ nuovo servizio. Per cambiare il profilo di un servizio già attivo usare
25
+ `/codeindex service stop` quando inattivo, oppure attendere il timeout idle.
26
+ I probe espliciti di doctor restano isolati e possono caricare un modello
27
+ aggiuntivo. Endpoint diversi permettono deliberatamente istanze separate.
28
+
29
+ Il servizio è stato verificato su Linux x64 con CPU, CUDA, WebGPU automatico
30
+ e WebGPU/Vulkan; DirectML/Windows e CoreML/macOS restano da verificare sulle
31
+ rispettive piattaforme. Le misure storiche Fase 5–7 non sono nuovi benchmark
32
+ del servizio condiviso.
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # pi-codeindex
2
2
 
3
3
  Indice semantico locale del codice per l'agente pi: embedding con
4
- **EmbeddingGemma 2** (ONNX, in-process) e ricerca ibrida (coseno + lessico)
5
- che restituisce `path:righe` + simbolo + anteprima. Nessun server, nessun
6
- database: due file in `~/.cache/pi-codeindex/<sha1(repo)>/`.
4
+ **EmbeddingGemma 2** (ONNX, servizio locale condiviso) e ricerca ibrida
5
+ (coseno + lessico) che restituisce `path:righe` + simbolo + anteprima.
6
+ Una sola copia del modello fra processi pi/CLI sullo stesso endpoint;
7
+ nessun servizio remoto o database. Indice: due file in
8
+ `~/.cache/pi-codeindex/<sha1(repo)>/`.
7
9
 
8
10
  ## Requisiti
9
11
 
@@ -50,11 +52,12 @@ modificata dal pacchetto.
50
52
 
51
53
  ## Embedding e backend
52
54
 
53
- Il modello **EmbeddingGemma 2** gira localmente, in-process. I soli pesi testo
54
- vengono scaricati al primo utilizzo (anche dall'indicizzazione automatica) e
55
- riutilizzati fra repository. Nessun server o API di embedding; dopo il primo
56
- download è possibile lavorare offline. Il codice da embeddare non viene
57
- inviato al server dei pesi. Un processo pi mantiene una propria pipeline.
55
+ Il modello **EmbeddingGemma 2** gira in un servizio locale condiviso avviato
56
+ automaticamente al primo utilizzo, anche dall'indicizzazione automatica.
57
+ I client pi/CLI non caricano una pipeline propria. I soli pesi testo vengono
58
+ scaricati e riutilizzati fra repository; dopo il primo download è possibile
59
+ lavorare offline. Il codice da embeddare viaggia solo sul loopback locale,
60
+ non viene inviato al server dei pesi o a un'API di embedding remota.
58
61
 
59
62
  | `device` | Default `dtype: auto` | Stato |
60
63
  |---|---|---|
@@ -130,6 +133,60 @@ macchina Linux x64 / RTX 5070 Ti: EmbeddingGemma fp16, indice incrementale,
130
133
  ricerca su fixture e coesistenza con i provider standard. Queste prove non
131
134
  sostituiscono un benchmark di qualità o prestazioni su altri hardware.
132
135
 
136
+ ## Servizio condiviso e memoria
137
+
138
+ Il primo client sceglie il profilo secondo la configurazione; gli altri
139
+ riutilizzano la stessa pipeline se quel profilo è consentito. Un profilo
140
+ incompatibile, una cache pesi diversa o valori diversi di `cpuThreads` o
141
+ `serviceIdleMs` danno errore: **nessun secondo modello e nessun fallback
142
+ in-process**. Un device forzato può riutilizzare un servizio CPU solo con
143
+ `fallback: cpu`, con avviso esplicito. Le richieste sono serializzate, con
144
+ massimo 16 richieste attive/in coda; ogni batch contiene al massimo 128 testi
145
+ e 2 MiB di testo. La saturazione della coda produce un errore, non un altro
146
+ worker. Un crash interrompe la richiesta corrente; un uso successivo può
147
+ riavviare il servizio, verificando il profilo già fissato dal client.
148
+
149
+ Dopo **5 minuti senza operazioni sul modello** il processo termina e l'OS
150
+ libera RAM/VRAM native. `status` non rinnova il timer. Chiudere/disabilitare
151
+ una singola sessione non arresta un modello che può servire altri processi.
152
+ Per liberarlo subito, quando non ci sono inferenze in corso:
153
+
154
+ ```text
155
+ /codeindex service status
156
+ /codeindex service status --json
157
+ /codeindex service stop
158
+ ```
159
+
160
+ Gli stessi comandi funzionano come `node src/cli.js service status|stop`.
161
+ Stop viene rifiutato se ci sono richieste attive/in coda; non interrompe altri
162
+ client. Il servizio si riavvierà al prossimo utilizzo del modello. Nessun
163
+ avvio al login, servizio di sistema o modifica alle impostazioni globali di pi.
164
+
165
+ Il listener è solo `127.0.0.1`, con porta deterministica per la directory
166
+ utente e autenticazione tramite token in `~/.cache/pi-codeindex/service/`
167
+ (directory 0700, record 0600 su POSIX). Il binding esclusivo della porta
168
+ impedisce due avvii simultanei. Una porta occupata da un altro programma dà
169
+ un errore: `servicePort` consente una scelta esplicita. Usa gli stessi
170
+ `serviceDir`/`servicePort` in tutti i client: cambiarli crea endpoint distinti
171
+ e permette deliberatamente più modelli. Non condividere il token o la directory.
172
+
173
+ Restano per-processo le cache degli indici, dei vettori e dei grafi: il servizio
174
+ elimina le copie del **modello**, non tutta la memoria di pi. Dopo l'aggiornamento
175
+ **chiudi e riapri i vecchi processi pi**: i modelli già caricati dal vecchio
176
+ codice in-process non possono essere liberati dal nuovo servizio.
177
+
178
+ Verifica reale opt-in, con pesi già in cache (offline), due processi client,
179
+ indice/ricerca e uscita idle:
180
+
181
+ ```bash
182
+ CODEINDEX_TEST_SERVICE=1 CODEINDEX_DEVICE=cuda node --test test/service-integration.test.js
183
+ # Oppure cpu/q8, oppure WebGPU con CODEINDEX_WEBGPU_BACKEND=vulkan dopo setup-vulkan.
184
+ ```
185
+
186
+ Questi test non sono un nuovo benchmark Fase 5–7. La piattaforma iniziale di
187
+ verifica del servizio è Linux x64; Windows/DirectML e macOS/CoreML richiedono
188
+ verifica sulle rispettive macchine.
189
+
133
190
  ## Configurazione
134
191
 
135
192
  Dentro pi:
@@ -142,6 +199,8 @@ Dentro pi:
142
199
  /codeindex config set webgpuBackend vulkan solo WebGPU, setup-vulkan richiesto
143
200
  /codeindex config set fallback cpu
144
201
  /codeindex config unset device ripristina la scelta automatica
202
+ /codeindex service status processo condiviso, profilo e memoria
203
+ /codeindex service stop rilascia il modello quando inattivo
145
204
  /codeindex doctor inventario, senza inferenza/download
146
205
  /codeindex doctor --probe --device webgpu prova reale isolata (può scaricare i pesi)
147
206
  ```
@@ -175,9 +234,15 @@ non default silenziosi. Schema per editor: [config.schema.json](config.schema.js
175
234
  | `offline` | booleano; niente download, errore se mancano pesi | `CODEINDEX_OFFLINE=true\|false\|1\|0` |
176
235
  | `cacheDir` | cache pesi, default `~/.cache/pi-codeindex/text-model` | `CODEINDEX_MODEL_CACHE` |
177
236
  | `cpuThreads` | 0 = default ORT, oppure 1–256; non limita la GPU | `CODEINDEX_CPU_THREADS` |
237
+ | `serviceIdleMs` | default 300000 (5 min), da 1000 a 86400000 ms | `CODEINDEX_SERVICE_IDLE_MS` |
238
+ | `serviceDir` | directory privata condivisa, default `~/.cache/pi-codeindex/service` | `CODEINDEX_SERVICE_DIR` |
239
+ | `servicePort` | 0 = deterministica, oppure 1024–65535; solo loopback | `CODEINDEX_SERVICE_PORT` |
178
240
 
179
241
  **Riavvia pi con un nuovo processo** dopo una modifica: non si cambia pipeline
180
- nel mezzo di un run. Il menu non riavvia né reindicizza da solo. Device, dtype
242
+ nel mezzo di un run. Se il servizio ha già un profilo/configurazione diversi,
243
+ usa anche `service stop` quando inattivo, oppure attendi l'uscita idle.
244
+ Prima di aggiornare a un protocollo incompatibile, arresta il vecchio servizio.
245
+ Il menu non riavvia né reindicizza da solo. Device, dtype
181
246
  e backend WebGPU effettivi sono salvati nell'indice; se diversi, la ricerca rifiuta di mescolare
182
247
  i vettori e chiede un re-index. Gli indici precedenti senza `device` vengono
183
248
  ricostruiti alla prossima indicizzazione.
@@ -198,9 +263,11 @@ CODEINDEX_DEVICE=cuda CODEINDEX_DTYPE=fp16 pi -e ./src/extension.js
198
263
  in subprocessi isolati con timeout di 120 s per backend; `--all` può scaricare
199
264
  più varianti. Ogni provider forzato viene provato senza fallback, per non
200
265
  attribuire alla GPU un successo ottenuto in CPU. Usa `offline: true` per provarle solo dalla cache. Un crash nativo
201
- di una prova non abbatte pi; questo isolamento riguarda `doctor`, non la normale
202
- pipeline in-process. Smoke del menu e dei comandi via RPC, senza chiamate al
203
- modello: `CODEINDEX_TEST_RPC=1 node --test test/config-rpc.test.js` (richiede
266
+ di una prova non abbatte pi. I probe restano esplicitamente isolati e **possono
267
+ caricare una copia aggiuntiva del modello** mentre il servizio è attivo: arresta
268
+ prima il servizio se vuoi evitare questo picco. Anche la pipeline ordinaria è
269
+ ora isolata, nel worker condiviso. Smoke del menu e dei comandi via RPC, senza
270
+ chiamate LLM e con cache modello temporanea/offline: `CODEINDEX_TEST_RPC=1 node --test test/config-rpc.test.js` (richiede
204
271
  `pi` sul PATH; usa un file config temporaneo, non modifica quello utente).
205
272
 
206
273
  ## Comandi
@@ -252,13 +319,40 @@ Cosa aggiunge:
252
319
  - **indicizzazione automatica** all'avvio della sessione, in background,
253
320
  single-flight, interrompibile, con cap 5.000 file / 200 MB.
254
321
  - **sincronizzazione**: watcher ricorsivo con debounce 800 ms, invalidazione
255
- diretta da `write`/`edit` e sweep `git status` dopo `bash`. Una riconciliazione
256
- riusa i vettori dei file intatti (misurato: 1 chunk re-embedded su 3 dopo una
257
- modifica, 920 ms fino all'indice aggiornato).
258
-
259
- Fuori da un repo git l'estensione si disattiva da sola; se il modello non è
260
- scaricabile o il reindex fallisce, `search_code` risponde con lo stato reale e
261
- la sessione continua.
322
+ diretta da `write`/`edit` e controllo dopo `bash`: sweep `git status` nei
323
+ repository, riconciliazione incrementale nelle cartelle senza Git. Una
324
+ riconciliazione riusa i vettori dei file intatti (misurato nei repository:
325
+ 1 chunk re-embedded su 3 dopo una modifica, 920 ms fino all'indice aggiornato).
326
+
327
+ Se il modello non è scaricabile o il reindex fallisce, `search_code` risponde
328
+ con lo stato reale e la sessione continua.
329
+
330
+ ### Cartelle senza Git
331
+
332
+ Git è opzionale. Nei repository la radice resta quella restituita da Git,
333
+ anche avviando pi da una sottocartella. Altrimenti viene usata **esattamente
334
+ la directory da cui avvii pi**: non viene creato `.git`, non servono commit
335
+ né un remote. `/codeindex status` mostra radice e modalità.
336
+
337
+ In modalità cartella la scansione esclude le directory nascoste e le consuete
338
+ directory di dipendenze/output (`node_modules`, `dist`, `build`, `out`,
339
+ `vendor`, `target`, `coverage`, `venv`, `__pycache__`); non segue i symlink.
340
+ Restano i limiti di estensione, dimensione, 5.000 file e 200 MB. Le esclusioni
341
+ sono quelle fisse della scansione locale: **senza Git i file `.gitignore`
342
+ non vengono interpretati**. Nei repository continua a valere `git ls-files`
343
+ con le esclusioni Git.
344
+
345
+ Il watcher gestisce anche rename/cancellazioni di directory; dopo `bash`
346
+ viene accodata una riconciliazione senza ricalcolare i vettori invariati.
347
+ Se il watcher non è disponibile, la riconciliazione viene accodata anche
348
+ all'inizio di un nuovo prompt. `/codeindex reindex` resta disponibile per
349
+ aggiornare manualmente l'indice; `--force` ricalcola tutti i vettori.
350
+
351
+ Per evitare scansioni involontarie dell'intera macchina, nella **home** e
352
+ nella **radice del disco**, se non sono repository Git, l'indice parte spento.
353
+ Avvia pi dalla cartella del progetto oppure abilitalo esplicitamente con
354
+ `/codeindex enable`; nelle due cartelle protette il consenso va ripetuto a
355
+ ogni avvio della sessione. Nessuna nuova configurazione globale è necessaria.
262
356
 
263
357
  ## Cosa indicizza
264
358
 
@@ -267,7 +361,10 @@ Markdown. Chunk declaration-aware, con fusione dei chunk piccoli (Markdown fino
267
361
  a 80 righe; codice solo micro-chunk ≤6 righe). Esclusi: binari, file >1 MB,
268
362
  minified, lockfile, `.pbl`/`.pbt`/`.pbw`.
269
363
 
270
- ## Numeri misurati (RTX 5070 Ti, fp16)
364
+ ## Numeri misurati (runtime originale in-process, RTX 5070 Ti, fp16)
365
+
366
+ Queste misure precedono il servizio condiviso e non ne misurano memoria,
367
+ latenza IPC o prestazioni con client concorrenti.
271
368
 
272
369
  | Repo | File | Chunk | Primo indice | Re-index |
273
370
  |---|---|---|---|---|
@@ -11,6 +11,9 @@
11
11
  "fallback": { "enum": ["error", "cpu"], "default": "error", "description": "Fallback consentito per un device forzato; auto include sempre CPU." },
12
12
  "offline": { "type": "boolean", "default": false },
13
13
  "cacheDir": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Cache dei pesi. Supporta ~/; i percorsi relativi sono risolti dal cwd." },
14
- "cpuThreads": { "type": "integer", "minimum": 0, "maximum": 256, "default": 0, "description": "0 usa il default di ONNX Runtime; non limita la GPU." }
14
+ "cpuThreads": { "type": "integer", "minimum": 0, "maximum": 256, "default": 0, "description": "0 usa il default di ONNX Runtime; non limita la GPU." },
15
+ "serviceDir": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Directory privata del servizio condiviso. Default ~/.cache/pi-codeindex/service; non cambiarla fra sessioni se vuoi condividere il modello." },
16
+ "servicePort": { "type": "integer", "anyOf": [{ "const": 0 }, { "minimum": 1024, "maximum": 65535 }], "default": 0, "description": "0 sceglie una porta loopback deterministica dalla directory del servizio. Cambiarla crea un endpoint distinto." },
17
+ "serviceIdleMs": { "type": "integer", "minimum": 1000, "maximum": 86400000, "default": 300000, "description": "Inattività prima di terminare il processo e liberare il modello (default 5 minuti). Status non rinnova il timer." }
15
18
  }
16
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alucard_24/pi-codeindex",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Indice semantico locale del codice per pi (EmbeddingGemma 2, ONNX)",
5
5
  "license": "MIT",
6
6
  "author": "Alucard24",
@@ -9,7 +9,7 @@
9
9
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
10
10
  "main": "./src/extension.js",
11
11
  "bin": { "pi-codeindex": "src/cli.js" },
12
- "files": ["src", "scripts/setup-gpu.sh", "scripts/setup-vulkan.mjs", "scripts/vulkan-sources.json", "eval/queries.jsonl", "config.schema.json", "README.md", "LICENSE"],
12
+ "files": ["src", "scripts/setup-gpu.sh", "scripts/setup-vulkan.mjs", "scripts/vulkan-sources.json", "eval/queries.jsonl", "config.schema.json", "README.md", "CHANGELOG.md", "LICENSE"],
13
13
  "engines": { "node": ">=22" },
14
14
  "scripts": {
15
15
  "test": "node --test 'test/*.test.js'",
package/src/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir } from './embed.js';
8
+ import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir, getServiceStatus, serviceCommand, embeddingStatus } from './embed.js';
9
9
  import { configCommand } from './config.js';
10
10
  import { formatProfile } from './backend.js';
11
11
  import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
@@ -44,7 +44,9 @@ async function embedTest() {
44
44
  console.log('cos(same) :', cosine(sameA, sameB).toFixed(6), '(atteso ~1.0)');
45
45
  console.log('cos(diverse) :', cosine(sameA, other).toFixed(6), '(atteso < 0.95)');
46
46
  console.log('latency cold :', tCold, 'ms per 3 testi');
47
- console.log('rss :', rssMb(), 'MB');
47
+ console.log('rss client :', rssMb(), 'MiB');
48
+ const service = await getServiceStatus();
49
+ console.log('servizio :', service.pid, '· RSS', Math.round(service.rssBytes / 1024 / 1024), 'MiB');
48
50
 
49
51
  const batch = Array.from({ length: 16 }, (_, i) => `${SAME} // variante ${i}`);
50
52
  const t1 = performance.now();
@@ -59,7 +61,7 @@ async function embedTest() {
59
61
  ]);
60
62
  console.log('query->args :', cosine(q[0], docs[0]).toFixed(4));
61
63
  console.log('query->idle :', cosine(q[0], docs[1]).toFixed(4));
62
- console.log('rss finale :', rssMb(), 'MB');
64
+ console.log('rss client finale:', rssMb(), 'MiB');
63
65
 
64
66
  const dump = arg('dump');
65
67
  if (dump && dump !== true) {
@@ -78,6 +80,7 @@ async function cmdIndex() {
78
80
  const r = await indexRepo({ root, maxFiles, maxBytes, batchSize, force });
79
81
  const out = {
80
82
  repo: r.repo,
83
+ profile: { model: r.index.model, dims: r.index.dims, device: r.index.device, dtype: r.index.dtype, webgpuBackend: r.index.webgpuBackend },
81
84
  files: r.index.stats.files,
82
85
  chunks: r.total,
83
86
  embedded: r.embedded,
@@ -91,6 +94,7 @@ async function cmdIndex() {
91
94
  console.log(JSON.stringify(out));
92
95
  } else {
93
96
  console.log(`repo : ${r.repo}`);
97
+ console.log(`runtime : ${formatProfile(r.index)} · servizio condiviso`);
94
98
  console.log(`file : ${out.files} (${fmtBytes(out.bytes)}) · invariati ${out.unchangedFiles}`);
95
99
  console.log(`chunk : ${out.chunks} · riusati ${out.reused} · embeddati ${out.embedded}`);
96
100
  console.log(`esclusi : estensione ${out.skip.ext}, dimensione ${out.skip.big}, binari ${out.skip.binary}, minified ${out.skip.minified}, cap ${out.skip.cap}`);
@@ -164,6 +168,7 @@ async function cmdEval() {
164
168
  const cmd = process.argv[2];
165
169
  try {
166
170
  if (cmd === 'config') console.log(await configCommand(process.argv.slice(3)));
171
+ else if (cmd === 'service') console.log(await serviceCommand(process.argv.slice(3)));
167
172
  else if (cmd === 'setup-vulkan') {
168
173
  const { setupVulkan } = await import('../scripts/setup-vulkan.mjs');
169
174
  await setupVulkan(process.argv.slice(3));
@@ -186,9 +191,12 @@ try {
186
191
  else if (cmd === 'search') await cmdSearch();
187
192
  else if (cmd === 'eval') await cmdEval();
188
193
  else {
189
- console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
194
+ console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | service [status [--json]|stop] | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
190
195
  process.exit(1);
191
196
  }
197
+ const runtime = embeddingStatus();
198
+ const warnings = [...runtime.warnings, ...runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`)];
199
+ if (runtime.profile && warnings.length) console.error(`codeindex: selezionato ${formatProfile(runtime.profile)}; ${warnings.join('; ')}`);
192
200
  } catch (e) {
193
201
  console.error(`errore: ${e.message}`);
194
202
  if (process.env.CODEINDEX_DEBUG) console.error(e.stack);
package/src/config-ui.js CHANGED
@@ -14,10 +14,10 @@ export async function configure(ctx, options = {}) {
14
14
  if (!selected || selected === 'Annulla') return null;
15
15
  if (selected === 'Salva') {
16
16
  if (!Object.keys(patch).length) return null;
17
- if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
17
+ if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Per cambiare un servizio già attivo usa /codeindex service stop quando inattivo. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
18
18
  const file = await updateConfig(patch, options);
19
19
  const overrides = Object.keys(patch).filter((key) => loaded.sources[key] === ENV_KEYS[key]);
20
- return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare.` +
20
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare; per cambiare un servizio già attivo usa /codeindex service stop quando inattivo.` +
21
21
  (overrides.length ? `\nAttenzione: override d'ambiente attivi per ${overrides.join(', ')}.` : '') +
22
22
  `\n${formatConfig(await loadConfig(options))}`;
23
23
  }
@@ -25,9 +25,11 @@ export async function configure(ctx, options = {}) {
25
25
  if (!key) continue;
26
26
  const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
27
27
  : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : key === 'offline' ? ['false', 'true'] : null;
28
- const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan' : '';
28
+ const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan'
29
+ : key === 'serviceIdleMs' ? ' — millisecondi; 300000 = 5 minuti'
30
+ : key === 'serviceDir' || key === 'servicePort' ? ' — lascia uguale fra sessioni per condividere un solo modello' : '';
29
31
  const text = choices ? await ctx.ui.select(`${key}${hint} (attuale: ${draft[key]})`, choices)
30
- : await ctx.ui.input(`${key} (attuale: ${draft[key]})`, String(draft[key]));
32
+ : await ctx.ui.input(`${key}${hint} (attuale: ${draft[key]})`, String(draft[key]));
31
33
  if (text === undefined) continue;
32
34
  try { draft[key] = patch[key] = parseSetting(key, text); }
33
35
  catch (e) { ctx.ui.notify(e.message, 'error'); }
package/src/config.js CHANGED
@@ -12,6 +12,7 @@ export const ENV_KEYS = {
12
12
  device: 'CODEINDEX_DEVICE', dtype: 'CODEINDEX_DTYPE', fallback: 'CODEINDEX_FALLBACK',
13
13
  offline: 'CODEINDEX_OFFLINE', cacheDir: 'CODEINDEX_MODEL_CACHE', cpuThreads: 'CODEINDEX_CPU_THREADS',
14
14
  webgpuBackend: 'CODEINDEX_WEBGPU_BACKEND',
15
+ serviceDir: 'CODEINDEX_SERVICE_DIR', servicePort: 'CODEINDEX_SERVICE_PORT', serviceIdleMs: 'CODEINDEX_SERVICE_IDLE_MS',
15
16
  };
16
17
 
17
18
  export function configPath(env = process.env, home = os.homedir()) {
@@ -26,7 +27,8 @@ function expandPath(value, home = os.homedir()) {
26
27
 
27
28
  export function defaults(home = os.homedir()) {
28
29
  return { device: 'auto', dtype: 'auto', webgpuBackend: 'auto', fallback: 'error', offline: false,
29
- cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0 };
30
+ cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0,
31
+ serviceDir: path.join(home, '.cache', 'pi-codeindex', 'service'), servicePort: 0, serviceIdleMs: 300_000 };
30
32
  }
31
33
 
32
34
  export function validateConfig(value, source = 'config') {
@@ -44,8 +46,10 @@ export function validateConfig(value, source = 'config') {
44
46
  : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : null;
45
47
  if (choices && !choices.includes(v)) throw new Error(`${source}: ${key} deve essere uno di: ${choices.join(', ')}`);
46
48
  if (key === 'offline' && typeof v !== 'boolean') throw new Error(`${source}: offline deve essere booleano`);
47
- if (key === 'cacheDir' && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: cacheDir deve essere un percorso non vuoto`);
49
+ if (['cacheDir', 'serviceDir'].includes(key) && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: ${key} deve essere un percorso non vuoto`);
48
50
  if (key === 'cpuThreads' && (!Number.isInteger(v) || v < 0 || v > 256)) throw new Error(`${source}: cpuThreads deve essere un intero tra 0 e 256`);
51
+ if (key === 'servicePort' && (!Number.isInteger(v) || (v !== 0 && (v < 1024 || v > 65535)))) throw new Error(`${source}: servicePort deve essere 0 (automatico) oppure un intero tra 1024 e 65535`);
52
+ if (key === 'serviceIdleMs' && (!Number.isInteger(v) || v < 1000 || v > 86_400_000)) throw new Error(`${source}: serviceIdleMs deve essere un intero tra 1000 e 86400000`);
49
53
  }
50
54
  return value;
51
55
  }
@@ -56,8 +60,8 @@ export function parseSetting(key, text) {
56
60
  if (key === 'offline') {
57
61
  if (!['true', 'false', '1', '0'].includes(text)) throw new Error('offline: usa true oppure false');
58
62
  value = text === 'true' || text === '1';
59
- } else if (key === 'cpuThreads') {
60
- if (!/^\d+$/.test(text)) throw new Error('cpuThreads: atteso un intero tra 0 e 256');
63
+ } else if (['cpuThreads', 'servicePort', 'serviceIdleMs'].includes(key)) {
64
+ if (!/^\d+$/.test(text)) throw new Error(`${key}: atteso un intero`);
61
65
  value = Number(text);
62
66
  }
63
67
  validateConfig({ [key]: value });
@@ -86,6 +90,7 @@ export async function loadConfig({ env = process.env, home = os.homedir() } = {}
86
90
  }
87
91
  validateConfig(config, file);
88
92
  config.cacheDir = expandPath(config.cacheDir, home);
93
+ config.serviceDir = expandPath(config.serviceDir, home);
89
94
  return { config: Object.freeze(config), sources, file };
90
95
  }
91
96
 
@@ -122,5 +127,5 @@ export async function configCommand(args, options = {}) {
122
127
  if (action === 'set' && key && rest.length) file = await updateConfig({ [key]: parseSetting(key, rest.join(' ')) }, options);
123
128
  else if (action === 'unset' && key && !rest.length) file = await updateConfig({}, { ...options, unset: [key] });
124
129
  else throw new Error('uso: config [show|path|set CHIAVE VALORE|unset CHIAVE]');
125
- return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
130
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Per cambiare un profilo già attivo usa anche /codeindex service stop quando il servizio è inattivo. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
126
131
  }
package/src/doctor.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { loadConfig, parseSetting } from './config.js';
6
6
  import { nativeBackends, formatProfile } from './backend.js';
7
7
  import { vulkanStatus } from './vulkan.js';
8
+ import { createEmbeddingClient } from './service-client.js';
8
9
 
9
10
  export function parseDoctorArgs(args) {
10
11
  const options = { probe: false, all: false, json: false };
@@ -63,7 +64,9 @@ export async function doctor({ probe = false, all = false, device, dtype, webgpu
63
64
  }
64
65
  const settings = await loadConfig({ env });
65
66
  const native = nativeBackends();
66
- const report = { ok: true, platform: process.platform, arch: process.arch, node: process.version,
67
+ const service = await createEmbeddingClient({ settings: () => settings, env }).status()
68
+ .catch((e) => ({ running: false, error: e.message }));
69
+ const report = { ok: !service.error, service, platform: process.platform, arch: process.arch, node: process.version,
67
70
  runtime: native.version, settings,
68
71
  requested: { device: device ?? settings.config.device, dtype: dtype ?? settings.config.dtype,
69
72
  webgpuBackend: webgpuBackend ?? settings.config.webgpuBackend },
@@ -73,7 +76,7 @@ export async function doctor({ probe = false, all = false, device, dtype, webgpu
73
76
  for (const selected of devices) {
74
77
  report.probes.push(await probeBackend({ device: selected, dtype, webgpuBackend, timeoutMs, env }));
75
78
  }
76
- report.ok = report.probes.length > 0 && report.probes.every((p) => p.ok);
79
+ report.ok = !service.error && report.probes.length > 0 && report.probes.every((p) => p.ok);
77
80
  }
78
81
  return report;
79
82
  }
@@ -85,12 +88,14 @@ export function formatDoctor(report) {
85
88
  ...report.backends.map((b) => `${b.device.padEnd(8)}: ${b.available ? 'presente' : 'assente'} — ${b.reason}`),
86
89
  `backend WebGPU richiesto: ${(report.requested ?? report.settings.config).webgpuBackend} (solo quando il provider selezionato è WebGPU)`,
87
90
  `Vulkan forzato: ${report.vulkan?.available ? 'presente' : 'assente'} — ${report.vulkan?.reason ?? 'non verificato'}`];
91
+ lines.push(`servizio condiviso: ${report.service?.error ?? (report.service?.running ? `PID ${report.service.pid} · ${report.service.pending} richieste · ${Math.round(report.service.rssBytes / 1024 / 1024)} MiB RSS` : report.service?.stopping ? 'in arresto' : 'fermo (nessun avvio da doctor)')}`);
88
92
  if (!report.probes.length) lines.push('Nessuna inferenza eseguita. Usa doctor --probe (può scaricare i pesi), oppure --probe --all.');
89
93
  for (const p of report.probes) {
90
94
  lines.push(p.ok ? `PASS ${p.requested}: ${formatProfile(p.profile)} · ${p.profile.dims}d · ${p.elapsedMs} ms · cos(stesso) ${p.cosSame.toFixed(6)}`
91
95
  : `FAIL ${p.requested}: ${p.error}`);
92
96
  for (const a of p.attempts ?? []) if (!a.ok) lines.push(` ${formatProfile(a)}: ${a.error}`);
93
97
  }
98
+ if (report.probes.length) lines.push('Probe isolati: possono caricare una copia aggiuntiva del modello mentre il servizio condiviso è attivo.');
94
99
  lines.push('Un provider presente non prova compatibilità/velocità del modello; ORT può eseguire alcuni operatori su CPU.');
95
100
  return lines.join('\n');
96
101
  }
package/src/embed.js CHANGED
@@ -1,88 +1,57 @@
1
- // Embedding locale in-process. Selezione del provider verificata con il
2
- // modello reale; il profilo resta fisso per tutta la vita del processo.
3
- import { loadConfig } from './config.js';
4
- import { createEmbeddingRuntime, nativeBackends, validateEmbeddings, MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
5
- import { ensureTextModel, modelPath } from './model.js';
1
+ // Facciata dei client pi/CLI. L'unica pipeline vive nel servizio condiviso;
2
+ // nessun errore di IPC attiva un fallback in-process.
3
+ import { createEmbeddingClient } from './service-client.js';
4
+ import { modelPath } from './model.js';
5
+ import { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
6
6
 
7
7
  export { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix };
8
+ const client = createEmbeddingClient();
8
9
 
9
- const runtime = createEmbeddingRuntime({
10
- settings: () => loadConfig(),
11
- inventory: async () => nativeBackends().backends,
12
- log: (s) => process.stderr.write(`${s}\n`),
13
- initialize: async (profile, config) => {
14
- const sessionOptions = {};
15
- if (profile.webgpuBackend === 'vulkan') {
16
- // Il setup mancante fallisce PRIMA di scaricare il modello. Backend unico,
17
- // esplicito e non registrato come fallback dei provider standard.
18
- const { registerVulkanBackend } = await import('./vulkan.js');
19
- sessionOptions.executionProviders = [await registerVulkanBackend()];
20
- }
21
- const dir = await ensureTextModel(profile, config);
22
- // Import lazy: config/status/doctor senza --probe non caricano il modello.
23
- const { pipeline } = await import('@huggingface/transformers');
24
- if (config.cpuThreads) sessionOptions.intraOpNumThreads = config.cpuThreads;
25
- if (profile.device === 'dml') {
26
- sessionOptions.executionMode = 'sequential';
27
- sessionOptions.enableMemPattern = false;
28
- }
29
- let pipe;
30
- try {
31
- pipe = await pipeline('feature-extraction', dir, {
32
- dtype: profile.dtype, device: profile.device, local_files_only: true,
33
- session_options: sessionOptions,
34
- });
35
- const out = await pipe([QUERY_PREFIX + 'code retrieval smoke test'], { pooling: 'mean', normalize: true });
36
- validateEmbeddings(out.tolist(), 1);
37
- return pipe;
38
- } catch (e) {
39
- try { await pipe?.dispose(); } catch { /* conserva l'errore originale */ }
40
- throw e;
41
- }
42
- },
43
- });
44
-
45
- export async function getEmbedder() {
46
- return (await runtime.get()).pipe;
47
- }
48
-
49
- export async function getEmbeddingProfile() {
50
- return (await runtime.get()).profile;
51
- }
52
-
53
- export function embeddingStatus() {
54
- return runtime.info();
55
- }
10
+ export const getEmbeddingProfile = () => client.getProfile();
11
+ export const embeddingStatus = () => client.info();
12
+ export const getServiceStatus = () => client.status();
13
+ export const stopEmbeddingService = () => client.stop();
56
14
 
57
15
  /** Testi -> Float32Array[] finiti, normalizzati L2 per default. */
58
- export async function embed(texts, { normalize = true } = {}) {
16
+ export async function embed(texts, options = {}) {
59
17
  if (!Array.isArray(texts) || texts.length === 0) return [];
60
- const { pipe } = await runtime.get();
61
- // Non fare fallback qui: l'indice è già associato al profilo selezionato.
62
- const out = await pipe(texts, { pooling: 'mean', normalize });
63
- const rows = validateEmbeddings(out.tolist(), texts.length);
64
- return rows.map((row) => Float32Array.from(row));
18
+ return client.embed(texts, options);
65
19
  }
66
-
67
- export function embedQuery(q) {
68
- return embed([QUERY_PREFIX + q]);
69
- }
70
-
20
+ export function embedQuery(q) { return embed([QUERY_PREFIX + q]); }
71
21
  export function embedDocs(docs) {
72
22
  return embed(docs.map((d) => typeof d === 'string' ? d : docPrefix(d.path, d.title) + d.text));
73
23
  }
74
-
75
24
  export function cosine(a, b) {
76
25
  let dot = 0, na = 0, nb = 0;
77
26
  for (let i = 0; i < a.length; i++) {
78
- dot += a[i] * b[i];
79
- na += a[i] * a[i];
80
- nb += b[i] * b[i];
27
+ dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
81
28
  }
82
29
  return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
83
30
  }
84
-
85
31
  export async function modelDir() {
86
- const { profile, config } = await runtime.get();
87
- return modelPath(config, profile.dtype);
32
+ const profile = await client.getProfile();
33
+ return modelPath(client.info().config, profile.dtype);
34
+ }
35
+
36
+ export function formatServiceStatus(status) {
37
+ if (status.stopping) return `servizio condiviso: in arresto · PID ${status.pid} (rilascio delle risorse in corso)`;
38
+ if (!status.running) return `servizio condiviso: fermo (avvio automatico al prossimo embedding) · porta ${status.port}`;
39
+ const profile = status.profile;
40
+ return [`servizio condiviso: attivo · PID ${status.pid} · 127.0.0.1:${status.port}`,
41
+ `modello: ${profile ? `${profile.device}${profile.device === 'webgpu' ? `→${profile.webgpuBackend}` : ''}/${profile.dtype}` : status.initializing ? 'caricamento' : 'non caricato'}`,
42
+ `richieste attive/in coda: ${status.pending} · RSS servizio: ${Math.round(status.rssBytes / 1024 / 1024)} MiB`,
43
+ `uscita dopo inattività: ${status.idleMs} ms`].join('\n');
44
+ }
45
+ export async function serviceCommand(args, { service = client } = {}) {
46
+ const [action = 'status', ...rest] = args;
47
+ if (rest.some((arg) => arg !== '--json') || rest.length > 1 || (action !== 'status' && rest.length)) throw new Error('uso: service [status [--json]|stop]');
48
+ if (action === 'status') {
49
+ const status = await service.status();
50
+ return rest.includes('--json') ? JSON.stringify(status) : formatServiceStatus(status);
51
+ }
52
+ if (action === 'stop') {
53
+ const result = await service.stop();
54
+ return result.stopping ? `arresto del servizio condiviso richiesto (PID ${result.pid}); si riavvierà al prossimo embedding` : 'servizio condiviso già fermo';
55
+ }
56
+ throw new Error('uso: service [status [--json]|stop]');
88
57
  }