@stackmemoryai/stackmemory 1.2.2 → 1.2.6
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/README.md +10 -6
- package/dist/src/cli/claude-sm.js +33 -4
- package/dist/src/cli/codex-sm-danger.js +4 -1
- package/dist/src/cli/codex-sm.js +6 -8
- package/dist/src/cli/commands/config.js +0 -81
- package/dist/src/cli/commands/context-rehydrate.js +133 -47
- package/dist/src/cli/commands/db.js +35 -8
- package/dist/src/cli/commands/handoff.js +1 -1
- package/dist/src/cli/commands/linear.js +9 -0
- package/dist/src/cli/commands/ralph.js +2 -2
- package/dist/src/cli/commands/setup.js +2 -2
- package/dist/src/cli/commands/signup.js +3 -1
- package/dist/src/cli/commands/skills.js +108 -1
- package/dist/src/cli/commands/storage-tier.js +26 -8
- package/dist/src/cli/index.js +1 -57
- package/dist/src/core/config/feature-flags.js +0 -4
- package/dist/src/core/context/dual-stack-manager.js +10 -3
- package/dist/src/core/context/frame-database.js +32 -0
- package/dist/src/core/context/frame-handoff-manager.js +2 -2
- package/dist/src/core/context/{refactored-frame-manager.js → frame-manager.js} +3 -3
- package/dist/src/core/context/index.js +2 -2
- package/dist/src/core/database/sqlite-adapter.js +161 -1
- package/dist/src/core/digest/frame-digest-integration.js +1 -1
- package/dist/src/core/digest/index.js +1 -1
- package/dist/src/core/execution/parallel-executor.js +5 -1
- package/dist/src/core/projects/project-isolation.js +18 -4
- package/dist/src/core/security/index.js +2 -0
- package/dist/src/core/security/input-sanitizer.js +23 -0
- package/dist/src/core/utils/update-checker.js +10 -6
- package/dist/src/daemon/daemon-config.js +2 -1
- package/dist/src/daemon/services/auto-save-service.js +121 -0
- package/dist/src/daemon/services/maintenance-service.js +76 -1
- package/dist/src/features/sweep/prompt-builder.js +2 -2
- package/dist/src/hooks/daemon.js +0 -8
- package/dist/src/integrations/linear/config.js +3 -1
- package/dist/src/integrations/linear/sync.js +18 -5
- package/dist/src/integrations/linear/webhook.js +0 -16
- package/dist/src/integrations/mcp/handlers/code-execution-handlers.js +33 -7
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +397 -0
- package/dist/src/integrations/mcp/handlers/index.js +55 -9
- package/dist/src/integrations/mcp/handlers/task-handlers.js +55 -12
- package/dist/src/integrations/mcp/handlers/team-handlers.js +211 -0
- package/dist/src/integrations/mcp/handlers/trace-handlers.js +28 -9
- package/dist/src/integrations/mcp/index.js +2 -2
- package/dist/src/integrations/mcp/refactored-server.js +31 -10
- package/dist/src/integrations/mcp/server.js +0 -130
- package/dist/src/integrations/mcp/tool-definitions.js +196 -67
- package/dist/src/integrations/ralph/context/context-budget-manager.js +10 -2
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +54 -22
- package/dist/src/integrations/ralph/learning/pattern-learner.js +59 -24
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +81 -35
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +12 -4
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +32 -9
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +25 -8
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +17 -5
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +73 -22
- package/dist/src/skills/claude-skills.js +37 -103
- package/dist/src/skills/theory-skill.js +191 -0
- package/dist/src/utils/hook-installer.js +16 -0
- package/package.json +6 -6
- package/scripts/install-claude-hooks-auto.js +8 -0
- package/templates/claude-hooks/cord-trace.js +225 -0
- package/templates/claude-hooks/theory-capture.js +100 -0
- package/dist/src/core/config/storage-config.js +0 -114
- package/dist/src/core/storage/chromadb-adapter.js +0 -379
- package/dist/src/hooks/graphiti-hooks.js +0 -253
- package/dist/src/integrations/claude-code/enhanced-pre-clear-hooks.js +0 -458
- package/dist/src/integrations/graphiti/client.js +0 -115
- package/dist/src/integrations/graphiti/config.js +0 -17
- package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +0 -115
- package/dist/src/integrations/graphiti/types.js +0 -4
- package/dist/src/integrations/ralph/coordination/enhanced-coordination.js +0 -409
- package/dist/src/skills/repo-ingestion-skill.js +0 -631
- package/templates/claude-hooks/chromadb-wrapper +0 -21
- /package/dist/src/core/context/{enhanced-rehydration.js → rehydration.js} +0 -0
- /package/dist/src/core/digest/{enhanced-hybrid-digest.js → hybrid-digest.js} +0 -0
- /package/dist/src/core/session/{enhanced-handoff.js → handoff.js} +0 -0
|
@@ -237,6 +237,10 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
237
237
|
"FTS5 index populated from existing frames (migration v1\u2192v2)"
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
|
+
if (version < 3) {
|
|
241
|
+
this.db.prepare("INSERT OR REPLACE INTO schema_version (version) VALUES (?)").run(3);
|
|
242
|
+
logger.info("Schema migration v2\u2192v3: GC importance_score support");
|
|
243
|
+
}
|
|
240
244
|
}
|
|
241
245
|
/**
|
|
242
246
|
* Initialize sqlite-vec for vector search
|
|
@@ -305,6 +309,7 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
305
309
|
const retentionDays = options.retentionDays ?? 90;
|
|
306
310
|
const batchSize = options.batchSize ?? 100;
|
|
307
311
|
const dryRun = options.dryRun ?? false;
|
|
312
|
+
const protectedRunIds = options.protectedRunIds ?? [];
|
|
308
313
|
const nowSec = Math.floor(Date.now() / 1e3);
|
|
309
314
|
const defaultCutoff = nowSec - retentionDays * 86400;
|
|
310
315
|
const ttl30dCutoff = nowSec - 30 * 86400;
|
|
@@ -317,8 +322,17 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
317
322
|
OR (retention_policy = 'ttl_7d' AND created_at < ?)
|
|
318
323
|
)
|
|
319
324
|
AND retention_policy != 'keep_forever'
|
|
325
|
+
AND state = 'closed'
|
|
326
|
+
AND run_id NOT IN (SELECT value FROM json_each(?))
|
|
327
|
+
ORDER BY importance_score ASC, created_at ASC
|
|
320
328
|
LIMIT ?`
|
|
321
|
-
).all(
|
|
329
|
+
).all(
|
|
330
|
+
defaultCutoff,
|
|
331
|
+
ttl30dCutoff,
|
|
332
|
+
ttl7dCutoff,
|
|
333
|
+
JSON.stringify(protectedRunIds),
|
|
334
|
+
batchSize
|
|
335
|
+
);
|
|
322
336
|
const frameIds = candidates.map((r) => r.frame_id);
|
|
323
337
|
if (frameIds.length === 0) {
|
|
324
338
|
return {
|
|
@@ -388,6 +402,69 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
388
402
|
ftsEntriesDeleted: frameIds.length
|
|
389
403
|
};
|
|
390
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Compute importance score for a single frame.
|
|
407
|
+
* Score range: [0.0, 1.0] — higher means more important, less likely to be GC'd.
|
|
408
|
+
*/
|
|
409
|
+
computeImportanceScore(frameId) {
|
|
410
|
+
if (!this.db)
|
|
411
|
+
throw new DatabaseError(
|
|
412
|
+
"Database not connected",
|
|
413
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
414
|
+
);
|
|
415
|
+
const frame = this.db.prepare(
|
|
416
|
+
"SELECT frame_id, digest_text, created_at FROM frames WHERE frame_id = ?"
|
|
417
|
+
).get(frameId);
|
|
418
|
+
if (!frame) return 0.3;
|
|
419
|
+
let score = 0.3;
|
|
420
|
+
const decisionCount = this.db.prepare(
|
|
421
|
+
"SELECT COUNT(*) as count FROM anchors WHERE frame_id = ? AND type = 'DECISION'"
|
|
422
|
+
).get(frameId).count;
|
|
423
|
+
if (decisionCount > 0) score += 0.15;
|
|
424
|
+
const eventCount = this.db.prepare("SELECT COUNT(*) as count FROM events WHERE frame_id = ?").get(frameId).count;
|
|
425
|
+
if (eventCount > 3) score += 0.1;
|
|
426
|
+
if (frame.digest_text) score += 0.15;
|
|
427
|
+
const childCount = this.db.prepare(
|
|
428
|
+
"SELECT COUNT(*) as count FROM frames WHERE parent_frame_id = ?"
|
|
429
|
+
).get(frameId).count;
|
|
430
|
+
if (childCount > 0) score += 0.1;
|
|
431
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
432
|
+
if (nowSec - frame.created_at < 86400) score += 0.1;
|
|
433
|
+
return Math.round(Math.min(score, 1) * 100) / 100;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Recompute importance scores for frames still at default score (0.5).
|
|
437
|
+
* Processes oldest frames first in batches.
|
|
438
|
+
* Returns count of frames updated.
|
|
439
|
+
*/
|
|
440
|
+
recomputeImportanceScores(batchSize = 100) {
|
|
441
|
+
if (!this.db)
|
|
442
|
+
throw new DatabaseError(
|
|
443
|
+
"Database not connected",
|
|
444
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
445
|
+
);
|
|
446
|
+
const frames = this.db.prepare(
|
|
447
|
+
"SELECT frame_id FROM frames WHERE importance_score = 0.5 ORDER BY created_at ASC LIMIT ?"
|
|
448
|
+
).all(batchSize);
|
|
449
|
+
const updateStmt = this.db.prepare(
|
|
450
|
+
"UPDATE frames SET importance_score = ? WHERE frame_id = ?"
|
|
451
|
+
);
|
|
452
|
+
let updated = 0;
|
|
453
|
+
for (const { frame_id } of frames) {
|
|
454
|
+
const score = this.computeImportanceScore(frame_id);
|
|
455
|
+
if (score !== 0.5) {
|
|
456
|
+
updateStmt.run(score, frame_id);
|
|
457
|
+
updated++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (updated > 0) {
|
|
461
|
+
logger.info("Recomputed importance scores", {
|
|
462
|
+
checked: frames.length,
|
|
463
|
+
updated
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return updated;
|
|
467
|
+
}
|
|
391
468
|
async migrateSchema(targetVersion) {
|
|
392
469
|
if (!this.db)
|
|
393
470
|
throw new DatabaseError(
|
|
@@ -1252,6 +1329,89 @@ class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
|
|
|
1252
1329
|
}
|
|
1253
1330
|
return Buffer.from(JSON.stringify(data, null, 2));
|
|
1254
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Get recent frames from other run_ids within the same project.
|
|
1334
|
+
* Used by team tools to read cross-agent context.
|
|
1335
|
+
*/
|
|
1336
|
+
async getRecentFramesExcludingRun(projectId, excludeRunId, opts) {
|
|
1337
|
+
if (!this.db)
|
|
1338
|
+
throw new DatabaseError(
|
|
1339
|
+
"Database not connected",
|
|
1340
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
1341
|
+
);
|
|
1342
|
+
const limit = opts?.limit ?? 10;
|
|
1343
|
+
const params = [projectId, excludeRunId];
|
|
1344
|
+
let whereExtra = "";
|
|
1345
|
+
if (opts?.since) {
|
|
1346
|
+
whereExtra += " AND created_at > ?";
|
|
1347
|
+
params.push(Math.floor(opts.since / 1e3));
|
|
1348
|
+
}
|
|
1349
|
+
if (opts?.types && opts.types.length > 0) {
|
|
1350
|
+
const placeholders = opts.types.map(() => "?").join(",");
|
|
1351
|
+
whereExtra += ` AND type IN (${placeholders})`;
|
|
1352
|
+
params.push(...opts.types);
|
|
1353
|
+
}
|
|
1354
|
+
params.push(limit);
|
|
1355
|
+
const rows = this.db.prepare(
|
|
1356
|
+
`SELECT * FROM frames
|
|
1357
|
+
WHERE project_id = ? AND run_id != ?${whereExtra}
|
|
1358
|
+
ORDER BY created_at DESC
|
|
1359
|
+
LIMIT ?`
|
|
1360
|
+
).all(...params);
|
|
1361
|
+
const frameIds = rows.map((r) => r.frame_id);
|
|
1362
|
+
const anchorsByFrame = /* @__PURE__ */ new Map();
|
|
1363
|
+
if (frameIds.length > 0) {
|
|
1364
|
+
const placeholders = frameIds.map(() => "?").join(",");
|
|
1365
|
+
const anchorRows = this.db.prepare(
|
|
1366
|
+
`SELECT * FROM anchors WHERE frame_id IN (${placeholders})
|
|
1367
|
+
ORDER BY priority DESC, created_at ASC`
|
|
1368
|
+
).all(...frameIds);
|
|
1369
|
+
for (const row of anchorRows) {
|
|
1370
|
+
const list = anchorsByFrame.get(row.frame_id) || [];
|
|
1371
|
+
list.push({ ...row, metadata: JSON.parse(row.metadata || "{}") });
|
|
1372
|
+
anchorsByFrame.set(row.frame_id, list);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return rows.map((row) => ({
|
|
1376
|
+
...row,
|
|
1377
|
+
inputs: JSON.parse(row.inputs || "{}"),
|
|
1378
|
+
outputs: JSON.parse(row.outputs || "{}"),
|
|
1379
|
+
digest_json: JSON.parse(row.digest_json || "{}"),
|
|
1380
|
+
anchors: anchorsByFrame.get(row.frame_id) || []
|
|
1381
|
+
}));
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Get anchors explicitly shared for team visibility.
|
|
1385
|
+
* Finds anchors where metadata contains `"shared":true`.
|
|
1386
|
+
*/
|
|
1387
|
+
async getSharedAnchors(projectId, opts) {
|
|
1388
|
+
if (!this.db)
|
|
1389
|
+
throw new DatabaseError(
|
|
1390
|
+
"Database not connected",
|
|
1391
|
+
ErrorCode.DB_CONNECTION_FAILED
|
|
1392
|
+
);
|
|
1393
|
+
const limit = opts?.limit ?? 20;
|
|
1394
|
+
const params = [projectId];
|
|
1395
|
+
let whereExtra = "";
|
|
1396
|
+
if (opts?.since) {
|
|
1397
|
+
whereExtra += " AND a.created_at > ?";
|
|
1398
|
+
params.push(Math.floor(opts.since / 1e3));
|
|
1399
|
+
}
|
|
1400
|
+
params.push(limit);
|
|
1401
|
+
const rows = this.db.prepare(
|
|
1402
|
+
`SELECT a.*, f.name as frame_name, f.run_id
|
|
1403
|
+
FROM anchors a
|
|
1404
|
+
JOIN frames f ON a.frame_id = f.frame_id
|
|
1405
|
+
WHERE f.project_id = ?
|
|
1406
|
+
AND a.metadata LIKE '%"shared":true%'${whereExtra}
|
|
1407
|
+
ORDER BY a.priority DESC, a.created_at DESC
|
|
1408
|
+
LIMIT ?`
|
|
1409
|
+
).all(...params);
|
|
1410
|
+
return rows.map((row) => ({
|
|
1411
|
+
...row,
|
|
1412
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
1413
|
+
}));
|
|
1414
|
+
}
|
|
1255
1415
|
async importData(data, format, options) {
|
|
1256
1416
|
if (!this.db)
|
|
1257
1417
|
throw new DatabaseError(
|
|
@@ -2,7 +2,7 @@ import { fileURLToPath as __fileURLToPath } from 'url';
|
|
|
2
2
|
import { dirname as __pathDirname } from 'path';
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
|
-
import { EnhancedHybridDigestGenerator } from "./
|
|
5
|
+
import { EnhancedHybridDigestGenerator } from "./hybrid-digest.js";
|
|
6
6
|
import { logger } from "../monitoring/logger.js";
|
|
7
7
|
class FrameDigestIntegration {
|
|
8
8
|
frameManager;
|
|
@@ -4,5 +4,5 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
export * from "./types.js";
|
|
6
6
|
export * from "./hybrid-digest-generator.js";
|
|
7
|
-
export * from "./
|
|
7
|
+
export * from "./hybrid-digest.js";
|
|
8
8
|
export * from "./frame-digest-integration.js";
|
|
@@ -106,7 +106,11 @@ class ParallelExecutor extends EventEmitter {
|
|
|
106
106
|
taskId,
|
|
107
107
|
error: lastError.message
|
|
108
108
|
});
|
|
109
|
-
this.emit("task-retry", {
|
|
109
|
+
this.emit("task-retry", {
|
|
110
|
+
taskId,
|
|
111
|
+
attempt: attempts,
|
|
112
|
+
error: lastError
|
|
113
|
+
});
|
|
110
114
|
if (attempts < maxRetries) {
|
|
111
115
|
await this.delay(Math.pow(2, attempts) * 1e3);
|
|
112
116
|
}
|
|
@@ -25,9 +25,20 @@ class ProjectIsolationManager {
|
|
|
25
25
|
try {
|
|
26
26
|
const remoteUrl = this.getGitRemoteUrl(projectRoot);
|
|
27
27
|
const gitInfo = this.parseGitRemote(remoteUrl);
|
|
28
|
-
const projectId = this.createStableProjectId(
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const projectId = this.createStableProjectId(
|
|
29
|
+
gitInfo.organization,
|
|
30
|
+
gitInfo.repository
|
|
31
|
+
);
|
|
32
|
+
const workspaceFilter = this.createWorkspaceFilter(
|
|
33
|
+
gitInfo.organization,
|
|
34
|
+
gitInfo.repository,
|
|
35
|
+
projectRoot
|
|
36
|
+
);
|
|
37
|
+
const linearConfig = this.getLinearConfiguration(
|
|
38
|
+
gitInfo.organization,
|
|
39
|
+
gitInfo.repository,
|
|
40
|
+
projectRoot
|
|
41
|
+
);
|
|
31
42
|
const identification = {
|
|
32
43
|
projectId,
|
|
33
44
|
organization: gitInfo.organization,
|
|
@@ -45,7 +56,10 @@ class ProjectIsolationManager {
|
|
|
45
56
|
});
|
|
46
57
|
return identification;
|
|
47
58
|
} catch (error) {
|
|
48
|
-
logger.warn(
|
|
59
|
+
logger.warn(
|
|
60
|
+
"Could not determine git remote, using fallback identification",
|
|
61
|
+
{ error }
|
|
62
|
+
);
|
|
49
63
|
const fallback = this.createFallbackIdentification(projectRoot);
|
|
50
64
|
this.projectCache.set(cacheKey, fallback);
|
|
51
65
|
return fallback;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
validateInput,
|
|
16
16
|
createAggregateSchema,
|
|
17
17
|
validateShellArg,
|
|
18
|
+
validateShellCommand,
|
|
18
19
|
safeJsonParse
|
|
19
20
|
} from "./input-sanitizer.js";
|
|
20
21
|
export {
|
|
@@ -30,5 +31,6 @@ export {
|
|
|
30
31
|
sanitizeIdentifier,
|
|
31
32
|
validateInput,
|
|
32
33
|
validateShellArg,
|
|
34
|
+
validateShellCommand,
|
|
33
35
|
validateTableName
|
|
34
36
|
};
|
|
@@ -280,6 +280,28 @@ function createAggregateSchema(allowedFields) {
|
|
|
280
280
|
limit: z.number().int().min(1).max(1e3).optional()
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
|
+
const DANGEROUS_SHELL_PATTERNS = [
|
|
284
|
+
/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive)\b/i,
|
|
285
|
+
// rm -rf, rm -fr, rm --recursive
|
|
286
|
+
/\brm\s+-[a-zA-Z]*r\b/i,
|
|
287
|
+
// rm -r (any flag combo with r)
|
|
288
|
+
/\bmkfs\b/i,
|
|
289
|
+
/\bdd\s+if=/i,
|
|
290
|
+
/\b:\(\)\s*\{/i
|
|
291
|
+
// fork bomb
|
|
292
|
+
];
|
|
293
|
+
function validateShellCommand(command) {
|
|
294
|
+
for (const pattern of DANGEROUS_SHELL_PATTERNS) {
|
|
295
|
+
if (pattern.test(command)) {
|
|
296
|
+
throw new ValidationError(
|
|
297
|
+
`Blocked dangerous shell command: ${command.substring(0, 80)}`,
|
|
298
|
+
ErrorCode.VALIDATION_FAILED,
|
|
299
|
+
{ reason: "dangerous_command" }
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return command;
|
|
304
|
+
}
|
|
283
305
|
function validateShellArg(arg) {
|
|
284
306
|
if (!arg) return "";
|
|
285
307
|
const dangerousChars = /[;&|`$(){}[\]<>!#*?~\n\r]/;
|
|
@@ -316,5 +338,6 @@ export {
|
|
|
316
338
|
sanitizeIdentifier,
|
|
317
339
|
validateInput,
|
|
318
340
|
validateShellArg,
|
|
341
|
+
validateShellCommand,
|
|
319
342
|
validateTableName
|
|
320
343
|
};
|
|
@@ -63,12 +63,16 @@ class UpdateChecker {
|
|
|
63
63
|
static async fetchLatestVersion() {
|
|
64
64
|
try {
|
|
65
65
|
const fetchVersion = async () => {
|
|
66
|
-
const output = execFileSync(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
const output = execFileSync(
|
|
67
|
+
"npm",
|
|
68
|
+
["view", this.PACKAGE_NAME, "version"],
|
|
69
|
+
{
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
72
|
+
timeout: 5e3
|
|
73
|
+
// 5 second timeout
|
|
74
|
+
}
|
|
75
|
+
).trim();
|
|
72
76
|
return output;
|
|
73
77
|
};
|
|
74
78
|
return await gracefulDegrade(
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
class DaemonContextService {
|
|
10
|
+
config;
|
|
11
|
+
state;
|
|
12
|
+
intervalId;
|
|
13
|
+
isRunning = false;
|
|
14
|
+
onLog;
|
|
15
|
+
constructor(config, onLog) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.onLog = onLog;
|
|
18
|
+
this.state = {
|
|
19
|
+
lastSaveTime: 0,
|
|
20
|
+
saveCount: 0,
|
|
21
|
+
errors: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
start() {
|
|
25
|
+
if (this.isRunning || !this.config.enabled) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.isRunning = true;
|
|
29
|
+
const intervalMs = this.config.interval * 60 * 1e3;
|
|
30
|
+
this.onLog("INFO", "Context service started", {
|
|
31
|
+
interval: this.config.interval
|
|
32
|
+
});
|
|
33
|
+
this.saveContext();
|
|
34
|
+
this.intervalId = setInterval(() => {
|
|
35
|
+
this.saveContext();
|
|
36
|
+
}, intervalMs);
|
|
37
|
+
}
|
|
38
|
+
stop() {
|
|
39
|
+
if (this.intervalId) {
|
|
40
|
+
clearInterval(this.intervalId);
|
|
41
|
+
this.intervalId = void 0;
|
|
42
|
+
}
|
|
43
|
+
this.isRunning = false;
|
|
44
|
+
this.onLog("INFO", "Context service stopped");
|
|
45
|
+
}
|
|
46
|
+
getState() {
|
|
47
|
+
return { ...this.state };
|
|
48
|
+
}
|
|
49
|
+
updateConfig(config) {
|
|
50
|
+
const wasRunning = this.isRunning;
|
|
51
|
+
if (wasRunning) {
|
|
52
|
+
this.stop();
|
|
53
|
+
}
|
|
54
|
+
this.config = { ...this.config, ...config };
|
|
55
|
+
if (wasRunning && this.config.enabled) {
|
|
56
|
+
this.start();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
forceSave() {
|
|
60
|
+
this.saveContext();
|
|
61
|
+
}
|
|
62
|
+
saveContext() {
|
|
63
|
+
if (!this.isRunning) return;
|
|
64
|
+
try {
|
|
65
|
+
const stackmemoryBin = this.getStackMemoryBin();
|
|
66
|
+
if (!stackmemoryBin) {
|
|
67
|
+
this.onLog("WARN", "StackMemory binary not found");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const message = this.config.checkpointMessage || `Auto-checkpoint #${this.state.saveCount + 1}`;
|
|
71
|
+
const fullMessage = `${message} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
72
|
+
execSync(`"${stackmemoryBin}" context add observation "${fullMessage}"`, {
|
|
73
|
+
timeout: 3e4,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: "pipe"
|
|
76
|
+
});
|
|
77
|
+
this.state.saveCount++;
|
|
78
|
+
this.state.lastSaveTime = Date.now();
|
|
79
|
+
this.onLog("INFO", "Context saved", {
|
|
80
|
+
saveCount: this.state.saveCount
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
if (!errorMsg.includes("EBUSY") && !errorMsg.includes("EAGAIN")) {
|
|
85
|
+
this.state.errors.push(errorMsg);
|
|
86
|
+
this.onLog("WARN", "Failed to save context", { error: errorMsg });
|
|
87
|
+
if (this.state.errors.length > 10) {
|
|
88
|
+
this.state.errors = this.state.errors.slice(-10);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getStackMemoryBin() {
|
|
94
|
+
const homeDir = homedir();
|
|
95
|
+
const locations = [
|
|
96
|
+
join(homeDir, ".stackmemory", "bin", "stackmemory"),
|
|
97
|
+
join(homeDir, ".local", "bin", "stackmemory"),
|
|
98
|
+
"/usr/local/bin/stackmemory",
|
|
99
|
+
"/opt/homebrew/bin/stackmemory"
|
|
100
|
+
];
|
|
101
|
+
for (const loc of locations) {
|
|
102
|
+
if (existsSync(loc)) {
|
|
103
|
+
return loc;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = execSync("which stackmemory", {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
stdio: "pipe"
|
|
110
|
+
}).trim();
|
|
111
|
+
if (result && existsSync(result)) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
DaemonContextService
|
|
121
|
+
};
|
|
@@ -10,6 +10,7 @@ class DaemonMaintenanceService {
|
|
|
10
10
|
state;
|
|
11
11
|
embeddingProvider = null;
|
|
12
12
|
intervalId;
|
|
13
|
+
gcIntervalId;
|
|
13
14
|
isRunning = false;
|
|
14
15
|
onLog;
|
|
15
16
|
constructor(config, onLog) {
|
|
@@ -48,12 +49,26 @@ class DaemonMaintenanceService {
|
|
|
48
49
|
);
|
|
49
50
|
});
|
|
50
51
|
}, intervalMs);
|
|
52
|
+
if (this.config.gcEnabled !== false) {
|
|
53
|
+
const gcMs = (this.config.gcIntervalSeconds ?? 60) * 1e3;
|
|
54
|
+
this.gcIntervalId = setInterval(() => {
|
|
55
|
+
this.runGCCycle().catch((err) => {
|
|
56
|
+
this.addError(
|
|
57
|
+
`GC cycle: ${err instanceof Error ? err.message : String(err)}`
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}, gcMs);
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
63
|
stop() {
|
|
53
64
|
if (this.intervalId) {
|
|
54
65
|
clearInterval(this.intervalId);
|
|
55
66
|
this.intervalId = void 0;
|
|
56
67
|
}
|
|
68
|
+
if (this.gcIntervalId) {
|
|
69
|
+
clearInterval(this.gcIntervalId);
|
|
70
|
+
this.gcIntervalId = void 0;
|
|
71
|
+
}
|
|
57
72
|
this.isRunning = false;
|
|
58
73
|
this.onLog("INFO", "Maintenance service stopped");
|
|
59
74
|
}
|
|
@@ -93,6 +108,7 @@ class DaemonMaintenanceService {
|
|
|
93
108
|
await this.backfillEmbeddings(db);
|
|
94
109
|
await this.maybeVacuum(db);
|
|
95
110
|
await this.generateMissingDigests(db);
|
|
111
|
+
await this.recomputeScores(db);
|
|
96
112
|
await this.runGC(db);
|
|
97
113
|
await db.disconnect();
|
|
98
114
|
this.state.lastRunTime = Date.now();
|
|
@@ -280,14 +296,73 @@ class DaemonMaintenanceService {
|
|
|
280
296
|
);
|
|
281
297
|
}
|
|
282
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Dedicated lightweight GC cycle (runs more frequently than full maintenance).
|
|
301
|
+
* Queries active run_ids for protection and delegates to adapter.runGC().
|
|
302
|
+
*/
|
|
303
|
+
async runGCCycle() {
|
|
304
|
+
try {
|
|
305
|
+
const db = await this.getDatabase();
|
|
306
|
+
if (!db) return;
|
|
307
|
+
const rawDb = db.getRawDatabase?.();
|
|
308
|
+
let protectedRunIds = [];
|
|
309
|
+
if (rawDb) {
|
|
310
|
+
const rows = rawDb.prepare(
|
|
311
|
+
"SELECT DISTINCT run_id FROM frames WHERE state = 'active' LIMIT 10"
|
|
312
|
+
).all();
|
|
313
|
+
protectedRunIds = rows.map((r) => r.run_id);
|
|
314
|
+
}
|
|
315
|
+
const result = await db.runGC({
|
|
316
|
+
retentionDays: this.config.gcRetentionDays ?? 90,
|
|
317
|
+
batchSize: this.config.gcBatchSize ?? 100,
|
|
318
|
+
dryRun: false,
|
|
319
|
+
protectedRunIds
|
|
320
|
+
});
|
|
321
|
+
this.state.framesGarbageCollected += result.framesDeleted;
|
|
322
|
+
this.state.lastGcRun = Date.now();
|
|
323
|
+
if (result.framesDeleted > 0) {
|
|
324
|
+
this.onLog(
|
|
325
|
+
"INFO",
|
|
326
|
+
`GC cycle deleted ${result.framesDeleted} expired frames`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
await db.disconnect();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
this.addError(
|
|
332
|
+
`GC cycle: ${err instanceof Error ? err.message : String(err)}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async recomputeScores(db) {
|
|
337
|
+
try {
|
|
338
|
+
if (typeof db.recomputeImportanceScores !== "function") return;
|
|
339
|
+
const updated = db.recomputeImportanceScores(100);
|
|
340
|
+
if (updated > 0) {
|
|
341
|
+
this.onLog("INFO", `Recomputed ${updated} importance scores`);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
this.addError(
|
|
345
|
+
`Score recompute: ${err instanceof Error ? err.message : String(err)}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
283
349
|
async runGC(db) {
|
|
284
350
|
try {
|
|
285
351
|
if (this.config.gcEnabled === false) return;
|
|
286
352
|
if (typeof db.runGC !== "function") return;
|
|
353
|
+
const rawDb = db.getRawDatabase?.();
|
|
354
|
+
let protectedRunIds = [];
|
|
355
|
+
if (rawDb) {
|
|
356
|
+
const rows = rawDb.prepare(
|
|
357
|
+
"SELECT DISTINCT run_id FROM frames WHERE state = 'active' LIMIT 10"
|
|
358
|
+
).all();
|
|
359
|
+
protectedRunIds = rows.map((r) => r.run_id);
|
|
360
|
+
}
|
|
287
361
|
const result = await db.runGC({
|
|
288
362
|
retentionDays: this.config.gcRetentionDays ?? 90,
|
|
289
363
|
batchSize: this.config.gcBatchSize ?? 100,
|
|
290
|
-
dryRun: false
|
|
364
|
+
dryRun: false,
|
|
365
|
+
protectedRunIds
|
|
291
366
|
});
|
|
292
367
|
this.state.framesGarbageCollected += result.framesDeleted;
|
|
293
368
|
this.state.lastGcRun = Date.now();
|
|
@@ -49,7 +49,7 @@ function trimContentAroundCursor(lines, cursorLine, cursorCol, maxTokens) {
|
|
|
49
49
|
const windowSize = Math.floor(targetChars / avgLineLength);
|
|
50
50
|
const halfWindow = Math.floor(windowSize / 2);
|
|
51
51
|
let start = Math.max(0, cursorLine - halfWindow);
|
|
52
|
-
|
|
52
|
+
const end = Math.min(lines.length, start + windowSize);
|
|
53
53
|
if (end === lines.length) {
|
|
54
54
|
start = Math.max(0, end - windowSize);
|
|
55
55
|
}
|
|
@@ -60,7 +60,7 @@ function trimContentAroundCursor(lines, cursorLine, cursorCol, maxTokens) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
function parseCompletion(completionText, originalLines, windowStart, windowEnd) {
|
|
63
|
-
|
|
63
|
+
const text = completionText.replace(/<\|file_sep\|>$/, "").replace(/<\/s>$/, "").trimEnd();
|
|
64
64
|
if (!text || text.trim().length === 0) {
|
|
65
65
|
return null;
|
|
66
66
|
}
|
package/dist/src/hooks/daemon.js
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { join, extname, relative } from "path";
|
|
14
14
|
import { spawn } from "child_process";
|
|
15
15
|
import { loadConfig } from "./config.js";
|
|
16
|
-
import { GraphitiHooks } from "./graphiti-hooks.js";
|
|
17
16
|
import {
|
|
18
17
|
hookEmitter
|
|
19
18
|
} from "./events.js";
|
|
@@ -143,13 +142,6 @@ function registerBuiltinHandlers() {
|
|
|
143
142
|
hookEmitter.registerHandler("file_change", handleFileChange);
|
|
144
143
|
hookEmitter.registerHandler("suggestion_ready", handleSuggestionReady);
|
|
145
144
|
hookEmitter.registerHandler("error", handleError);
|
|
146
|
-
try {
|
|
147
|
-
if (process.env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENABLED === "true") {
|
|
148
|
-
const graphiti = new GraphitiHooks();
|
|
149
|
-
graphiti.register(hookEmitter);
|
|
150
|
-
}
|
|
151
|
-
} catch {
|
|
152
|
-
}
|
|
153
145
|
hookEmitter.on("*", () => {
|
|
154
146
|
state.eventsProcessed++;
|
|
155
147
|
});
|
|
@@ -62,7 +62,9 @@ class LinearConfigManager {
|
|
|
62
62
|
* Get default configuration with project isolation
|
|
63
63
|
*/
|
|
64
64
|
getDefaultConfig() {
|
|
65
|
-
const projectId = this.isolationManager.getProjectIdentification(
|
|
65
|
+
const projectId = this.isolationManager.getProjectIdentification(
|
|
66
|
+
this.projectRoot
|
|
67
|
+
);
|
|
66
68
|
return {
|
|
67
69
|
enabled: true,
|
|
68
70
|
interval: 5,
|