archgraph-argo 0.20.5 → 0.20.7
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.
|
@@ -369,6 +369,13 @@ function resolveWorkspaceRoot(args) {
|
|
|
369
369
|
|
|
370
370
|
async function callTool(name, args = {}, progressToken = null, dependencies = undefined) {
|
|
371
371
|
loadRepositoryArgoEnvironment(resolveWorkspaceRoot(args));
|
|
372
|
+
try {
|
|
373
|
+
const crash = require('./graph-rag/mcpCrashDiagnostics.js');
|
|
374
|
+
crash.installCrashDiagnostics(resolveWorkspaceRoot(args));
|
|
375
|
+
crash.markPhase('tool:' + name);
|
|
376
|
+
} catch {
|
|
377
|
+
// diagnostics are best-effort; never block a tool call
|
|
378
|
+
}
|
|
372
379
|
if (name === 'initializeWorkspace') {
|
|
373
380
|
const workspace = await initializeWorkspace(resolveWorkspaceRoot(args));
|
|
374
381
|
// Deterministic argo-init harness report (Neo4j structural sync, semantic
|
|
@@ -777,6 +784,12 @@ async function main() {
|
|
|
777
784
|
// server starts, rebuild the index in the BACKGROUND (async, logged) so the
|
|
778
785
|
// first query rarely pays the multi-second reconstruction. No-op when aligned.
|
|
779
786
|
if (process.env.ARGO_REPO_ROOT && process.env.ARGO_REPO_ROOT.trim() !== '') {
|
|
787
|
+
try {
|
|
788
|
+
require('./graph-rag/mcpCrashDiagnostics.js')
|
|
789
|
+
.installCrashDiagnostics(process.env.ARGO_REPO_ROOT);
|
|
790
|
+
} catch {
|
|
791
|
+
// diagnostics are best-effort; never block server startup
|
|
792
|
+
}
|
|
780
793
|
try {
|
|
781
794
|
require('./graph-rag/semanticAlignmentRunner.js')
|
|
782
795
|
.preheatSemanticAlignment(process.env.ARGO_REPO_ROOT);
|
|
@@ -32,6 +32,9 @@ const {
|
|
|
32
32
|
const {
|
|
33
33
|
runSemanticAlignment,
|
|
34
34
|
} = require('./semanticAlignmentRunner.js');
|
|
35
|
+
const {
|
|
36
|
+
markPhase,
|
|
37
|
+
} = require('./mcpCrashDiagnostics.js');
|
|
35
38
|
|
|
36
39
|
const APPROVED_SOURCE_KEYS = Object.freeze([
|
|
37
40
|
'ARGO_EMBEDDING_BASE_URL',
|
|
@@ -233,7 +236,9 @@ async function executeWpP2Retrieval({
|
|
|
233
236
|
configuration: configurationEvidence.configuration,
|
|
234
237
|
transport: composition.transport,
|
|
235
238
|
});
|
|
239
|
+
markPhase('retrieval:embed:start');
|
|
236
240
|
const vector = await provider.embed(request.intent);
|
|
241
|
+
markPhase('retrieval:embed:done');
|
|
237
242
|
requireQualifiedVector(vector);
|
|
238
243
|
const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
|
|
239
244
|
const strict = AUDIT_PURPOSES.has(purpose);
|
|
@@ -250,6 +255,7 @@ async function executeWpP2Retrieval({
|
|
|
250
255
|
const pool = rerank ? Math.max(topK, rerankOptions.poolSize) : topK;
|
|
251
256
|
const channelSeeds = [];
|
|
252
257
|
for (const channel of CHANNELS) {
|
|
258
|
+
markPhase('retrieval:vector:' + channel.channel);
|
|
253
259
|
const vectorSeeds = await exhaustChannel({
|
|
254
260
|
channel,
|
|
255
261
|
neo4jDriver: composition.neo4jDriver,
|
|
@@ -260,6 +266,7 @@ async function executeWpP2Retrieval({
|
|
|
260
266
|
});
|
|
261
267
|
let seeds = vectorSeeds;
|
|
262
268
|
if (hybrid) {
|
|
269
|
+
markPhase('retrieval:lexical:' + channel.channel);
|
|
263
270
|
const lexicalSeeds = await exhaustLexicalChannel({
|
|
264
271
|
channel,
|
|
265
272
|
neo4jDriver: composition.neo4jDriver,
|
|
@@ -272,6 +279,7 @@ async function executeWpP2Retrieval({
|
|
|
272
279
|
channelSeeds.push({ channel, seeds });
|
|
273
280
|
}
|
|
274
281
|
if (rerank) {
|
|
282
|
+
markPhase('retrieval:rerank:start');
|
|
275
283
|
// Rerank every channel CONCURRENTLY: the LLM calls dominate latency and are
|
|
276
284
|
// independent, so parallelizing turns the cost from sum(channels) into
|
|
277
285
|
// ~one call. fail-open: a null/empty order keeps the original ordering.
|
|
@@ -287,6 +295,7 @@ async function executeWpP2Retrieval({
|
|
|
287
295
|
: seeds
|
|
288
296
|
)));
|
|
289
297
|
channelSeeds.forEach((entry, index) => { entry.seeds = rerankedSeeds[index]; });
|
|
298
|
+
markPhase('retrieval:rerank:done');
|
|
290
299
|
}
|
|
291
300
|
const seedsByType = {};
|
|
292
301
|
for (const { channel, seeds } of channelSeeds) {
|
|
@@ -736,6 +745,7 @@ async function exhaustChannel({
|
|
|
736
745
|
const effectiveMax = Number.isInteger(maxSeeds) && maxSeeds > 0 ? maxSeeds : Number.POSITIVE_INFINITY;
|
|
737
746
|
let offset = 0;
|
|
738
747
|
while (accepted.length < effectiveMax) {
|
|
748
|
+
markPhase('retrieval:vector-window:' + channel.channel);
|
|
739
749
|
const parameters = Object.freeze({
|
|
740
750
|
indexName: channel.indexName,
|
|
741
751
|
channel: channel.channel,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Crash diagnostics for the ARGO MCP server. A native abort (libuv assertion) or
|
|
4
|
+
// an uncaught error currently kills the server with no trace in the host log.
|
|
5
|
+
// This module:
|
|
6
|
+
// - persists a "phase breadcrumb" synchronously on every phase change, so the
|
|
7
|
+
// LAST phase before a crash is always recoverable (even for native aborts
|
|
8
|
+
// that never fire JS handlers);
|
|
9
|
+
// - appends uncaught-exception (incl. unhandled-rejection, which Node raises as
|
|
10
|
+
// an uncaught exception) and process-exit records to a per-workspace crash
|
|
11
|
+
// log.
|
|
12
|
+
// It uses `uncaughtExceptionMonitor` (which logs WITHOUT changing Node's default
|
|
13
|
+
// crash behaviour) and never logs secret values.
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
let installed = false;
|
|
19
|
+
let crashLogPath = null;
|
|
20
|
+
let phasePath = null;
|
|
21
|
+
let currentPhase = 'startup';
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
|
|
24
|
+
function tempPath(workspaceRoot, name) {
|
|
25
|
+
return path.join(workspaceRoot, '.argo', 'temp', name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function crashLogFilePath() {
|
|
29
|
+
return crashLogPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendCrash(entry) {
|
|
33
|
+
if (!crashLogPath) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
|
|
38
|
+
const error = entry && entry.error;
|
|
39
|
+
const line = JSON.stringify({
|
|
40
|
+
at: new Date().toISOString(),
|
|
41
|
+
pid: process.pid,
|
|
42
|
+
uptimeMs: Date.now() - startedAt,
|
|
43
|
+
phase: currentPhase,
|
|
44
|
+
kind: entry && entry.kind,
|
|
45
|
+
...(entry && entry.origin ? { origin: entry.origin } : {}),
|
|
46
|
+
...(entry && entry.code !== undefined ? { code: entry.code } : {}),
|
|
47
|
+
...(entry && entry.signal !== undefined ? { signal: entry.signal } : {}),
|
|
48
|
+
...(error ? {
|
|
49
|
+
category: error.category,
|
|
50
|
+
message: String(error.message || error).slice(0, 1000),
|
|
51
|
+
stack: String(error.stack || '').slice(0, 4000),
|
|
52
|
+
} : {}),
|
|
53
|
+
}) + '\n';
|
|
54
|
+
fs.appendFileSync(crashLogPath, line, 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
// best-effort diagnostics only; never throw from the crash path
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installCrashDiagnostics(workspaceRoot) {
|
|
61
|
+
if (workspaceRoot) {
|
|
62
|
+
crashLogPath = tempPath(workspaceRoot, 'mcp-crash.log');
|
|
63
|
+
phasePath = tempPath(workspaceRoot, 'mcp-phase.json');
|
|
64
|
+
markPhase(currentPhase);
|
|
65
|
+
}
|
|
66
|
+
if (installed) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
installed = true;
|
|
70
|
+
// Logs without altering Node's default crash behaviour (unlike a bare
|
|
71
|
+
// 'uncaughtException' listener, which would swallow the crash).
|
|
72
|
+
process.on('uncaughtExceptionMonitor', (error, origin) => {
|
|
73
|
+
appendCrash({ kind: 'uncaughtException', origin, error });
|
|
74
|
+
});
|
|
75
|
+
process.on('exit', (code, signal) => {
|
|
76
|
+
appendCrash({ kind: 'exit', code: code === undefined ? null : code, signal: signal === undefined ? null : signal });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Synchronous breadcrumb: guarantees the last phase survives a hard abort.
|
|
81
|
+
function markPhase(phase) {
|
|
82
|
+
currentPhase = String(phase === undefined || phase === null ? '' : phase);
|
|
83
|
+
if (!phasePath) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
fs.mkdirSync(path.dirname(phasePath), { recursive: true });
|
|
88
|
+
fs.writeFileSync(phasePath, JSON.stringify({
|
|
89
|
+
phase: currentPhase,
|
|
90
|
+
at: new Date().toISOString(),
|
|
91
|
+
pid: process.pid,
|
|
92
|
+
}) + '\n', 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
// best-effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readLastPhase(workspaceRoot) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(fs.readFileSync(tempPath(workspaceRoot, 'mcp-phase.json'), 'utf8'));
|
|
101
|
+
return parsed && typeof parsed.phase === 'string' ? parsed.phase : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
installCrashDiagnostics,
|
|
109
|
+
markPhase,
|
|
110
|
+
readLastPhase,
|
|
111
|
+
crashLogFilePath,
|
|
112
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
-
const { spawn } = require('node:child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4
|
+
const { markPhase } = require('./graph-rag/mcpCrashDiagnostics.js');
|
|
4
5
|
const readline = require('node:readline');
|
|
5
6
|
const crypto = require('node:crypto');
|
|
6
7
|
|
|
@@ -1768,6 +1769,7 @@ function removeEntries(existing, removals) {
|
|
|
1768
1769
|
}
|
|
1769
1770
|
|
|
1770
1771
|
async function buildMutationResult(context, mutations, write, dependencies) {
|
|
1772
|
+
markPhase('mutation:' + (write ? 'apply' : 'preview'));
|
|
1771
1773
|
const beforeSummary = summarizeDocument(context.document);
|
|
1772
1774
|
let mutationResult;
|
|
1773
1775
|
try {
|
|
@@ -1855,6 +1857,7 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1855
1857
|
// but ALWAYS reported on the result (passed / failed / noop+reason) so a missing EA
|
|
1856
1858
|
// update is never silent.
|
|
1857
1859
|
{
|
|
1860
|
+
markPhase('mutation:qeaProjection');
|
|
1858
1861
|
const resolved = resolveQeaProjectionTarget(context);
|
|
1859
1862
|
const qeaTarget = resolved && resolved.target;
|
|
1860
1863
|
if (qeaTarget) {
|
|
@@ -1883,6 +1886,7 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1883
1886
|
}
|
|
1884
1887
|
|
|
1885
1888
|
if (shouldSyncCanonicalGraphToNeo4j(context.graphPath.relativePath)) {
|
|
1889
|
+
markPhase('mutation:neo4jSync');
|
|
1886
1890
|
try {
|
|
1887
1891
|
const syncResult = await syncArchitectureToNeo4j({
|
|
1888
1892
|
architecturePath: context.graphPath.relativePath,
|
|
@@ -1907,7 +1911,9 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1907
1911
|
}
|
|
1908
1912
|
}
|
|
1909
1913
|
|
|
1914
|
+
markPhase('mutation:embeddingLifecycle');
|
|
1910
1915
|
await attachMutationEmbeddingLifecycle(context, result, mutationResult.document);
|
|
1916
|
+
markPhase('mutation:done');
|
|
1911
1917
|
|
|
1912
1918
|
return result;
|
|
1913
1919
|
}
|
|
@@ -2127,33 +2133,35 @@ function resolveQeaProjectionTarget(context) {
|
|
|
2127
2133
|
}
|
|
2128
2134
|
|
|
2129
2135
|
function runQeaProjection(target) {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
})
|
|
2136
|
+
// Synchronous child: this runs right before the async Neo4j sync + embedding
|
|
2137
|
+
// lifecycle. An async child leaves a libuv handle closing while subsequent
|
|
2138
|
+
// async I/O (fetch / neo4j-driver) proceeds, which on Windows can trip a
|
|
2139
|
+
// native `UV_HANDLE_CLOSING` assertion and abort the whole MCP. spawnSync has
|
|
2140
|
+
// no lingering handle (~150ms, bounded); the caller's `await` still applies.
|
|
2141
|
+
const script = path.join(__dirname, 'ea-qea-sync.js');
|
|
2142
|
+
const started = Date.now();
|
|
2143
|
+
if (!fs.existsSync(script)) {
|
|
2144
|
+
return { ok: false, error: 'argo/scripts/ea-qea-sync.js missing', ms: 0 };
|
|
2145
|
+
}
|
|
2146
|
+
const snapshotDir = path.join(target.workspaceRoot, '.argo', 'temp', 'qea-backups');
|
|
2147
|
+
// -y enables the projection-owned delete reconcile: objects that carry a schema anchor
|
|
2148
|
+
// (t_object.Alias / t_connectortag schema_id) but are no longer in canonical are removed
|
|
2149
|
+
// from the .qea, so a graph-side deletion actually disappears from EA on the next
|
|
2150
|
+
// projection. Human-drawn (un-anchored) content is never a delete candidate.
|
|
2151
|
+
const args = [script, '--mode', 'sync', '--graph', target.graphPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir, '-y'];
|
|
2152
|
+
try {
|
|
2153
|
+
const res = spawnSync(process.execPath, args, { cwd: target.workspaceRoot, windowsHide: true, encoding: 'utf8', timeout: 120000 });
|
|
2154
|
+
const ok = res.status === 0;
|
|
2155
|
+
return {
|
|
2156
|
+
ok,
|
|
2157
|
+
code: res.status,
|
|
2158
|
+
ms: Date.now() - started,
|
|
2159
|
+
stderr: String(res.stderr || '').slice(0, 600),
|
|
2160
|
+
...(ok ? {} : { error: String((res.stderr || '').slice(0, 600) || ('exit ' + res.status)) }),
|
|
2161
|
+
};
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
return { ok: false, error: String(error && error.message ? error.message : error), ms: Date.now() - started };
|
|
2164
|
+
}
|
|
2157
2165
|
}
|
|
2158
2166
|
|
|
2159
2167
|
function writeGraph(graphPath, document) {
|
|
@@ -2764,6 +2772,7 @@ function evaluateSemanticDedupGate(advisory, mutations) {
|
|
|
2764
2772
|
}
|
|
2765
2773
|
|
|
2766
2774
|
async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
2775
|
+
markPhase('mutation:semanticDedup');
|
|
2767
2776
|
if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
|
|
2768
2777
|
return undefined;
|
|
2769
2778
|
}
|
package/package.json
CHANGED