shieldcortex 3.4.13 → 3.4.15
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/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +4 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +4 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/2473c16c0c2f6b5f.css +2 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/2613246b017827b8.css +1 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/5bbc9f4c42f6d07a.js +9 -0
- package/dist/api/routes/memories.js +10 -5
- package/dist/database/init.d.ts +22 -0
- package/dist/database/init.js +242 -50
- package/dist/memory/consolidate.js +2 -2
- package/dist/memory/contradiction.js +3 -1
- package/dist/memory/store.js +8 -0
- package/package.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/6fb35de00da9b448.js +0 -9
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/d1a51ed13ed3d5d9.css +0 -3
- /package/dashboard/.next/standalone/dashboard/.next/static/{924VfjczmI9xxxvnt8rVE → 7A-bqTP1Le-_398gUtAL4}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{924VfjczmI9xxxvnt8rVE → 7A-bqTP1Le-_398gUtAL4}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{924VfjczmI9xxxvnt8rVE → 7A-bqTP1Le-_398gUtAL4}/_ssgManifest.js +0 -0
|
@@ -343,29 +343,34 @@ export function registerMemoryRoutes(app, deps) {
|
|
|
343
343
|
const params = project ? [project] : [];
|
|
344
344
|
const stale = db.prepare(`
|
|
345
345
|
SELECT * FROM memories
|
|
346
|
-
WHERE
|
|
346
|
+
WHERE COALESCE(status, 'active') NOT IN ('archived', 'suppressed')
|
|
347
|
+
AND decayed_score < 0.3 ${projectFilter}
|
|
347
348
|
AND last_accessed < datetime('now', '-30 days')
|
|
348
349
|
ORDER BY decayed_score ASC LIMIT ?
|
|
349
350
|
`).all(...params, limit);
|
|
350
351
|
const neverUsed = db.prepare(`
|
|
351
352
|
SELECT * FROM memories
|
|
352
|
-
WHERE
|
|
353
|
+
WHERE COALESCE(status, 'active') NOT IN ('archived', 'suppressed')
|
|
354
|
+
AND access_count = 0 ${projectFilter}
|
|
353
355
|
AND created_at < datetime('now', '-1 day')
|
|
354
356
|
ORDER BY created_at DESC LIMIT ?
|
|
355
357
|
`).all(...params, limit);
|
|
356
358
|
const lowTrust = db.prepare(`
|
|
357
359
|
SELECT * FROM memories
|
|
358
|
-
WHERE
|
|
360
|
+
WHERE COALESCE(status, 'active') NOT IN ('archived', 'suppressed')
|
|
361
|
+
AND trust_score < 0.7 ${projectFilter}
|
|
359
362
|
ORDER BY trust_score ASC, updated_at DESC LIMIT ?
|
|
360
363
|
`).all(...params, limit);
|
|
361
364
|
const noisyAutoExtracted = db.prepare(`
|
|
362
365
|
SELECT * FROM memories
|
|
363
|
-
WHERE (
|
|
366
|
+
WHERE COALESCE(status, 'active') NOT IN ('archived', 'suppressed')
|
|
367
|
+
AND (capture_method = 'auto' OR tags LIKE '%auto-extracted%') ${projectFilter}
|
|
364
368
|
ORDER BY updated_at DESC LIMIT ?
|
|
365
369
|
`).all(...params, limit);
|
|
366
370
|
const projectless = db.prepare(`
|
|
367
371
|
SELECT * FROM memories
|
|
368
|
-
WHERE (
|
|
372
|
+
WHERE COALESCE(status, 'active') NOT IN ('archived', 'suppressed')
|
|
373
|
+
AND (project IS NULL OR project = '') AND scope != 'global'
|
|
369
374
|
ORDER BY updated_at DESC LIMIT ?
|
|
370
375
|
`).all(limit);
|
|
371
376
|
const openClawSummary = db.prepare(`
|
package/dist/database/init.d.ts
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
* Database initialization and connection management
|
|
3
3
|
*/
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
|
+
type RuntimeKind = 'installed' | 'npx-cache' | 'project-checkout' | 'unknown';
|
|
6
|
+
interface RuntimeInfo {
|
|
7
|
+
kind: RuntimeKind;
|
|
8
|
+
entryPath: string;
|
|
9
|
+
}
|
|
10
|
+
interface DatabaseInspection {
|
|
11
|
+
integrity: string;
|
|
12
|
+
count: number | null;
|
|
13
|
+
}
|
|
14
|
+
interface BackupCandidate {
|
|
15
|
+
path: string;
|
|
16
|
+
count: number;
|
|
17
|
+
mtimeMs: number;
|
|
18
|
+
}
|
|
19
|
+
declare function resolveRuntimeInfo(): RuntimeInfo;
|
|
20
|
+
declare function enforceSafeRuntimePath(expandedPath: string, explicitDbPath: boolean): void;
|
|
21
|
+
declare function inspectDatabaseFile(dbPath: string): DatabaseInspection;
|
|
22
|
+
declare function listHealthyBackups(dbPath: string): BackupCandidate[];
|
|
5
23
|
declare function isLikelyFtsIntegrityIssue(integrityResult: string): boolean;
|
|
6
24
|
declare function attemptFtsRecovery(database: Database.Database): boolean;
|
|
7
25
|
declare function verifyOnDiskIntegrity(dbPath: string): string;
|
|
@@ -9,6 +27,10 @@ export declare const __databaseTestUtils: {
|
|
|
9
27
|
isLikelyFtsIntegrityIssue: typeof isLikelyFtsIntegrityIssue;
|
|
10
28
|
attemptFtsRecovery: typeof attemptFtsRecovery;
|
|
11
29
|
verifyOnDiskIntegrity: typeof verifyOnDiskIntegrity;
|
|
30
|
+
inspectDatabaseFile: typeof inspectDatabaseFile;
|
|
31
|
+
listHealthyBackups: typeof listHealthyBackups;
|
|
32
|
+
resolveRuntimeInfo: typeof resolveRuntimeInfo;
|
|
33
|
+
enforceSafeRuntimePath: typeof enforceSafeRuntimePath;
|
|
12
34
|
};
|
|
13
35
|
/**
|
|
14
36
|
* Initialize the database connection
|
package/dist/database/init.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Database initialization and connection management
|
|
3
3
|
*/
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, unlinkSync, renameSync, copyFileSync } from 'fs';
|
|
6
|
-
import { dirname, join } from 'path';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, unlinkSync, renameSync, copyFileSync, readdirSync, openSync, closeSync, realpathSync } from 'fs';
|
|
6
|
+
import { basename, dirname, join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
@@ -13,6 +13,7 @@ const _currentDir = dirname(_currentFile);
|
|
|
13
13
|
let db = null;
|
|
14
14
|
let currentDbPath = null;
|
|
15
15
|
let lockFilePath = null;
|
|
16
|
+
let lockFileFd = null;
|
|
16
17
|
// Anti-bloat: Database size limits
|
|
17
18
|
const MAX_DB_SIZE = 100 * 1024 * 1024; // 100MB hard limit
|
|
18
19
|
const WARN_DB_SIZE = 50 * 1024 * 1024; // 50MB warning threshold
|
|
@@ -40,6 +41,177 @@ function getDefaultDbPath() {
|
|
|
40
41
|
// Fall back to legacy path for existing users
|
|
41
42
|
return legacyPath;
|
|
42
43
|
}
|
|
44
|
+
function resolveRuntimeInfo() {
|
|
45
|
+
let entryPath = process.argv[1] ?? '';
|
|
46
|
+
try {
|
|
47
|
+
if (entryPath) {
|
|
48
|
+
entryPath = realpathSync(entryPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Fall back to the raw argv path.
|
|
53
|
+
}
|
|
54
|
+
if (entryPath.includes('/.npm/_npx/')) {
|
|
55
|
+
return { kind: 'npx-cache', entryPath };
|
|
56
|
+
}
|
|
57
|
+
if (entryPath.includes('/node_modules/shieldcortex/')) {
|
|
58
|
+
return { kind: 'installed', entryPath };
|
|
59
|
+
}
|
|
60
|
+
if (entryPath.includes('/ShieldCortex/') || entryPath.includes('\\ShieldCortex\\')) {
|
|
61
|
+
return { kind: 'project-checkout', entryPath };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'unknown', entryPath };
|
|
64
|
+
}
|
|
65
|
+
function enforceSafeRuntimePath(expandedPath, explicitDbPath) {
|
|
66
|
+
if (explicitDbPath || process.env.SHIELDCORTEX_ALLOW_UNSAFE_RUNTIME === '1') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const defaultPath = expandPath(getDefaultDbPath());
|
|
70
|
+
if (expandedPath !== defaultPath) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const runtime = resolveRuntimeInfo();
|
|
74
|
+
if (runtime.kind === 'npx-cache' || runtime.kind === 'project-checkout') {
|
|
75
|
+
throw new Error(`[database] Refusing to open ${defaultPath} from ${runtime.kind === 'npx-cache' ? 'an npx cache' : 'a project checkout'}.\n` +
|
|
76
|
+
`Use the installed CLI (\`shieldcortex\`), pass \`--db\` to use a separate database, or set SHIELDCORTEX_ALLOW_UNSAFE_RUNTIME=1 if you really mean to do this.\n` +
|
|
77
|
+
`Runtime path: ${runtime.entryPath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function inspectDatabaseFile(dbPath) {
|
|
81
|
+
let inspectionDb = null;
|
|
82
|
+
try {
|
|
83
|
+
inspectionDb = new Database(dbPath, {
|
|
84
|
+
readonly: true,
|
|
85
|
+
fileMustExist: true,
|
|
86
|
+
});
|
|
87
|
+
const integrity = runIntegrityCheck(inspectionDb);
|
|
88
|
+
let count = null;
|
|
89
|
+
try {
|
|
90
|
+
count = inspectionDb.prepare('SELECT COUNT(*) AS count FROM memories').get().count;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
count = null;
|
|
94
|
+
}
|
|
95
|
+
return { integrity, count };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return { integrity: `inspect threw: ${error}`, count: null };
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
try {
|
|
102
|
+
inspectionDb?.close();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Best-effort close.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function listHealthyBackups(dbPath) {
|
|
110
|
+
const dir = dirname(dbPath);
|
|
111
|
+
const prefix = `${basename(dbPath)}.corrupt.`;
|
|
112
|
+
return readdirSync(dir)
|
|
113
|
+
.filter((name) => name.startsWith(prefix) && !name.endsWith('-wal') && !name.endsWith('-shm'))
|
|
114
|
+
.map((name) => join(dir, name))
|
|
115
|
+
.map((backupPath) => {
|
|
116
|
+
const inspection = inspectDatabaseFile(backupPath);
|
|
117
|
+
const stats = statSync(backupPath);
|
|
118
|
+
return {
|
|
119
|
+
path: backupPath,
|
|
120
|
+
integrity: inspection.integrity,
|
|
121
|
+
count: inspection.count,
|
|
122
|
+
mtimeMs: stats.mtimeMs,
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
.filter((candidate) => candidate.integrity === 'ok' && typeof candidate.count === 'number' && candidate.count > 0)
|
|
126
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
127
|
+
.map(({ path, count, mtimeMs }) => ({ path, count, mtimeMs }));
|
|
128
|
+
}
|
|
129
|
+
function stashLiveDatabase(dbPath, suffix) {
|
|
130
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
131
|
+
const stashPath = `${dbPath}.${suffix}.${timestamp}`;
|
|
132
|
+
if (existsSync(dbPath)) {
|
|
133
|
+
try {
|
|
134
|
+
renameSync(dbPath, stashPath);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
try {
|
|
138
|
+
copyFileSync(dbPath, stashPath);
|
|
139
|
+
unlinkSync(dbPath);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Last resort: leave the original in place.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const ext of ['-wal', '-shm']) {
|
|
147
|
+
const liveSidecar = dbPath + ext;
|
|
148
|
+
if (existsSync(liveSidecar)) {
|
|
149
|
+
try {
|
|
150
|
+
renameSync(liveSidecar, stashPath + ext);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
try {
|
|
154
|
+
unlinkSync(liveSidecar);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Best-effort cleanup.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function restoreBackupAsLive(dbPath, backupPath, reason) {
|
|
164
|
+
stashLiveDatabase(dbPath, reason);
|
|
165
|
+
copyFileSync(backupPath, dbPath);
|
|
166
|
+
}
|
|
167
|
+
function acquireStartupLock(dbPath) {
|
|
168
|
+
lockFilePath = `${dbPath}.lock`;
|
|
169
|
+
const runtime = resolveRuntimeInfo();
|
|
170
|
+
const payload = JSON.stringify({
|
|
171
|
+
pid: process.pid,
|
|
172
|
+
startedAt: new Date().toISOString(),
|
|
173
|
+
entryPath: runtime.entryPath,
|
|
174
|
+
}, null, 2);
|
|
175
|
+
const tryOpen = () => {
|
|
176
|
+
lockFileFd = openSync(lockFilePath, 'wx');
|
|
177
|
+
writeFileSync(lockFileFd, payload, 'utf-8');
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
tryOpen();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
let activeProcessError = null;
|
|
185
|
+
try {
|
|
186
|
+
const existing = JSON.parse(readFileSync(lockFilePath, 'utf-8'));
|
|
187
|
+
if (typeof existing.pid === 'number') {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(existing.pid, 0);
|
|
190
|
+
activeProcessError = new Error(`[database] Refusing startup because ShieldCortex PID ${existing.pid} is already using ${dbPath}` +
|
|
191
|
+
(existing.entryPath ? ` (${existing.entryPath})` : ''));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.code !== 'ESRCH') {
|
|
195
|
+
activeProcessError = error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Stale or unreadable lock file — remove and retry.
|
|
202
|
+
}
|
|
203
|
+
if (activeProcessError) {
|
|
204
|
+
throw activeProcessError;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
unlinkSync(lockFilePath);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Best-effort stale lock cleanup.
|
|
211
|
+
}
|
|
212
|
+
tryOpen();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
43
215
|
/**
|
|
44
216
|
* Back up a corrupt database file with a timestamped name.
|
|
45
217
|
* Returns the backup path.
|
|
@@ -164,30 +336,16 @@ function attemptFtsRecovery(database) {
|
|
|
164
336
|
}
|
|
165
337
|
}
|
|
166
338
|
function verifyOnDiskIntegrity(dbPath) {
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
verificationDb = new Database(dbPath, {
|
|
170
|
-
readonly: true,
|
|
171
|
-
fileMustExist: true,
|
|
172
|
-
});
|
|
173
|
-
return runIntegrityCheck(verificationDb);
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
return `fresh integrity check threw: ${error}`;
|
|
177
|
-
}
|
|
178
|
-
finally {
|
|
179
|
-
try {
|
|
180
|
-
verificationDb?.close();
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// Best-effort close for verification handles.
|
|
184
|
-
}
|
|
185
|
-
}
|
|
339
|
+
return inspectDatabaseFile(dbPath).integrity;
|
|
186
340
|
}
|
|
187
341
|
export const __databaseTestUtils = {
|
|
188
342
|
isLikelyFtsIntegrityIssue,
|
|
189
343
|
attemptFtsRecovery,
|
|
190
344
|
verifyOnDiskIntegrity,
|
|
345
|
+
inspectDatabaseFile,
|
|
346
|
+
listHealthyBackups,
|
|
347
|
+
resolveRuntimeInfo,
|
|
348
|
+
enforceSafeRuntimePath,
|
|
191
349
|
};
|
|
192
350
|
/**
|
|
193
351
|
* Initialize the database connection
|
|
@@ -195,10 +353,12 @@ export const __databaseTestUtils = {
|
|
|
195
353
|
export function initDatabase(dbPath) {
|
|
196
354
|
// Use auto-detected path if not specified
|
|
197
355
|
const resolvedPath = dbPath || getDefaultDbPath();
|
|
356
|
+
const explicitDbPath = Boolean(dbPath);
|
|
198
357
|
if (db) {
|
|
199
358
|
return db;
|
|
200
359
|
}
|
|
201
360
|
const expandedPath = expandPath(resolvedPath);
|
|
361
|
+
enforceSafeRuntimePath(expandedPath, explicitDbPath);
|
|
202
362
|
const dir = dirname(expandedPath);
|
|
203
363
|
// Create directory if it doesn't exist
|
|
204
364
|
if (!existsSync(dir)) {
|
|
@@ -206,6 +366,9 @@ export function initDatabase(dbPath) {
|
|
|
206
366
|
}
|
|
207
367
|
// Store path for size monitoring
|
|
208
368
|
currentDbPath = expandedPath;
|
|
369
|
+
acquireStartupLock(expandedPath);
|
|
370
|
+
console.log(`[database] Startup runtime=${resolveRuntimeInfo().kind} db=${expandedPath} wal=${existsSync(expandedPath + '-wal')} shm=${existsSync(expandedPath + '-shm')}`);
|
|
371
|
+
const healthyBackups = listHealthyBackups(expandedPath);
|
|
209
372
|
// Wrap the initial open in try/catch to handle corrupt files gracefully
|
|
210
373
|
let database;
|
|
211
374
|
try {
|
|
@@ -213,11 +376,19 @@ export function initDatabase(dbPath) {
|
|
|
213
376
|
}
|
|
214
377
|
catch (openError) {
|
|
215
378
|
// Database file is corrupt or not a valid SQLite database
|
|
216
|
-
console.error(
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
379
|
+
console.error(`❌ Database open failed for ${expandedPath}: ${openError}`);
|
|
380
|
+
const latestHealthyBackup = healthyBackups[0];
|
|
381
|
+
if (latestHealthyBackup) {
|
|
382
|
+
console.error(`[database] Restoring latest healthy backup with ${latestHealthyBackup.count} memories: ${latestHealthyBackup.path}`);
|
|
383
|
+
restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'failed-open');
|
|
384
|
+
database = new Database(expandedPath);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const backupPath = backupCorruptDatabase(expandedPath);
|
|
388
|
+
console.error(` Backed up to ${backupPath}`);
|
|
389
|
+
console.error(' Creating fresh database...');
|
|
390
|
+
database = new Database(expandedPath);
|
|
391
|
+
}
|
|
221
392
|
}
|
|
222
393
|
// Integrity check on existing databases (skip for newly created files)
|
|
223
394
|
if (existsSync(expandedPath) && statSync(expandedPath).size > 0) {
|
|
@@ -244,18 +415,40 @@ export function initDatabase(dbPath) {
|
|
|
244
415
|
database = recovered;
|
|
245
416
|
}
|
|
246
417
|
else {
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
418
|
+
const latestHealthyBackup = healthyBackups[0];
|
|
419
|
+
if (latestHealthyBackup) {
|
|
420
|
+
console.error(`[database] Recovery failed. Restoring latest healthy backup with ${latestHealthyBackup.count} memories: ${latestHealthyBackup.path}`);
|
|
421
|
+
restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'recovery-failed');
|
|
422
|
+
database = new Database(expandedPath);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Recovery failed — backup and create fresh
|
|
426
|
+
if (existsSync(expandedPath)) {
|
|
427
|
+
const backupPath = backupCorruptDatabase(expandedPath);
|
|
428
|
+
console.error(`[database] Recovery failed. Backed up corrupt file to: ${backupPath}`);
|
|
429
|
+
}
|
|
430
|
+
console.error('[database] Creating fresh database...');
|
|
431
|
+
database = new Database(expandedPath);
|
|
251
432
|
}
|
|
252
|
-
console.error('[database] Creating fresh database...');
|
|
253
|
-
database = new Database(expandedPath);
|
|
254
433
|
}
|
|
255
434
|
}
|
|
256
435
|
}
|
|
257
436
|
}
|
|
258
437
|
}
|
|
438
|
+
const currentInspection = inspectDatabaseFile(expandedPath);
|
|
439
|
+
const latestHealthyBackup = healthyBackups[0];
|
|
440
|
+
const liveStats = existsSync(expandedPath) ? statSync(expandedPath) : null;
|
|
441
|
+
if (currentInspection.integrity === 'ok'
|
|
442
|
+
&& currentInspection.count === 0
|
|
443
|
+
&& latestHealthyBackup
|
|
444
|
+
&& latestHealthyBackup.count >= 100
|
|
445
|
+
&& liveStats
|
|
446
|
+
&& (liveStats.mtimeMs - latestHealthyBackup.mtimeMs) < (24 * 60 * 60 * 1000)) {
|
|
447
|
+
console.error(`[database] Empty live database detected alongside a recent healthy backup (${latestHealthyBackup.count} memories). Restoring ${latestHealthyBackup.path}`);
|
|
448
|
+
database.close();
|
|
449
|
+
restoreBackupAsLive(expandedPath, latestHealthyBackup.path, 'empty-live');
|
|
450
|
+
database = new Database(expandedPath);
|
|
451
|
+
}
|
|
259
452
|
db = database;
|
|
260
453
|
// Enable WAL mode for better concurrency
|
|
261
454
|
db.pragma('journal_mode = WAL');
|
|
@@ -265,15 +458,6 @@ export function initDatabase(dbPath) {
|
|
|
265
458
|
db.pragma('busy_timeout = 10000');
|
|
266
459
|
// Auto-checkpoint every 100 pages (~400KB) to prevent WAL bloat
|
|
267
460
|
db.pragma('wal_autocheckpoint = 100');
|
|
268
|
-
// Create lock file to help detect concurrent instances
|
|
269
|
-
lockFilePath = expandedPath + '.lock';
|
|
270
|
-
const pid = process.pid;
|
|
271
|
-
try {
|
|
272
|
-
writeFileSync(lockFilePath, `${pid}\n${new Date().toISOString()}`);
|
|
273
|
-
}
|
|
274
|
-
catch {
|
|
275
|
-
// Non-fatal - lock file is advisory
|
|
276
|
-
}
|
|
277
461
|
// Register cleanup handlers for graceful shutdown
|
|
278
462
|
registerShutdownHandlers();
|
|
279
463
|
// Run migrations FIRST for existing databases
|
|
@@ -633,16 +817,24 @@ export function closeDatabase() {
|
|
|
633
817
|
db.close();
|
|
634
818
|
db = null;
|
|
635
819
|
currentDbPath = null;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
820
|
+
}
|
|
821
|
+
if (lockFileFd !== null) {
|
|
822
|
+
try {
|
|
823
|
+
closeSync(lockFileFd);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// Best-effort close.
|
|
827
|
+
}
|
|
828
|
+
lockFileFd = null;
|
|
829
|
+
}
|
|
830
|
+
if (lockFilePath && existsSync(lockFilePath)) {
|
|
831
|
+
try {
|
|
832
|
+
unlinkSync(lockFilePath);
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Non-fatal
|
|
645
836
|
}
|
|
837
|
+
lockFilePath = null;
|
|
646
838
|
}
|
|
647
839
|
}
|
|
648
840
|
/**
|
|
@@ -195,8 +195,8 @@ export function findDuplicateMemoryPairs(options) {
|
|
|
195
195
|
const db = getDatabase();
|
|
196
196
|
const limit = options?.limit ?? 20;
|
|
197
197
|
const rows = options?.project
|
|
198
|
-
? db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND project = ? ORDER BY created_at ASC").all(options.project)
|
|
199
|
-
: db.prepare("SELECT * FROM memories WHERE type = 'long_term' ORDER BY created_at ASC").all();
|
|
198
|
+
? db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND project = ? AND COALESCE(status, 'active') NOT IN ('archived', 'suppressed') ORDER BY created_at ASC").all(options.project)
|
|
199
|
+
: db.prepare("SELECT * FROM memories WHERE type = 'long_term' AND COALESCE(status, 'active') NOT IN ('archived', 'suppressed') ORDER BY created_at ASC").all();
|
|
200
200
|
const groups = new Map();
|
|
201
201
|
for (const mem of rows) {
|
|
202
202
|
const cat = mem.category || 'note';
|
|
@@ -204,7 +204,9 @@ export function detectContradictions(options = {}) {
|
|
|
204
204
|
SUBSTR(content, 1, 500) as content,
|
|
205
205
|
project, tags, salience, access_count, last_accessed,
|
|
206
206
|
created_at, decayed_score, metadata, scope, transferable
|
|
207
|
-
FROM memories
|
|
207
|
+
FROM memories
|
|
208
|
+
WHERE 1=1
|
|
209
|
+
AND COALESCE(status, 'active') NOT IN ('archived', 'suppressed')`;
|
|
208
210
|
const params = [];
|
|
209
211
|
if (project) {
|
|
210
212
|
sql += ' AND project = ?';
|
package/dist/memory/store.js
CHANGED
|
@@ -578,6 +578,14 @@ export function updateMemory(id, updates) {
|
|
|
578
578
|
fields.push('project = ?');
|
|
579
579
|
values.push(updates.project);
|
|
580
580
|
}
|
|
581
|
+
if (updates.scope !== undefined) {
|
|
582
|
+
fields.push('scope = ?');
|
|
583
|
+
values.push(updates.scope);
|
|
584
|
+
}
|
|
585
|
+
if (updates.transferable !== undefined) {
|
|
586
|
+
fields.push('transferable = ?');
|
|
587
|
+
values.push(updates.transferable ? 1 : 0);
|
|
588
|
+
}
|
|
581
589
|
if (updates.tags !== undefined) {
|
|
582
590
|
fields.push('tags = ?');
|
|
583
591
|
values.push(JSON.stringify(updates.tags));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shieldcortex",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.15",
|
|
4
4
|
"description": "Trustworthy memory and security for AI agents. Recall debugging, review queue, OpenClaw session capture, and memory poisoning defence for Claude Code, Codex, OpenClaw, LangChain, and MCP agents.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|