archgraph-argo 0.20.5 → 0.20.6

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,6 +236,7 @@ async function executeWpP2Retrieval({
233
236
  configuration: configurationEvidence.configuration,
234
237
  transport: composition.transport,
235
238
  });
239
+ markPhase('retrieval:embed');
236
240
  const vector = await provider.embed(request.intent);
237
241
  requireQualifiedVector(vector);
238
242
  const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
@@ -272,6 +276,7 @@ async function executeWpP2Retrieval({
272
276
  channelSeeds.push({ channel, seeds });
273
277
  }
274
278
  if (rerank) {
279
+ markPhase('retrieval:rerank');
275
280
  // Rerank every channel CONCURRENTLY: the LLM calls dominate latency and are
276
281
  // independent, so parallelizing turns the cost from sum(channels) into
277
282
  // ~one call. fail-open: a null/empty order keeps the original ordering.
@@ -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
- return new Promise((resolve) => {
2131
- const script = path.join(__dirname, 'ea-qea-sync.js');
2132
- if (!fs.existsSync(script)) {
2133
- resolve({ ok: false, error: 'argo/scripts/ea-qea-sync.js missing', ms: 0 });
2134
- return;
2135
- }
2136
- const snapshotDir = path.join(target.workspaceRoot, '.argo', 'temp', 'qea-backups');
2137
- // -y enables the projection-owned delete reconcile: objects that carry a schema anchor
2138
- // (t_object.Alias / t_connectortag schema_id) but are no longer in canonical are removed
2139
- // from the .qea, so a graph-side deletion actually disappears from EA on the next
2140
- // projection. Human-drawn (un-anchored) content is never a delete candidate.
2141
- const args = [script, '--mode', 'sync', '--graph', target.graphPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir, '-y'];
2142
- const started = Date.now();
2143
- let stderr = '';
2144
- let child;
2145
- try {
2146
- child = spawn(process.execPath, args, { cwd: target.workspaceRoot, windowsHide: true });
2147
- } catch (error) {
2148
- resolve({ ok: false, error: String(error && error.message ? error.message : error), ms: Date.now() - started });
2149
- return;
2150
- }
2151
- child.stderr.on('data', (d) => { stderr += String(d); });
2152
- child.on('error', (err) => resolve({ ok: false, error: String(err && err.message ? err.message : err), ms: Date.now() - started, stderr: stderr.slice(0, 600) }));
2153
- child.on('close', (code) => {
2154
- resolve({ ok: code === 0, code, ms: Date.now() - started, stderr: stderr.slice(0, 600) });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.20.5",
3
+ "version": "0.20.6",
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": {