smart-context-mcp 1.6.0 → 1.6.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
5
5
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
6
6
  "type": "module",
@@ -2,12 +2,16 @@ import fs from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { setTimeout as delay } from 'node:timers/promises';
5
6
  import { projectRoot } from '../utils/runtime-config.js';
6
7
 
7
8
  export const STATE_DB_FILENAME = 'state.sqlite';
8
9
  export const SQLITE_SCHEMA_VERSION = 5;
9
10
  export const ACTIVE_SESSION_SCOPE = 'project';
10
11
  export const STATE_DB_SOFT_MAX_BYTES = 32 * 1024 * 1024;
12
+ const STATE_DB_BUSY_TIMEOUT_MS = 1000;
13
+ const STATE_DB_LOCK_RETRY_ATTEMPTS = 3;
14
+ const STATE_DB_LOCK_RETRY_DELAY_MS = 75;
11
15
  export const EXPECTED_TABLES = [
12
16
  'active_session',
13
17
  'context_access',
@@ -444,7 +448,32 @@ export const getMeta = (db, key) => {
444
448
  const getSchemaVersion = (db) => Number(getMeta(db, 'schema_version') ?? 0);
445
449
  const VALID_STATUSES = new Set(['planning', 'in_progress', 'blocked', 'completed']);
446
450
 
447
- const applyPragmas = (db) => {
451
+ const isStateDbLockError = (error) =>
452
+ /database is locked|database table is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(String(error?.message ?? error ?? ''));
453
+
454
+ const withStateDbLockRetry = async (operation) => {
455
+ for (let attempt = 1; attempt <= STATE_DB_LOCK_RETRY_ATTEMPTS; attempt += 1) {
456
+ try {
457
+ return await operation();
458
+ } catch (error) {
459
+ if (!isStateDbLockError(error) || attempt === STATE_DB_LOCK_RETRY_ATTEMPTS) {
460
+ throw error;
461
+ }
462
+
463
+ await delay(STATE_DB_LOCK_RETRY_DELAY_MS * attempt);
464
+ }
465
+ }
466
+
467
+ throw new Error('SQLite lock retry exhausted unexpectedly.');
468
+ };
469
+
470
+ const applyPragmas = (db, { readOnly = false } = {}) => {
471
+ db.exec(`PRAGMA busy_timeout = ${STATE_DB_BUSY_TIMEOUT_MS}`);
472
+
473
+ if (readOnly) {
474
+ return;
475
+ }
476
+
448
477
  db.exec('PRAGMA foreign_keys = ON');
449
478
  db.exec('PRAGMA journal_mode = WAL');
450
479
  db.exec('PRAGMA synchronous = NORMAL');
@@ -505,17 +534,28 @@ export const listStateTables = (db) =>
505
534
 
506
535
  export const openStateDb = async ({ filePath = getStateDbPath(), readOnly = false } = {}) => {
507
536
  try {
508
- const { DatabaseSync } = await loadSqliteModule();
509
- if (!readOnly) {
510
- ensureStateDir(filePath);
511
- }
537
+ return await withStateDbLockRetry(async () => {
538
+ const { DatabaseSync } = await loadSqliteModule();
539
+ if (!readOnly) {
540
+ ensureStateDir(filePath);
541
+ }
512
542
 
513
- const db = new DatabaseSync(filePath, readOnly ? { readOnly: true } : {});
514
- if (!readOnly) {
515
- applyPragmas(db);
516
- runStateMigrations(db);
517
- }
518
- return db;
543
+ let db = null;
544
+
545
+ try {
546
+ db = new DatabaseSync(filePath, readOnly ? { readOnly: true } : {});
547
+ applyPragmas(db, { readOnly });
548
+ if (!readOnly) {
549
+ runStateMigrations(db);
550
+ }
551
+ return db;
552
+ } catch (error) {
553
+ try {
554
+ db?.close();
555
+ } catch {}
556
+ throw error;
557
+ }
558
+ });
519
559
  } catch (error) {
520
560
  throw enrichStateDbError(error, { filePath, readOnly });
521
561
  }
@@ -1,3 +1,4 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import { persistMetrics } from './metrics.js';
2
3
  import { countTokens } from './tokenCounter.js';
3
4
  import { TASK_RUNNER_QUALITY_ANALYTICS_KIND } from './analytics/product-quality.js';
@@ -22,6 +23,8 @@ import {
22
23
 
23
24
  const START_MAX_TOKENS = 350;
24
25
  const END_MAX_TOKENS = 350;
26
+ const RUNNER_LOCK_RETRY_ATTEMPTS = 3;
27
+ const RUNNER_LOCK_RETRY_DELAY_MS = 100;
25
28
 
26
29
  const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
27
30
  const truncate = (value, maxLength = 160) => {
@@ -39,6 +42,13 @@ const truncate = (value, maxLength = 160) => {
39
42
 
40
43
  const asArray = (value) => Array.isArray(value) ? value : [];
41
44
  const uniqueCompact = (values) => [...new Set(asArray(values).map((value) => normalizeWhitespace(value)).filter(Boolean))];
45
+ const extractContextTopFiles = (topFiles) => uniqueCompact(asArray(topFiles).map((item) => {
46
+ if (typeof item === 'string') {
47
+ return item;
48
+ }
49
+
50
+ return item?.file ?? item?.path ?? '';
51
+ })).slice(0, 3);
42
52
 
43
53
  const extractPreflightTopFiles = (preflightResult) => {
44
54
  if (!preflightResult) {
@@ -50,12 +60,7 @@ const extractPreflightTopFiles = (preflightResult) => {
50
60
  }
51
61
 
52
62
  if (preflightResult.tool === 'smart_search') {
53
- return uniqueCompact(asArray(preflightResult.result?.topFiles).map((item) => {
54
- if (typeof item === 'string') {
55
- return item;
56
- }
57
- return item?.file ?? item?.path ?? '';
58
- })).slice(0, 3);
63
+ return extractContextTopFiles(preflightResult.result?.topFiles);
59
64
  }
60
65
 
61
66
  return [];
@@ -100,7 +105,7 @@ const buildPreflightTask = ({ workflowProfile, prompt, startResult }) => {
100
105
  const normalizedPrompt = normalizeWhitespace(prompt);
101
106
  const persistedNextStep = normalizeWhitespace(startResult?.summary?.nextStep);
102
107
  const currentFocus = normalizeWhitespace(startResult?.summary?.currentFocus);
103
- const refreshedTopFiles = uniqueCompact(startResult?.refreshedContext?.topFiles).slice(0, 3);
108
+ const refreshedTopFiles = extractContextTopFiles(startResult?.refreshedContext?.topFiles);
104
109
 
105
110
  if (workflowProfile.commandName === 'continue' || workflowProfile.commandName === 'resume') {
106
111
  if (persistedNextStep) {
@@ -178,7 +183,7 @@ const buildContinuityGuidance = ({ startResult }) => {
178
183
  ];
179
184
  const nextStep = normalizeWhitespace(startResult?.summary?.nextStep);
180
185
  const currentFocus = normalizeWhitespace(startResult?.summary?.currentFocus);
181
- const refreshedTopFiles = uniqueCompact(startResult?.refreshedContext?.topFiles).slice(0, 3);
186
+ const refreshedTopFiles = extractContextTopFiles(startResult?.refreshedContext?.topFiles);
182
187
  const recommendedNextTools = asArray(startResult?.recommendedPath?.nextTools)
183
188
  .map((tool) => normalizeWhitespace(tool))
184
189
  .filter(Boolean)
@@ -253,6 +258,29 @@ const buildWorkflowPolicyPayload = ({ commandName, workflowProfile, preflightSum
253
258
  preflight: preflightSummary,
254
259
  });
255
260
 
261
+ const isRetriableLockError = (error) => {
262
+ const issue = error?.storageHealth?.issue ?? error?.cause?.storageHealth?.issue ?? null;
263
+ const retriable = error?.storageHealth?.retriable ?? error?.cause?.storageHealth?.retriable ?? false;
264
+ const message = String(error?.message ?? error?.cause?.message ?? error ?? '');
265
+ return retriable || issue === 'locked' || /database is locked|database table is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(message);
266
+ };
267
+
268
+ const withRunnerLockRetry = async (operation) => {
269
+ for (let attempt = 1; attempt <= RUNNER_LOCK_RETRY_ATTEMPTS; attempt += 1) {
270
+ try {
271
+ return await operation();
272
+ } catch (error) {
273
+ if (!isRetriableLockError(error) || attempt === RUNNER_LOCK_RETRY_ATTEMPTS) {
274
+ throw error;
275
+ }
276
+
277
+ await delay(RUNNER_LOCK_RETRY_DELAY_MS * attempt);
278
+ }
279
+ }
280
+
281
+ throw new Error('Task runner lock retry exhausted unexpectedly.');
282
+ };
283
+
256
284
  const recordRunnerMetrics = async ({
257
285
  commandName,
258
286
  client,
@@ -330,13 +358,13 @@ const runWorkflowCommand = async ({
330
358
  });
331
359
  const workflowProfile = buildWorkflowPolicyProfile({ commandName });
332
360
 
333
- const start = await smartTurn({
361
+ const start = await withRunnerLockRetry(() => smartTurn({
334
362
  phase: 'start',
335
363
  sessionId,
336
364
  prompt: requestedPrompt,
337
365
  ensureSession: true,
338
366
  maxTokens: START_MAX_TOKENS,
339
- });
367
+ }));
340
368
 
341
369
  const gate = evaluateRunnerGate({ startResult: start });
342
370
  let preflightSummary = null;
@@ -363,7 +391,7 @@ const runWorkflowCommand = async ({
363
391
  });
364
392
 
365
393
  if (gate.requiresDoctor && !allowDegraded) {
366
- const doctor = await smartDoctor();
394
+ const doctor = await withRunnerLockRetry(() => smartDoctor());
367
395
  const blockedResult = buildRunnerBlockedResult({
368
396
  commandName,
369
397
  client,
@@ -423,7 +451,7 @@ const runWorkflowCommand = async ({
423
451
  };
424
452
 
425
453
  const runDoctorCommand = async ({ verifyIntegrity = true, client }) => {
426
- const result = await smartDoctor({ verifyIntegrity });
454
+ const result = await withRunnerLockRetry(() => smartDoctor({ verifyIntegrity }));
427
455
  await recordRunnerMetrics({
428
456
  commandName: 'doctor',
429
457
  client,
@@ -436,7 +464,7 @@ const runDoctorCommand = async ({ verifyIntegrity = true, client }) => {
436
464
  };
437
465
 
438
466
  const runStatusCommand = async ({ format = 'compact', maxItems = 10, client }) => {
439
- const result = await smartStatus({ format, maxItems });
467
+ const result = await withRunnerLockRetry(() => smartStatus({ format, maxItems }));
440
468
  await recordRunnerMetrics({
441
469
  commandName: 'status',
442
470
  client,
@@ -451,13 +479,13 @@ const runCheckpointCommand = async ({
451
479
  event = 'milestone',
452
480
  update = {},
453
481
  }) => {
454
- const result = await smartTurn({
482
+ const result = await withRunnerLockRetry(() => smartTurn({
455
483
  phase: 'end',
456
484
  sessionId,
457
485
  event,
458
486
  update,
459
487
  maxTokens: END_MAX_TOKENS,
460
- });
488
+ }));
461
489
  await recordRunnerMetrics({
462
490
  commandName: 'checkpoint',
463
491
  client,
@@ -27,14 +27,14 @@ const readEnvValue = (...names) => {
27
27
  };
28
28
 
29
29
  const defaultDevctxRoot = path.resolve(currentDir, '..', '..');
30
- const defaultProjectRoot = path.resolve(defaultDevctxRoot, '..', '..');
30
+ const defaultProjectRoot = path.resolve(process.cwd());
31
31
  const projectRootArg = readArgValue('--project-root');
32
32
  const projectRootEnv = readEnvValue('DEVCTX_PROJECT_ROOT', 'MCP_PROJECT_ROOT');
33
33
  const rawProjectRoot = projectRootArg ?? projectRootEnv ?? defaultProjectRoot;
34
34
 
35
35
  export const devctxRoot = defaultDevctxRoot;
36
36
  export let projectRoot = path.resolve(rawProjectRoot);
37
- export const projectRootSource = projectRootArg ? 'argv' : projectRootEnv ? 'env' : 'default';
37
+ export const projectRootSource = projectRootArg ? 'argv' : projectRootEnv ? 'env' : 'cwd';
38
38
 
39
39
  export const setProjectRoot = (newRoot) => {
40
40
  projectRoot = path.resolve(newRoot);