archgraph-argo 0.20.7 → 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
  }
@@ -333,6 +351,28 @@ async function createProductionComposition(dependencies) {
333
351
  const repositoryRoot = dependencies.repositoryRoot
334
352
  || getWorkspaceRoot();
335
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
+ }
336
376
  return Object.freeze({
337
377
  async resolveConfiguration() {
338
378
  configurationEvidence = await resolveApprovedLiveConfiguration({
@@ -354,59 +394,61 @@ async function createProductionComposition(dependencies) {
354
394
  if (!configurationEvidence) {
355
395
  throw safeError('EXTERNAL_CREDENTIALS_REQUIRED');
356
396
  }
357
- 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
+ }
358
408
  },
359
409
  }),
410
+ async dispose() {
411
+ await disposeNeo4j();
412
+ },
360
413
  });
361
414
  }
362
415
 
363
- async function executeProductionNeo4jOperation(configuration, operation) {
364
- if (operation && operation.kind === 'semantic-auto-alignment-attempt') {
365
- 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] : [] };
366
429
  }
367
- const neo4j = require('neo4j-driver');
368
- const driver = neo4j.driver(
369
- configuration.neo4jDatabaseUrl,
370
- neo4j.auth.basic(
371
- configuration.neo4jDatabaseUsername,
372
- configuration.neo4jDatabasePassword,
373
- ),
374
- );
375
- const session = driver.session(configuration.neo4jDatabase === undefined
376
- ? undefined
377
- : { database: configuration.neo4jDatabase });
378
- try {
379
- const result = await session.run(operation.cypher, operation.parameters);
380
- if (operation.kind === 'semantic-readiness-read') {
381
- const readiness = result.records[0] && result.records[0].get('readiness');
382
- return { records: readiness ? [readiness] : [] };
383
- }
384
- const records = result.records.map(record => ({
385
- ...record.get('record'),
386
- score: numberValue(record.get('score')),
387
- }));
388
- if (operation.kind === 'semantic-lexical-query') {
389
- return { records };
390
- }
391
- const offset = operation.parameters.offset;
392
- const windowSize = operation.parameters.windowSize;
393
- const returnedCount = Math.max(0, records.length - offset);
394
- const hasMore = records.length === operation.parameters.topK;
395
- return {
396
- records,
397
- windowEvidence: {
398
- offset,
399
- windowSize,
400
- returnedCount,
401
- hasMore,
402
- nextOffset: hasMore ? operation.parameters.topK : null,
403
- windowExhausted: !hasMore,
404
- },
405
- };
406
- } finally {
407
- await session.close();
408
- 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 };
409
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
+ };
410
452
  }
411
453
 
412
454
  async function attemptAutomaticAlignment({ composition, request, alignment }) {
@@ -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.7",
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": {