@sdsrs/code-graph 0.85.6 → 0.85.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.85.6",
7
+ "version": "0.85.8",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -33,7 +33,9 @@ function commandExists(cmd) {
33
33
  const GITHUB_REPO = 'sdsrss/code-graph-mcp';
34
34
  const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
35
35
  const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
36
- const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
36
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h — steady-state re-check
37
+ const UP_TO_DATE_RECHECK_MS = 30 * 60 * 1000; // 30min — re-verify an "up to date" result (release-race guard)
38
+ const SESSION_START_MIN_GAP_MS = 2 * 60 * 1000; // 2min — anti-hammer floor for forced (session-start) checks
37
39
  const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
38
40
  const FETCH_TIMEOUT_MS = 3000;
39
41
 
@@ -45,6 +47,13 @@ function isInstallMissingMode(argv = process.argv.slice(2)) {
45
47
  return argv.includes('--install-missing');
46
48
  }
47
49
 
50
+ // High-intent trigger (session start / explicit reload) → bypass the soft
51
+ // throttle so an available update is picked up immediately, not on the next
52
+ // 6h/30min tick. Passed by session-init's launchBackgroundAutoUpdate.
53
+ function isForceMode(argv = process.argv.slice(2)) {
54
+ return argv.includes('--force');
55
+ }
56
+
48
57
  // ── Platform → GitHub release asset name mapping ──────────
49
58
  function getPlatformAssetName() {
50
59
  const platform = os.platform();
@@ -74,10 +83,25 @@ function saveState(state) {
74
83
 
75
84
  // ── Throttle ───────────────────────────────────────────────
76
85
 
77
- function shouldCheck(state) {
86
+ // Whether to hit GitHub now. Keyed to the previous check's outcome, with a force
87
+ // override for high-intent triggers (session start / explicit reload). Ordering:
88
+ // 1. rate-limit backoff (24h) wins over everything — never push more requests
89
+ // into a GitHub 403.
90
+ // 2. force → only the short SESSION_START_MIN_GAP_MS floor applies, so opening
91
+ // a new session re-checks immediately while a crash/reopen loop still can't
92
+ // hammer the API.
93
+ // 3. otherwise → an "up to date" result is re-verified on a short cadence
94
+ // (UP_TO_DATE_RECHECK_MS). This is the release-publish race guard: a version
95
+ // can go live seconds AFTER a check that said "up to date", and the plain 6h
96
+ // interval left it invisible for the full 6h (observed live — v0.85.7
97
+ // published 8s after a check pinned v0.85.6). A pending-but-unfinished update
98
+ // keeps the 6h steady-state interval.
99
+ function shouldCheck(state, { force = false } = {}) {
78
100
  if (!state.lastCheck) return true;
79
101
  const elapsed = Date.now() - new Date(state.lastCheck).getTime();
80
- const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
102
+ if (state.rateLimited) return elapsed >= RATE_LIMIT_INTERVAL_MS;
103
+ if (force) return elapsed >= SESSION_START_MIN_GAP_MS;
104
+ const interval = state.updateAvailable === false ? UP_TO_DATE_RECHECK_MS : CHECK_INTERVAL_MS;
81
105
  return elapsed >= interval;
82
106
  }
83
107
 
@@ -472,7 +496,7 @@ async function selfHealStaleBinary(latest, { needsUpdate = cachedBinaryNeedsUpda
472
496
  return await download(latest);
473
497
  }
474
498
 
475
- async function checkForUpdate({ installMissing = false } = {}) {
499
+ async function checkForUpdate({ installMissing = false, force = false } = {}) {
476
500
  try {
477
501
  // Skip in dev mode — unless the launcher explicitly requested a missing-
478
502
  // binary install, in which case we MUST proceed regardless of mode (the
@@ -491,7 +515,7 @@ async function checkForUpdate({ installMissing = false } = {}) {
491
515
  // fetch + self-heal path below.
492
516
  const binaryMissing = !fs.existsSync(cachedBinaryPath());
493
517
  const binaryStale = cachedBinaryStaleVsState(state);
494
- if (!binaryMissing && !binaryStale && !shouldCheck(state)) {
518
+ if (!binaryMissing && !binaryStale && !shouldCheck(state, { force })) {
495
519
  if (state.installedVersion !== installedVersion) {
496
520
  saveState({ ...state, installedVersion });
497
521
  }
@@ -567,9 +591,9 @@ async function checkForUpdate({ installMissing = false } = {}) {
567
591
  }
568
592
 
569
593
  module.exports = {
570
- checkForUpdate, commandExists, isDevMode, readState, compareVersions,
594
+ checkForUpdate, commandExists, isDevMode, readState, compareVersions, shouldCheck,
571
595
  getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
572
- isSilentMode, isInstallMissingMode,
596
+ isSilentMode, isInstallMissingMode, isForceMode,
573
597
  requestJson, parseLatestRelease, fetchLatestRelease,
574
598
  downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate, cachedBinaryStaleVsState,
575
599
  selfHealStaleBinary,
@@ -583,12 +607,13 @@ if (require.main === module) {
583
607
  const cmd = argv.find(arg => !arg.startsWith('--')) || 'check';
584
608
  const silent = isSilentMode(argv);
585
609
  const installMissing = isInstallMissingMode(argv);
610
+ const force = isForceMode(argv);
586
611
  if (cmd === 'status') {
587
612
  const state = readState();
588
613
  console.log(JSON.stringify(state, null, 2));
589
614
  } else {
590
615
  if (!silent) console.log('Checking for updates...');
591
- const result = await checkForUpdate({ installMissing });
616
+ const result = await checkForUpdate({ installMissing, force });
592
617
  if (silent) return;
593
618
  if (result && result.updated) {
594
619
  console.log(`Updated: v${result.from} → v${result.to} (binary: ${result.binaryUpdated ? 'yes' : 'no'})`);
@@ -20,6 +20,7 @@ const {
20
20
  selfHealStaleBinary,
21
21
  isInstallMissingMode,
22
22
  isSilentMode,
23
+ shouldCheck,
23
24
  } = require('./auto-update');
24
25
 
25
26
  function mkDir(t, prefix) {
@@ -180,6 +181,45 @@ test('cachedBinaryStaleVsState bypasses throttle only for a present-but-stale bi
180
181
  assert.equal(cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath }), false);
181
182
  });
182
183
 
184
+ test('shouldCheck re-verifies an up-to-date state on a short cadence (release-publish race)', () => {
185
+ const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
186
+
187
+ // never checked → always check
188
+ assert.equal(shouldCheck({}), true);
189
+
190
+ // Bug repro: the last check reported "up to date" (updateAvailable:false) and a
191
+ // release published moments later. 45min on, the plain 6h throttle kept the
192
+ // stale answer latched (every session reopen re-reported up-to-date); the short
193
+ // up-to-date cadence must allow a re-check so the new release is discovered.
194
+ assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: false }), true);
195
+
196
+ // within the short window → still throttled (don't hammer the API every call)
197
+ assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
198
+
199
+ // a pending-but-unfinished update keeps the 6h steady-state interval
200
+ assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: true }), false);
201
+
202
+ // rate-limit backoff (24h) wins even over the up-to-date short cadence
203
+ assert.equal(shouldCheck({ lastCheck: minsAgo(120), updateAvailable: false, rateLimited: true }), false);
204
+ });
205
+
206
+ test('shouldCheck lets a forced (session-start) check bypass the soft throttle', () => {
207
+ const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
208
+
209
+ // A new session / explicit reload is a strong "get me latest" signal: a forced
210
+ // check runs even inside the 30min up-to-date window (contrast the non-forced
211
+ // call on the same state, which stays throttled).
212
+ assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }, { force: true }), true);
213
+ assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
214
+
215
+ // ...but a short anti-hammer floor still applies, so a crash/reopen loop can't
216
+ // pound the GitHub API on every restart.
217
+ assert.equal(shouldCheck({ lastCheck: minsAgo(0.5), updateAvailable: false }, { force: true }), false);
218
+
219
+ // Rate-limit backoff wins even over force — never push more requests into a 403.
220
+ assert.equal(shouldCheck({ lastCheck: minsAgo(60), updateAvailable: false, rateLimited: true }, { force: true }), false);
221
+ });
222
+
183
223
  test('selfHealStaleBinary wires the stale-binary check to a download (the v0.45.x glue)', async () => {
184
224
  const latest = { version: '0.45.2', binaryUrl: 'https://example/bin' };
185
225
 
@@ -166,9 +166,14 @@ function formatRecentImpact(changed, affected, dependentCap = 6) {
166
166
  return lines.join('\n');
167
167
  }
168
168
 
169
- function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
169
+ function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env, { force = false } = {}) {
170
170
  try {
171
- const child = spawnFn(process.execPath, [path.join(__dirname, 'auto-update.js'), 'check', '--silent'], {
171
+ const args = [path.join(__dirname, 'auto-update.js'), 'check', '--silent'];
172
+ // A session start / reload forces an immediate check (bypasses the soft
173
+ // throttle down to auto-update.js's short anti-hammer floor + rate-limit
174
+ // backoff), so an available update is picked up now rather than on the next tick.
175
+ if (force) args.push('--force');
176
+ const child = spawnFn(process.execPath, args, {
172
177
  detached: true,
173
178
  stdio: 'ignore',
174
179
  env: { ...env, CODE_GRAPH_AUTO_UPDATE_SILENT: '1' },
@@ -180,6 +185,14 @@ function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
180
185
  }
181
186
  }
182
187
 
188
+ // A session start / resume / clear / explicit reload is a strong "I'm here, get
189
+ // me the latest" signal → force an immediate update check. Automatic mid-session
190
+ // compaction is not high-intent, so it keeps auto-update.js's gentle background
191
+ // cadence. Unknown source (direct calls / tests) is treated as high-intent.
192
+ function isHighIntentSource(source) {
193
+ return source !== 'compact';
194
+ }
195
+
183
196
  function syncLifecycleConfig() {
184
197
  // v0.49.1: stale-relic guard. A still-running Claude Code process fires
185
198
  // SessionStart from the plugin-cache dir it loaded at startup; after
@@ -509,7 +522,7 @@ function runSessionInit({ source } = {}) {
509
522
  // Verify binary availability — catch issues early with actionable diagnostics
510
523
  const binaryCheck = verifyBinary();
511
524
 
512
- const autoUpdateLaunched = launchBackgroundAutoUpdate();
525
+ const autoUpdateLaunched = launchBackgroundAutoUpdate(spawn, process.env, { force: isHighIntentSource(source) });
513
526
  const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
514
527
 
515
528
  // 上下文感知默认:插件模式下首次 SessionStart 自动安装(创建/注入 CLAUDE.md 块 +
@@ -752,6 +765,7 @@ function detectHookDark() {
752
765
 
753
766
  module.exports = {
754
767
  launchBackgroundAutoUpdate,
768
+ isHighIntentSource,
755
769
  syncLifecycleConfig,
756
770
  ensureIndexFresh,
757
771
  indexNeedsRevalidation,
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const os = require('os');
8
- const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, indexNeedsRevalidation, verifyBinary, computeQuietHooks, shouldInjectMap, shouldInjectRecentImpact, recentImpactWorthShowing, filterSourceFiles, parseGitStatusPaths, formatRecentImpact } = require('./session-init');
8
+ const { launchBackgroundAutoUpdate, isHighIntentSource, syncLifecycleConfig, ensureIndexFresh, indexNeedsRevalidation, verifyBinary, computeQuietHooks, shouldInjectMap, shouldInjectRecentImpact, recentImpactWorthShowing, filterSourceFiles, parseGitStatusPaths, formatRecentImpact } = require('./session-init');
9
9
 
10
10
  // Write an executable stub named `code-graph-mcp` that emits `json` to stdout on
11
11
  // `health-check` and exits with `exitCode`. Mirrors how the real binary behaves:
@@ -121,6 +121,28 @@ test('launchBackgroundAutoUpdate spawns detached silent updater', () => {
121
121
  assert.equal(calls[0].unrefCalled, true);
122
122
  });
123
123
 
124
+ test('launchBackgroundAutoUpdate forwards --force only when asked (session-start bypass)', () => {
125
+ const calls = [];
126
+ const capture = (_command, args) => {
127
+ calls.push({ args });
128
+ return { unref() {} };
129
+ };
130
+
131
+ launchBackgroundAutoUpdate(capture, {}, { force: true });
132
+ assert.deepEqual(calls[0].args.slice(1), ['check', '--silent', '--force']);
133
+
134
+ launchBackgroundAutoUpdate(capture, {}); // default → no --force
135
+ assert.deepEqual(calls[1].args.slice(1), ['check', '--silent']);
136
+ });
137
+
138
+ test('isHighIntentSource forces on session start/resume/clear but not automatic compaction', () => {
139
+ assert.equal(isHighIntentSource('startup'), true);
140
+ assert.equal(isHighIntentSource('resume'), true);
141
+ assert.equal(isHighIntentSource('clear'), true);
142
+ assert.equal(isHighIntentSource(undefined), true); // direct call / unknown → high intent
143
+ assert.equal(isHighIntentSource('compact'), false); // frequent + automatic → gentle cadence
144
+ });
145
+
124
146
  const { consistencyCheck, runSessionInit } = require('./session-init');
125
147
 
126
148
  test('consistencyCheck is exported as a function', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.85.6",
3
+ "version": "0.85.8",
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.85.6",
39
- "@sdsrs/code-graph-linux-arm64": "0.85.6",
40
- "@sdsrs/code-graph-darwin-x64": "0.85.6",
41
- "@sdsrs/code-graph-darwin-arm64": "0.85.6",
42
- "@sdsrs/code-graph-win32-x64": "0.85.6"
38
+ "@sdsrs/code-graph-linux-x64": "0.85.8",
39
+ "@sdsrs/code-graph-linux-arm64": "0.85.8",
40
+ "@sdsrs/code-graph-darwin-x64": "0.85.8",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.85.8",
42
+ "@sdsrs/code-graph-win32-x64": "0.85.8"
43
43
  }
44
44
  }