archgraph-argo 0.20.6 → 0.20.8

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.
@@ -154,65 +154,83 @@ function createDefaultSemanticRetrieval(dependencies = {}) {
154
154
  return Object.freeze({
155
155
  async retrieve(request = {}) {
156
156
  const composition = await resolveRetrievalComposition(dependencies);
157
- const activeTestComposition = testCompositionStorage.getStore();
158
- const activeReadinessBoundary = activeTestComposition
159
- && activeTestComposition.useReadinessBoundary !== true
160
- ? undefined
161
- : readinessBoundary;
162
- let configurationEvidence = await composition.resolveConfiguration();
163
- let evidence = await readAndEvaluatePersistentReadiness(
164
- composition,
165
- canonicalGraph,
166
- activeReadinessBoundary,
167
- );
168
- if (!evidence.alignment.aligned) {
169
- await attemptAutomaticAlignment({
170
- composition,
171
- request,
172
- alignment: evidence.alignment,
173
- });
174
- evidence = await readAndEvaluatePersistentReadiness(
157
+ try {
158
+ const activeTestComposition = testCompositionStorage.getStore();
159
+ const activeReadinessBoundary = activeTestComposition
160
+ && activeTestComposition.useReadinessBoundary !== true
161
+ ? undefined
162
+ : readinessBoundary;
163
+ let configurationEvidence = await composition.resolveConfiguration();
164
+ let evidence = await readAndEvaluatePersistentReadiness(
175
165
  composition,
176
166
  canonicalGraph,
177
167
  activeReadinessBoundary,
178
168
  );
179
169
  if (!evidence.alignment.aligned) {
180
- throw semanticAutomaticAlignmentFailed(evidence.alignment);
170
+ await attemptAutomaticAlignment({
171
+ composition,
172
+ request,
173
+ alignment: evidence.alignment,
174
+ });
175
+ evidence = await readAndEvaluatePersistentReadiness(
176
+ composition,
177
+ canonicalGraph,
178
+ activeReadinessBoundary,
179
+ );
180
+ if (!evidence.alignment.aligned) {
181
+ throw semanticAutomaticAlignmentFailed(evidence.alignment);
182
+ }
183
+ }
184
+ return await executeWpP2Retrieval({
185
+ composition,
186
+ request,
187
+ canonicalGraph,
188
+ readiness: evidence.readiness,
189
+ configurationEvidence,
190
+ });
191
+ } finally {
192
+ if (typeof composition.dispose === 'function') {
193
+ await composition.dispose();
181
194
  }
182
195
  }
183
- return executeWpP2Retrieval({
184
- composition,
185
- request,
186
- canonicalGraph,
187
- readiness: evidence.readiness,
188
- configurationEvidence,
189
- });
190
196
  },
191
197
  async probeQueryability(request = {}, readiness = {}) {
192
198
  const composition = await resolveRetrievalComposition(dependencies);
193
- const configurationEvidence = await composition.resolveConfiguration();
194
- return executeWpP2Retrieval({
195
- composition,
196
- request,
197
- canonicalGraph,
198
- readiness,
199
- configurationEvidence,
200
- });
199
+ try {
200
+ const configurationEvidence = await composition.resolveConfiguration();
201
+ return await executeWpP2Retrieval({
202
+ composition,
203
+ request,
204
+ canonicalGraph,
205
+ readiness,
206
+ configurationEvidence,
207
+ });
208
+ } finally {
209
+ if (typeof composition.dispose === 'function') {
210
+ await composition.dispose();
211
+ }
212
+ }
201
213
  },
202
214
  async readReadiness() {
203
215
  const composition = await resolveRetrievalComposition(dependencies);
204
- const activeTestComposition = testCompositionStorage.getStore();
205
- const activeReadinessBoundary = activeTestComposition
206
- && activeTestComposition.useReadinessBoundary !== true
207
- ? undefined
208
- : readinessBoundary;
209
- await composition.resolveConfiguration();
210
- const evidence = await readAndEvaluatePersistentReadiness(
211
- composition,
212
- canonicalGraph,
213
- activeReadinessBoundary,
214
- );
215
- return publicReadinessOutcome(evidence.alignment);
216
+ try {
217
+ const activeTestComposition = testCompositionStorage.getStore();
218
+ const activeReadinessBoundary = activeTestComposition
219
+ && activeTestComposition.useReadinessBoundary !== true
220
+ ? undefined
221
+ : readinessBoundary;
222
+ await composition.resolveConfiguration();
223
+ const evidence = await readAndEvaluatePersistentReadiness(
224
+ composition,
225
+ canonicalGraph,
226
+ activeReadinessBoundary,
227
+ );
228
+ return publicReadinessOutcome(evidence.alignment);
229
+ } finally {
230
+ if (typeof composition.dispose === 'function') {
231
+ await composition.dispose();
232
+ }
233
+ }
216
234
  },
217
235
  });
218
236
  }
@@ -236,8 +254,9 @@ async function executeWpP2Retrieval({
236
254
  configuration: configurationEvidence.configuration,
237
255
  transport: composition.transport,
238
256
  });
239
- markPhase('retrieval:embed');
257
+ markPhase('retrieval:embed:start');
240
258
  const vector = await provider.embed(request.intent);
259
+ markPhase('retrieval:embed:done');
241
260
  requireQualifiedVector(vector);
242
261
  const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
243
262
  const strict = AUDIT_PURPOSES.has(purpose);
@@ -254,6 +273,7 @@ async function executeWpP2Retrieval({
254
273
  const pool = rerank ? Math.max(topK, rerankOptions.poolSize) : topK;
255
274
  const channelSeeds = [];
256
275
  for (const channel of CHANNELS) {
276
+ markPhase('retrieval:vector:' + channel.channel);
257
277
  const vectorSeeds = await exhaustChannel({
258
278
  channel,
259
279
  neo4jDriver: composition.neo4jDriver,
@@ -264,6 +284,7 @@ async function executeWpP2Retrieval({
264
284
  });
265
285
  let seeds = vectorSeeds;
266
286
  if (hybrid) {
287
+ markPhase('retrieval:lexical:' + channel.channel);
267
288
  const lexicalSeeds = await exhaustLexicalChannel({
268
289
  channel,
269
290
  neo4jDriver: composition.neo4jDriver,
@@ -276,7 +297,7 @@ async function executeWpP2Retrieval({
276
297
  channelSeeds.push({ channel, seeds });
277
298
  }
278
299
  if (rerank) {
279
- markPhase('retrieval:rerank');
300
+ markPhase('retrieval:rerank:start');
280
301
  // Rerank every channel CONCURRENTLY: the LLM calls dominate latency and are
281
302
  // independent, so parallelizing turns the cost from sum(channels) into
282
303
  // ~one call. fail-open: a null/empty order keeps the original ordering.
@@ -292,6 +313,7 @@ async function executeWpP2Retrieval({
292
313
  : seeds
293
314
  )));
294
315
  channelSeeds.forEach((entry, index) => { entry.seeds = rerankedSeeds[index]; });
316
+ markPhase('retrieval:rerank:done');
295
317
  }
296
318
  const seedsByType = {};
297
319
  for (const { channel, seeds } of channelSeeds) {
@@ -329,6 +351,28 @@ async function createProductionComposition(dependencies) {
329
351
  const repositoryRoot = dependencies.repositoryRoot
330
352
  || getWorkspaceRoot();
331
353
  let configurationEvidence;
354
+ let neo4jHandle = null;
355
+ async function disposeNeo4j() {
356
+ const handle = neo4jHandle;
357
+ neo4jHandle = null;
358
+ if (!handle) return;
359
+ try { await handle.session.close(); } catch { /* ignore */ }
360
+ try { await handle.driver.close(); } catch { /* ignore */ }
361
+ }
362
+ function ensureNeo4jSession() {
363
+ if (neo4jHandle) return neo4jHandle.session;
364
+ const configuration = configurationEvidence.configuration;
365
+ const neo4j = require('neo4j-driver');
366
+ const driver = neo4j.driver(
367
+ configuration.neo4jDatabaseUrl,
368
+ neo4j.auth.basic(configuration.neo4jDatabaseUsername, configuration.neo4jDatabasePassword),
369
+ );
370
+ const session = driver.session(configuration.neo4jDatabase === undefined
371
+ ? undefined
372
+ : { database: configuration.neo4jDatabase });
373
+ neo4jHandle = { driver, session };
374
+ return session;
375
+ }
332
376
  return Object.freeze({
333
377
  async resolveConfiguration() {
334
378
  configurationEvidence = await resolveApprovedLiveConfiguration({
@@ -350,59 +394,61 @@ async function createProductionComposition(dependencies) {
350
394
  if (!configurationEvidence) {
351
395
  throw safeError('EXTERNAL_CREDENTIALS_REQUIRED');
352
396
  }
353
- return executeProductionNeo4jOperation(configurationEvidence.configuration, operation);
397
+ if (operation && operation.kind === 'semantic-auto-alignment-attempt') {
398
+ return runScriptOwnedSemanticAlignment(operation);
399
+ }
400
+ try {
401
+ return await runOperationOnSession(ensureNeo4jSession(), operation);
402
+ } catch (error) {
403
+ // A failed run may leave the session unusable; drop the handle so the
404
+ // rest of this retrieval (or the next one) reconnects cleanly.
405
+ await disposeNeo4j();
406
+ throw error;
407
+ }
354
408
  },
355
409
  }),
410
+ async dispose() {
411
+ await disposeNeo4j();
412
+ },
356
413
  });
357
414
  }
358
415
 
359
- async function executeProductionNeo4jOperation(configuration, operation) {
360
- if (operation && operation.kind === 'semantic-auto-alignment-attempt') {
361
- return runScriptOwnedSemanticAlignment(operation);
416
+ // Pure operation runner: runs one Cypher operation on a caller-owned Neo4j
417
+ // session and maps the result. The session/driver lifecycle lives in the
418
+ // production composition (ONE handle per retrieval, reused across every vector
419
+ // window, then closed) -- correctly reused without leaving a lingering handle,
420
+ // which would keep short-lived processes alive. Creating AND closing a driver
421
+ // per operation caused the rapid handle churn that intermittently aborted the
422
+ // MCP with a native libuv assertion on Windows (confirmed crash phase:
423
+ // retrieval:vector-window:ArchitectureRelationship).
424
+ async function runOperationOnSession(session, operation) {
425
+ const result = await session.run(operation.cypher, operation.parameters);
426
+ if (operation.kind === 'semantic-readiness-read') {
427
+ const readiness = result.records[0] && result.records[0].get('readiness');
428
+ return { records: readiness ? [readiness] : [] };
362
429
  }
363
- const neo4j = require('neo4j-driver');
364
- const driver = neo4j.driver(
365
- configuration.neo4jDatabaseUrl,
366
- neo4j.auth.basic(
367
- configuration.neo4jDatabaseUsername,
368
- configuration.neo4jDatabasePassword,
369
- ),
370
- );
371
- const session = driver.session(configuration.neo4jDatabase === undefined
372
- ? undefined
373
- : { database: configuration.neo4jDatabase });
374
- try {
375
- const result = await session.run(operation.cypher, operation.parameters);
376
- if (operation.kind === 'semantic-readiness-read') {
377
- const readiness = result.records[0] && result.records[0].get('readiness');
378
- return { records: readiness ? [readiness] : [] };
379
- }
380
- const records = result.records.map(record => ({
381
- ...record.get('record'),
382
- score: numberValue(record.get('score')),
383
- }));
384
- if (operation.kind === 'semantic-lexical-query') {
385
- return { records };
386
- }
387
- const offset = operation.parameters.offset;
388
- const windowSize = operation.parameters.windowSize;
389
- const returnedCount = Math.max(0, records.length - offset);
390
- const hasMore = records.length === operation.parameters.topK;
391
- return {
392
- records,
393
- windowEvidence: {
394
- offset,
395
- windowSize,
396
- returnedCount,
397
- hasMore,
398
- nextOffset: hasMore ? operation.parameters.topK : null,
399
- windowExhausted: !hasMore,
400
- },
401
- };
402
- } finally {
403
- await session.close();
404
- await driver.close();
430
+ const records = result.records.map(record => ({
431
+ ...record.get('record'),
432
+ score: numberValue(record.get('score')),
433
+ }));
434
+ if (operation.kind === 'semantic-lexical-query') {
435
+ return { records };
405
436
  }
437
+ const offset = operation.parameters.offset;
438
+ const windowSize = operation.parameters.windowSize;
439
+ const returnedCount = Math.max(0, records.length - offset);
440
+ const hasMore = records.length === operation.parameters.topK;
441
+ return {
442
+ records,
443
+ windowEvidence: {
444
+ offset,
445
+ windowSize,
446
+ returnedCount,
447
+ hasMore,
448
+ nextOffset: hasMore ? operation.parameters.topK : null,
449
+ windowExhausted: !hasMore,
450
+ },
451
+ };
406
452
  }
407
453
 
408
454
  async function attemptAutomaticAlignment({ composition, request, alignment }) {
@@ -741,6 +787,7 @@ async function exhaustChannel({
741
787
  const effectiveMax = Number.isInteger(maxSeeds) && maxSeeds > 0 ? maxSeeds : Number.POSITIVE_INFINITY;
742
788
  let offset = 0;
743
789
  while (accepted.length < effectiveMax) {
790
+ markPhase('retrieval:vector-window:' + channel.channel);
744
791
  const parameters = Object.freeze({
745
792
  indexName: channel.indexName,
746
793
  channel: channel.channel,
@@ -46,6 +46,18 @@ function isProjectInitialized(repositoryRoot) {
46
46
  }
47
47
  }
48
48
 
49
+ // A readiness record proves the workspace was initialized (argo init) before.
50
+ // The preheat only RECONCILES a previously-initialized-but-now-stale workspace;
51
+ // a never-aligned workspace (brand-new project, or a synthetic test copy) must
52
+ // be initialized explicitly, never rebuilt in the background.
53
+ function hasReadinessRecord(repositoryRoot) {
54
+ try {
55
+ return fs.existsSync(readinessRecordPath(repositoryRoot));
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
49
61
  function alignmentError() {
50
62
  const error = new Error('SEMANTIC_AUTO_ALIGNMENT_FAILED');
51
63
  error.category = 'SEMANTIC_AUTO_ALIGNMENT_FAILED';
@@ -57,7 +69,7 @@ function alignmentError() {
57
69
 
58
70
  // Run the alignment (once). Returns a promise resolving to { status: 'aligned' }
59
71
  // or rejecting with the SEMANTIC_AUTO_ALIGNMENT_FAILED envelope.
60
- function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
72
+ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot(), options = {}) {
61
73
  if (inFlight) {
62
74
  return inFlight;
63
75
  }
@@ -70,6 +82,11 @@ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
70
82
  env: process.env,
71
83
  stdio: ['ignore', 'ignore', 'inherit'],
72
84
  });
85
+ if (options.unref && typeof child.unref === 'function') {
86
+ // Background preheat must NOT keep the MCP process alive (a spawnSync-driven
87
+ // caller waits for the server to exit; an attached child would hang it).
88
+ child.unref();
89
+ }
73
90
  const finish = (failed, cause) => {
74
91
  inFlight = null;
75
92
  const ms = Date.now() - startedAt;
@@ -94,9 +111,14 @@ function preheatSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
94
111
  if (preheated || !repositoryRoot || !isProjectInitialized(repositoryRoot) || isSemanticReady(repositoryRoot)) {
95
112
  return;
96
113
  }
114
+ // Only reconcile a workspace that was initialized before (a readiness record
115
+ // exists); never rebuild a never-aligned project in the background.
116
+ if (!hasReadinessRecord(repositoryRoot)) {
117
+ return;
118
+ }
97
119
  preheated = true;
98
120
  console.error('[argo] readiness not aligned at startup; preheating semantic alignment in background…');
99
- runSemanticAlignment(repositoryRoot).catch(() => {});
121
+ runSemanticAlignment(repositoryRoot, { unref: true }).catch(() => {});
100
122
  }
101
123
 
102
124
  module.exports = {
@@ -104,5 +126,6 @@ module.exports = {
104
126
  preheatSemanticAlignment,
105
127
  isSemanticReady,
106
128
  isProjectInitialized,
129
+ hasReadinessRecord,
107
130
  readinessRecordPath,
108
131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.20.6",
3
+ "version": "0.20.8",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {