@stackmemoryai/stackmemory 1.2.1 → 1.2.4
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/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 +123 -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/graphiti-hooks.js +149 -0
- package/dist/src/hooks/session-summary.js +30 -0
- package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
- package/dist/src/integrations/greptile/client.js +101 -0
- package/dist/src/integrations/greptile/config.js +14 -0
- package/dist/src/integrations/greptile/index.js +11 -0
- package/dist/src/integrations/greptile/types.js +4 -0
- 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 +16 -0
- 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/greptile-handlers.js +456 -0
- package/dist/src/integrations/mcp/handlers/index.js +55 -1
- 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 +25 -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 +27 -0
- package/dist/src/integrations/mcp/tool-definitions.js +215 -1
- 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 +46 -103
- package/dist/src/skills/parallel-agent-skill.js +514 -0
- package/dist/src/utils/hook-installer.js +8 -0
- package/package.json +5 -5
- package/scripts/gepa/.before-optimize.md +140 -159
- package/scripts/gepa/config.json +7 -1
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
- package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
- package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
- package/scripts/gepa/generations/gen-000/baseline.md +172 -159
- package/scripts/gepa/generations/gen-001/baseline.md +172 -159
- package/scripts/gepa/generations/gen-001/variant-a.md +156 -146
- package/scripts/gepa/generations/gen-001/variant-b.md +199 -170
- package/scripts/gepa/generations/gen-001/variant-c.md +127 -46
- package/scripts/gepa/generations/gen-001/variant-d.md +160 -107
- package/scripts/gepa/hooks/reflect.js +44 -5
- package/scripts/gepa/optimize.js +281 -39
- package/scripts/gepa/results/eval-1-baseline.json +187 -10
- package/scripts/gepa/results/eval-1-variant-a.json +188 -11
- package/scripts/gepa/results/eval-1-variant-b.json +188 -11
- package/scripts/gepa/results/eval-1-variant-c.json +168 -11
- package/scripts/gepa/results/eval-1-variant-d.json +169 -12
- package/scripts/gepa/state.json +18 -18
- package/scripts/install-claude-hooks-auto.js +8 -0
- package/templates/claude-hooks/cord-trace.js +225 -0
- package/dist/src/core/config/storage-config.js +0 -114
- package/dist/src/core/storage/chromadb-adapter.js +0 -379
- package/dist/src/integrations/claude-code/enhanced-pre-clear-hooks.js +0 -458
- 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
|
@@ -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
|
}
|
|
@@ -20,6 +20,17 @@ class GraphitiHooks {
|
|
|
20
20
|
emitter.registerHandler("session_start", this.onSessionStart.bind(this));
|
|
21
21
|
emitter.registerHandler("file_change", this.onFileChange.bind(this));
|
|
22
22
|
emitter.registerHandler("session_end", this.onSessionEnd.bind(this));
|
|
23
|
+
emitter.registerHandler("input_idle", this.onInputIdle.bind(this));
|
|
24
|
+
emitter.registerHandler("context_switch", this.onContextSwitch.bind(this));
|
|
25
|
+
emitter.registerHandler("prompt_submit", this.onPromptSubmit.bind(this));
|
|
26
|
+
emitter.registerHandler("tool_use", this.onToolUse.bind(this));
|
|
27
|
+
emitter.registerHandler(
|
|
28
|
+
"suggestion_ready",
|
|
29
|
+
this.onSuggestionReady.bind(this)
|
|
30
|
+
);
|
|
31
|
+
emitter.registerHandler("agent_start", this.onAgentStart.bind(this));
|
|
32
|
+
emitter.registerHandler("agent_complete", this.onAgentComplete.bind(this));
|
|
33
|
+
emitter.registerHandler("agent_error", this.onAgentError.bind(this));
|
|
23
34
|
logger.info("Graphiti hooks registered", {
|
|
24
35
|
endpoint: this.config.endpoint,
|
|
25
36
|
backend: this.config.backend,
|
|
@@ -82,6 +93,144 @@ class GraphitiHooks {
|
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
async onInputIdle(event) {
|
|
97
|
+
const idle = event;
|
|
98
|
+
try {
|
|
99
|
+
const episode = {
|
|
100
|
+
type: "input_idle",
|
|
101
|
+
content: {
|
|
102
|
+
idleDuration: idle.data.idleDuration,
|
|
103
|
+
lastInput: idle.data.lastInput
|
|
104
|
+
},
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
source: "stackmemory"
|
|
107
|
+
};
|
|
108
|
+
await this.client.upsertEpisode(episode);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.debug("Graphiti input_idle episode failed", {
|
|
111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async onContextSwitch(event) {
|
|
116
|
+
const ctx = event;
|
|
117
|
+
try {
|
|
118
|
+
const episode = {
|
|
119
|
+
type: "context_switch",
|
|
120
|
+
content: {
|
|
121
|
+
fromBranch: ctx.data.fromBranch,
|
|
122
|
+
toBranch: ctx.data.toBranch,
|
|
123
|
+
fromProject: ctx.data.fromProject,
|
|
124
|
+
toProject: ctx.data.toProject
|
|
125
|
+
},
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
source: "stackmemory"
|
|
128
|
+
};
|
|
129
|
+
await this.client.upsertEpisode(episode);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.debug("Graphiti context_switch episode failed", {
|
|
132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async onPromptSubmit(event) {
|
|
137
|
+
try {
|
|
138
|
+
const episode = {
|
|
139
|
+
type: "prompt_submit",
|
|
140
|
+
content: event.data || {},
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
source: "stackmemory"
|
|
143
|
+
};
|
|
144
|
+
await this.client.upsertEpisode(episode);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.debug("Graphiti prompt_submit episode failed", {
|
|
147
|
+
error: error instanceof Error ? error.message : String(error)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async onToolUse(event) {
|
|
152
|
+
try {
|
|
153
|
+
const episode = {
|
|
154
|
+
type: "tool_use",
|
|
155
|
+
content: event.data || {},
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
source: "stackmemory"
|
|
158
|
+
};
|
|
159
|
+
await this.client.upsertEpisode(episode);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.debug("Graphiti tool_use episode failed", {
|
|
162
|
+
error: error instanceof Error ? error.message : String(error)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async onSuggestionReady(event) {
|
|
167
|
+
const suggestion = event;
|
|
168
|
+
try {
|
|
169
|
+
const episode = {
|
|
170
|
+
type: "suggestion_ready",
|
|
171
|
+
content: {
|
|
172
|
+
source: suggestion.data.source,
|
|
173
|
+
confidence: suggestion.data.confidence,
|
|
174
|
+
preview: suggestion.data.preview
|
|
175
|
+
},
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
source: "stackmemory"
|
|
178
|
+
};
|
|
179
|
+
await this.client.upsertEpisode(episode);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.debug("Graphiti suggestion_ready episode failed", {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async onAgentStart(event) {
|
|
187
|
+
const e = event;
|
|
188
|
+
try {
|
|
189
|
+
const episode = {
|
|
190
|
+
type: "agent_start",
|
|
191
|
+
content: { agentType: e.data.agentType, task: e.data.task },
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
source: "stackmemory"
|
|
194
|
+
};
|
|
195
|
+
await this.client.upsertEpisode(episode);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.debug("Graphiti agent_start episode failed", {
|
|
198
|
+
error: error instanceof Error ? error.message : String(error)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async onAgentComplete(event) {
|
|
203
|
+
const e = event;
|
|
204
|
+
try {
|
|
205
|
+
const episode = {
|
|
206
|
+
type: "agent_complete",
|
|
207
|
+
content: { ...e.data },
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
source: "stackmemory"
|
|
210
|
+
};
|
|
211
|
+
await this.client.upsertEpisode(episode);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.debug("Graphiti agent_complete episode failed", {
|
|
214
|
+
error: error instanceof Error ? error.message : String(error)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async onAgentError(event) {
|
|
219
|
+
const e = event;
|
|
220
|
+
try {
|
|
221
|
+
const episode = {
|
|
222
|
+
type: "agent_error",
|
|
223
|
+
content: { agentType: e.data.agentType, error: e.data.error },
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
source: "stackmemory"
|
|
226
|
+
};
|
|
227
|
+
await this.client.upsertEpisode(episode);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.debug("Graphiti agent_error episode failed", {
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
85
234
|
// Expose a simple temporal query helper for future MCP tooling
|
|
86
235
|
async buildTemporalContext(query = {}) {
|
|
87
236
|
const now = Date.now();
|
|
@@ -3,6 +3,8 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
6
8
|
import { pickNextLinearTask } from "./linear-task-picker.js";
|
|
7
9
|
function formatDuration(ms) {
|
|
8
10
|
const seconds = Math.floor(ms / 1e3);
|
|
@@ -125,6 +127,34 @@ async function generateSuggestions(context) {
|
|
|
125
127
|
}
|
|
126
128
|
} catch {
|
|
127
129
|
}
|
|
130
|
+
try {
|
|
131
|
+
const patchDir = path.join(process.cwd(), ".stackmemory", "patches");
|
|
132
|
+
if (fs.existsSync(patchDir)) {
|
|
133
|
+
const patches = fs.readdirSync(patchDir).filter((f) => f.endsWith(".patch"));
|
|
134
|
+
if (patches.length > 0) {
|
|
135
|
+
suggestions.push({
|
|
136
|
+
key: String(keyIndex++),
|
|
137
|
+
label: `Review ${patches.length} pending patch${patches.length > 1 ? "es" : ""}`,
|
|
138
|
+
action: `ls -la .stackmemory/patches/`,
|
|
139
|
+
priority: 75
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const tmpEntries = fs.readdirSync("/tmp").filter((d) => d.startsWith("sm-spec-"));
|
|
147
|
+
if (tmpEntries.length > 0) {
|
|
148
|
+
const latest = path.join("/tmp", tmpEntries[tmpEntries.length - 1]);
|
|
149
|
+
suggestions.push({
|
|
150
|
+
key: String(keyIndex++),
|
|
151
|
+
label: `Inspect spec branch (${tmpEntries.length} workspace${tmpEntries.length > 1 ? "s" : ""})`,
|
|
152
|
+
action: `cd ${latest} && git log --oneline -5`,
|
|
153
|
+
priority: 65
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
128
158
|
const durationMs = Date.now() - context.sessionStartTime;
|
|
129
159
|
if (durationMs > 30 * 60 * 1e3) {
|
|
130
160
|
suggestions.push({
|