@sdsrs/code-graph 0.45.1 → 0.45.3

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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.45.1",
7
+ "version": "0.45.3",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -197,6 +197,20 @@ function cachedBinaryNeedsUpdate(latest, { binaryPath = cachedBinaryPath(), read
197
197
  return readVersion(binaryPath) !== latest.version;
198
198
  }
199
199
 
200
+ /**
201
+ * Throttle-bypass predicate: is a *present* cached binary stale relative to the
202
+ * last known latest release (`state.latestVersion`, set on the previous fetch —
203
+ * no network here)? Used so a present-but-stale binary skips the time-based
204
+ * throttle instead of staying pinned for up to a full check interval. Returns
205
+ * false when there is no prior latestVersion (first run fetches anyway) or the
206
+ * binary is missing (handled by the separate `binaryMissing` bypass).
207
+ */
208
+ function cachedBinaryStaleVsState(state, { binaryPath = cachedBinaryPath(), readVersion = readBinaryVersion } = {}) {
209
+ if (!state || !state.latestVersion) return false;
210
+ if (!fs.existsSync(binaryPath)) return false;
211
+ return readVersion(binaryPath) !== state.latestVersion;
212
+ }
213
+
200
214
  /**
201
215
  * Download just the platform binary from a GitHub release into the cache.
202
216
  * Used in two paths:
@@ -353,6 +367,19 @@ async function downloadAndInstall(latest) {
353
367
 
354
368
  // ── Main Entry ─────────────────────────────────────────────
355
369
 
370
+ /**
371
+ * Self-heal the cached native binary when the plugin shell is already at latest
372
+ * but the binary lags (missing OR a different version). This is the orchestration
373
+ * glue that broke twice in the field (v0.45.1, v0.45.2): the decision predicate
374
+ * was correct, but nothing guaranteed checkForUpdate actually invoked the download
375
+ * on the shell-matches-latest path. Extracted + injectable so the wiring itself is
376
+ * regression-tested, not just the predicate. Returns true iff a download promoted.
377
+ */
378
+ async function selfHealStaleBinary(latest, { needsUpdate = cachedBinaryNeedsUpdate, download = downloadBinary } = {}) {
379
+ if (!needsUpdate(latest)) return false;
380
+ return await download(latest);
381
+ }
382
+
356
383
  async function checkForUpdate({ installMissing = false } = {}) {
357
384
  try {
358
385
  // Skip in dev mode — unless the launcher explicitly requested a missing-
@@ -365,11 +392,14 @@ async function checkForUpdate({ installMissing = false } = {}) {
365
392
  // bypasses auto-update.js, so re-sync state.installedVersion every call.
366
393
  const installedVersion = readManifest().version || '0.0.0';
367
394
 
368
- // Time-based throttle. A missing cache binary is a hard failure (launcher
369
- // cannot start) so it overrides the throttle without this bypass the
370
- // session wedges for up to 6h waiting for the next check window.
395
+ // Time-based throttle. Two conditions override it: a missing cache binary
396
+ // (launcher cannot start) and a present-but-stale binary (otherwise it stays
397
+ // pinned to the old version for up to a full check interval the binary
398
+ // self-heal would never run inside the throttle window). Both bypass to the
399
+ // fetch + self-heal path below.
371
400
  const binaryMissing = !fs.existsSync(cachedBinaryPath());
372
- if (!binaryMissing && !shouldCheck(state)) {
401
+ const binaryStale = cachedBinaryStaleVsState(state);
402
+ if (!binaryMissing && !binaryStale && !shouldCheck(state)) {
373
403
  if (state.installedVersion !== installedVersion) {
374
404
  saveState({ ...state, installedVersion });
375
405
  }
@@ -414,14 +444,10 @@ async function checkForUpdate({ installMissing = false } = {}) {
414
444
  }
415
445
 
416
446
  // No plugin-shell update — but self-heal the native binary if it is missing
417
- // OR stale. The shell version (manifest.version) can match latest while the
418
- // cached binary lags (a previous download failed silently, the cache was
419
- // wiped, an `npm install -g` optionalDependency dropped the platform package,
420
- // or the marketplace bumped the shell without re-fetching the binary).
421
- let selfHealedBinary = false;
422
- if (cachedBinaryNeedsUpdate(latest)) {
423
- selfHealedBinary = await downloadBinary(latest);
424
- }
447
+ // OR stale (see selfHealStaleBinary). The shell version (manifest.version)
448
+ // can match latest while the cached binary lags this is exactly the wild
449
+ // failure observed in the field (shell at v0.45, binary pinned at v0.16.6).
450
+ const selfHealedBinary = await selfHealStaleBinary(latest);
425
451
 
426
452
  saveState({
427
453
  ...state,
@@ -446,7 +472,8 @@ module.exports = {
446
472
  getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
447
473
  isSilentMode, isInstallMissingMode,
448
474
  requestJson, parseLatestRelease, fetchLatestRelease,
449
- downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate,
475
+ downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate, cachedBinaryStaleVsState,
476
+ selfHealStaleBinary,
450
477
  };
451
478
 
452
479
  // CLI: node auto-update.js [check|status] [--silent] [--install-missing]
@@ -14,7 +14,9 @@ const {
14
14
  promoteVerifiedBinary,
15
15
  cachedBinaryPath,
16
16
  cachedBinaryNeedsUpdate,
17
+ cachedBinaryStaleVsState,
17
18
  downloadBinary,
19
+ selfHealStaleBinary,
18
20
  isInstallMissingMode,
19
21
  isSilentMode,
20
22
  } = require('./auto-update');
@@ -116,6 +118,61 @@ test('cachedBinaryNeedsUpdate is version-aware, not existence-only', (t) => {
116
118
  assert.equal(cachedBinaryNeedsUpdate(null, { binaryPath }), false);
117
119
  });
118
120
 
121
+ test('cachedBinaryStaleVsState bypasses throttle only for a present-but-stale binary', (t) => {
122
+ const dir = mkDir(t, 'code-graph-throttle-');
123
+ const binaryPath = path.join(dir, 'code-graph-mcp');
124
+ fs.writeFileSync(binaryPath, 'x'); // present
125
+
126
+ // no prior latestVersion → don't bypass (first run fetches anyway)
127
+ assert.equal(cachedBinaryStaleVsState({}, { binaryPath }), false);
128
+ assert.equal(cachedBinaryStaleVsState(null, { binaryPath }), false);
129
+
130
+ // present + stale vs last known latest → bypass throttle (the 6h-gap fix)
131
+ assert.equal(
132
+ cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.16.6' }),
133
+ true,
134
+ );
135
+
136
+ // present + current → stay throttled
137
+ assert.equal(
138
+ cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.45.1' }),
139
+ false,
140
+ );
141
+
142
+ // missing binary → false here (the separate binaryMissing bypass handles it)
143
+ fs.rmSync(binaryPath);
144
+ assert.equal(cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath }), false);
145
+ });
146
+
147
+ test('selfHealStaleBinary wires the stale-binary check to a download (the v0.45.x glue)', async () => {
148
+ const latest = { version: '0.45.2', binaryUrl: 'https://example/bin' };
149
+
150
+ // Field failure mode: shell already at latest, binary pinned stale → MUST download.
151
+ let downloaded = false;
152
+ const healed = await selfHealStaleBinary(latest, {
153
+ needsUpdate: () => true,
154
+ download: async () => { downloaded = true; return true; },
155
+ });
156
+ assert.equal(downloaded, true, 'stale binary must trigger a download');
157
+ assert.equal(healed, true);
158
+
159
+ // Binary current → no download, no-op.
160
+ let touched = false;
161
+ const noop = await selfHealStaleBinary(latest, {
162
+ needsUpdate: () => false,
163
+ download: async () => { touched = true; return true; },
164
+ });
165
+ assert.equal(touched, false, 'current binary must not download');
166
+ assert.equal(noop, false);
167
+
168
+ // Download fails (no curl / network) → returns false so the next session retries.
169
+ const failed = await selfHealStaleBinary(latest, {
170
+ needsUpdate: () => true,
171
+ download: async () => false,
172
+ });
173
+ assert.equal(failed, false);
174
+ });
175
+
119
176
  test('parseLatestRelease selects the matching platform asset', () => {
120
177
  const latest = parseLatestRelease({
121
178
  tag_name: 'v1.2.3',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.45.1",
3
+ "version": "0.45.3",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.45.1",
39
- "@sdsrs/code-graph-linux-arm64": "0.45.1",
40
- "@sdsrs/code-graph-darwin-x64": "0.45.1",
41
- "@sdsrs/code-graph-darwin-arm64": "0.45.1",
42
- "@sdsrs/code-graph-win32-x64": "0.45.1"
38
+ "@sdsrs/code-graph-linux-x64": "0.45.3",
39
+ "@sdsrs/code-graph-linux-arm64": "0.45.3",
40
+ "@sdsrs/code-graph-darwin-x64": "0.45.3",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.45.3",
42
+ "@sdsrs/code-graph-win32-x64": "0.45.3"
43
43
  }
44
44
  }