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 +1 -1
- package/src/storage/sqlite.js +51 -11
- package/src/task-runner.js +43 -15
- package/src/utils/runtime-config.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
|
-
"version": "1.6.
|
|
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",
|
package/src/storage/sqlite.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
537
|
+
return await withStateDbLockRetry(async () => {
|
|
538
|
+
const { DatabaseSync } = await loadSqliteModule();
|
|
539
|
+
if (!readOnly) {
|
|
540
|
+
ensureStateDir(filePath);
|
|
541
|
+
}
|
|
512
542
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
}
|
package/src/task-runner.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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' : '
|
|
37
|
+
export const projectRootSource = projectRootArg ? 'argv' : projectRootEnv ? 'env' : 'cwd';
|
|
38
38
|
|
|
39
39
|
export const setProjectRoot = (newRoot) => {
|
|
40
40
|
projectRoot = path.resolve(newRoot);
|