@stackmemoryai/stackmemory 1.5.9 → 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/dist/src/cli/commands/orchestrate.js +506 -33
- package/dist/src/cli/commands/orchestrator.js +208 -31
- package/dist/src/daemon/daemon-config.js +2 -3
- package/dist/src/daemon/session-daemon.js +4 -5
- package/dist/src/daemon/unified-daemon.js +4 -5
- package/dist/src/features/sweep/sweep-server-manager.js +3 -6
- package/dist/src/hooks/daemon.js +5 -8
- package/dist/src/utils/process-cleanup.js +9 -0
- package/package.json +1 -2
- package/scripts/gepa/.before-optimize.md +0 -112
- package/scripts/gepa/README.md +0 -275
- package/scripts/gepa/config.json +0 -59
- package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
- package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
- package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
- package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
- package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
- package/scripts/gepa/generations/gen-000/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
- package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
- package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
- package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
- package/scripts/gepa/hooks/auto-optimize.js +0 -494
- package/scripts/gepa/hooks/eval-tracker.js +0 -203
- package/scripts/gepa/hooks/reflect.js +0 -350
- package/scripts/gepa/optimize.js +0 -853
- package/scripts/gepa/results/eval-1-baseline.json +0 -218
- package/scripts/gepa/results/eval-1-variant-a.json +0 -218
- package/scripts/gepa/results/eval-1-variant-b.json +0 -218
- package/scripts/gepa/results/eval-1-variant-c.json +0 -218
- package/scripts/gepa/results/eval-1-variant-d.json +0 -218
- package/scripts/gepa/state.json +0 -49
|
@@ -20,6 +20,7 @@ import { createInterface } from "readline";
|
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { Transform } from "stream";
|
|
22
22
|
import { logger } from "../../core/monitoring/logger.js";
|
|
23
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
23
24
|
import {
|
|
24
25
|
LinearClient
|
|
25
26
|
} from "../../integrations/linear/client.js";
|
|
@@ -37,6 +38,86 @@ function logAgentOutcome(entry) {
|
|
|
37
38
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
38
39
|
appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
|
|
39
40
|
}
|
|
41
|
+
function getRetryStrategy(issue, outcomes) {
|
|
42
|
+
if (!outcomes) {
|
|
43
|
+
const logPath = getOutcomesLogPath();
|
|
44
|
+
if (!existsSync(logPath)) {
|
|
45
|
+
return { shouldRetry: true, adjustments: [] };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
49
|
+
outcomes = lines.map((l) => JSON.parse(l));
|
|
50
|
+
} catch {
|
|
51
|
+
return { shouldRetry: true, adjustments: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const issueOutcomes = outcomes.filter((o) => o.issue === issue);
|
|
55
|
+
const relevant = issueOutcomes.length > 0 ? issueOutcomes : outcomes;
|
|
56
|
+
const failures = relevant.filter(
|
|
57
|
+
(o) => o.outcome === "failure" || o.outcome === "partial"
|
|
58
|
+
);
|
|
59
|
+
if (failures.length === 0) {
|
|
60
|
+
return { shouldRetry: true, adjustments: [] };
|
|
61
|
+
}
|
|
62
|
+
const lastFailure = failures[failures.length - 1];
|
|
63
|
+
if (lastFailure.errorTail && /429|rate.?limit|too many requests|usage.?limit|overloaded/i.test(
|
|
64
|
+
lastFailure.errorTail
|
|
65
|
+
)) {
|
|
66
|
+
return {
|
|
67
|
+
shouldRetry: false,
|
|
68
|
+
reason: "Last failure was a rate limit (429) \u2014 retrying immediately will not help",
|
|
69
|
+
adjustments: []
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (issueOutcomes.length > 0) {
|
|
73
|
+
const issueFailures = issueOutcomes.filter(
|
|
74
|
+
(o) => o.outcome === "failure" || o.outcome === "partial"
|
|
75
|
+
);
|
|
76
|
+
const phaseFailCounts = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const f of issueFailures) {
|
|
78
|
+
phaseFailCounts.set(f.phase, (phaseFailCounts.get(f.phase) || 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
for (const [phase, count] of phaseFailCounts) {
|
|
81
|
+
if (count >= 2) {
|
|
82
|
+
return {
|
|
83
|
+
shouldRetry: false,
|
|
84
|
+
reason: `Issue has failed ${count} times in '${phase}' phase \u2014 likely a structural problem`,
|
|
85
|
+
adjustments: []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const adjustments = [];
|
|
91
|
+
if (lastFailure.errorTail) {
|
|
92
|
+
const tail = lastFailure.errorTail;
|
|
93
|
+
if (/timeout|timed?.?out|ETIMEDOUT|ESOCKETTIMEDOUT/i.test(tail)) {
|
|
94
|
+
adjustments.push(
|
|
95
|
+
"Previous attempt timed out. Work in smaller increments and commit partial progress early."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (/lint|eslint|prettier|formatting|style.?error/i.test(tail)) {
|
|
99
|
+
adjustments.push(
|
|
100
|
+
"Previous attempt failed on linting. Run `npm run lint:fix` after each file change and fix all lint errors before committing."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (/test.?fail|assertion|expect|vitest|jest|FAIL/i.test(tail)) {
|
|
104
|
+
adjustments.push(
|
|
105
|
+
"Previous attempt failed tests. Run tests incrementally after each change. Check existing test expectations before modifying code."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (/build.?fail|tsc|type.?error|TS\d{4}|compile/i.test(tail)) {
|
|
109
|
+
adjustments.push(
|
|
110
|
+
"Previous attempt had build/type errors. Run `npm run build` after changes and fix type errors before committing."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (/cannot find module|ERR_MODULE_NOT_FOUND|import/i.test(tail)) {
|
|
114
|
+
adjustments.push(
|
|
115
|
+
"Previous attempt had module resolution errors. Ensure all imports use .js extensions for relative paths (ESM)."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { shouldRetry: true, adjustments };
|
|
120
|
+
}
|
|
40
121
|
function findPackageRoot() {
|
|
41
122
|
const currentFile = fileURLToPath(import.meta.url);
|
|
42
123
|
let dir = dirname(currentFile);
|
|
@@ -135,6 +216,52 @@ function inferPhase(msg) {
|
|
|
135
216
|
}
|
|
136
217
|
return null;
|
|
137
218
|
}
|
|
219
|
+
const SIMPLE_LABELS = ["bug", "fix", "typo", "chore", "docs", "hotfix"];
|
|
220
|
+
const COMPLEX_LABELS = [
|
|
221
|
+
"feature",
|
|
222
|
+
"refactor",
|
|
223
|
+
"architecture",
|
|
224
|
+
"migration",
|
|
225
|
+
"security",
|
|
226
|
+
"performance"
|
|
227
|
+
];
|
|
228
|
+
function estimateIssueComplexity(issue, attempt) {
|
|
229
|
+
let score = 0;
|
|
230
|
+
const descLen = (issue.description || "").length;
|
|
231
|
+
if (descLen > 800) score += 2;
|
|
232
|
+
else if (descLen > 400) score += 1;
|
|
233
|
+
else if (descLen < 200) score -= 1;
|
|
234
|
+
const labelNames = (issue.labels || []).map((l) => l.name.toLowerCase());
|
|
235
|
+
for (const label of labelNames) {
|
|
236
|
+
if (SIMPLE_LABELS.some((s) => label.includes(s))) score -= 1;
|
|
237
|
+
if (COMPLEX_LABELS.some((c) => label.includes(c))) score += 2;
|
|
238
|
+
}
|
|
239
|
+
const titleLower = issue.title.toLowerCase();
|
|
240
|
+
if (SIMPLE_LABELS.some((s) => titleLower.includes(s))) score -= 1;
|
|
241
|
+
if (COMPLEX_LABELS.some((c) => titleLower.includes(c))) score += 1;
|
|
242
|
+
if (issue.priority === 1) score += 2;
|
|
243
|
+
else if (issue.priority === 2) score += 1;
|
|
244
|
+
if (issue.estimate) {
|
|
245
|
+
if (issue.estimate >= 5) score += 2;
|
|
246
|
+
else if (issue.estimate >= 3) score += 1;
|
|
247
|
+
else if (issue.estimate <= 1) score -= 1;
|
|
248
|
+
}
|
|
249
|
+
if (attempt && attempt > 1) score += 2;
|
|
250
|
+
if (score >= 3) return "complex";
|
|
251
|
+
if (score >= 1) return "moderate";
|
|
252
|
+
return "simple";
|
|
253
|
+
}
|
|
254
|
+
function selectModelForIssue(complexity, config) {
|
|
255
|
+
const modelSetting = config.model || "auto";
|
|
256
|
+
if (modelSetting !== "auto") return modelSetting;
|
|
257
|
+
switch (complexity) {
|
|
258
|
+
case "simple":
|
|
259
|
+
case "moderate":
|
|
260
|
+
return "claude-sonnet-4-20250514";
|
|
261
|
+
case "complex":
|
|
262
|
+
return "claude-opus-4-20250514";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
138
265
|
const DEFAULT_CONFIG = {
|
|
139
266
|
activeStates: ["Todo"],
|
|
140
267
|
terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
|
|
@@ -654,7 +781,8 @@ class Conductor {
|
|
|
654
781
|
filesModified: run.filesModified,
|
|
655
782
|
tokensUsed: run.tokensUsed,
|
|
656
783
|
durationMs: Date.now() - run.startedAt,
|
|
657
|
-
hasCommits: true
|
|
784
|
+
hasCommits: true,
|
|
785
|
+
labels: issue.labels.map((l) => l.name)
|
|
658
786
|
});
|
|
659
787
|
await this.runHook(
|
|
660
788
|
"after-run",
|
|
@@ -677,6 +805,20 @@ class Conductor {
|
|
|
677
805
|
error: run.error,
|
|
678
806
|
attempt: run.attempt
|
|
679
807
|
});
|
|
808
|
+
logAgentOutcome({
|
|
809
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
810
|
+
issue: issue.identifier,
|
|
811
|
+
attempt: run.attempt,
|
|
812
|
+
outcome: "failure",
|
|
813
|
+
phase: run.phase,
|
|
814
|
+
toolCalls: run.toolCalls,
|
|
815
|
+
filesModified: run.filesModified,
|
|
816
|
+
tokensUsed: run.tokensUsed,
|
|
817
|
+
durationMs: Date.now() - run.startedAt,
|
|
818
|
+
hasCommits: false,
|
|
819
|
+
labels: issue.labels.map((l) => l.name),
|
|
820
|
+
errorTail: run.error?.slice(-500)
|
|
821
|
+
});
|
|
680
822
|
if (this.handleRateLimitError(run.error, issue.identifier)) {
|
|
681
823
|
throw err;
|
|
682
824
|
}
|
|
@@ -690,6 +832,23 @@ class Conductor {
|
|
|
690
832
|
});
|
|
691
833
|
}
|
|
692
834
|
if (run.attempt < maxAttempts && !this.stopping) {
|
|
835
|
+
const strategy = getRetryStrategy(issue.identifier);
|
|
836
|
+
if (!strategy.shouldRetry) {
|
|
837
|
+
console.log(
|
|
838
|
+
`[${issue.identifier}] Skipping retry: ${strategy.reason}`
|
|
839
|
+
);
|
|
840
|
+
logger.info("Retry skipped by intelligence", {
|
|
841
|
+
identifier: issue.identifier,
|
|
842
|
+
reason: strategy.reason
|
|
843
|
+
});
|
|
844
|
+
throw err;
|
|
845
|
+
}
|
|
846
|
+
if (strategy.adjustments.length > 0) {
|
|
847
|
+
run.retryAdjustments = strategy.adjustments;
|
|
848
|
+
console.log(
|
|
849
|
+
`[${issue.identifier}] Retry with adjustments: ${strategy.adjustments.length} hint(s)`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
693
852
|
console.log(
|
|
694
853
|
`[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
|
|
695
854
|
);
|
|
@@ -1023,25 +1182,33 @@ class Conductor {
|
|
|
1023
1182
|
*/
|
|
1024
1183
|
runAgentCLI(issue, run) {
|
|
1025
1184
|
return new Promise((resolve, reject) => {
|
|
1026
|
-
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1185
|
+
const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
|
|
1186
|
+
const complexity = estimateIssueComplexity(issue, run.attempt);
|
|
1187
|
+
const selectedModel = selectModelForIssue(complexity, this.config);
|
|
1188
|
+
logger.info("Agent model selected", {
|
|
1189
|
+
identifier: issue.identifier,
|
|
1190
|
+
complexity,
|
|
1191
|
+
model: selectedModel || "(default)",
|
|
1192
|
+
reason: this.config.model && this.config.model !== "auto" ? "config override" : `auto: ${complexity} issue`
|
|
1193
|
+
});
|
|
1027
1194
|
const claudeBin = this.findClaudeBinary();
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
);
|
|
1195
|
+
const args = [
|
|
1196
|
+
"-p",
|
|
1197
|
+
"--output-format",
|
|
1198
|
+
"stream-json",
|
|
1199
|
+
"--dangerously-skip-permissions",
|
|
1200
|
+
"--settings",
|
|
1201
|
+
'{"hooks":{}}'
|
|
1202
|
+
];
|
|
1203
|
+
if (selectedModel) {
|
|
1204
|
+
args.push("--model", selectedModel);
|
|
1205
|
+
}
|
|
1206
|
+
args.push(prompt);
|
|
1207
|
+
const proc = spawn(claudeBin, args, {
|
|
1208
|
+
cwd: run.workspacePath,
|
|
1209
|
+
env: this.buildAgentEnv(issue, run),
|
|
1210
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1211
|
+
});
|
|
1045
1212
|
run.process = proc;
|
|
1046
1213
|
proc.stdin.end();
|
|
1047
1214
|
const logStream = this.openAgentLogStream(issue.identifier);
|
|
@@ -1145,7 +1312,7 @@ class Conductor {
|
|
|
1145
1312
|
*/
|
|
1146
1313
|
runAgentAdapter(issue, run) {
|
|
1147
1314
|
return new Promise((resolve, reject) => {
|
|
1148
|
-
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1315
|
+
const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
|
|
1149
1316
|
const proc = spawn("node", [this.config.appServerPath], {
|
|
1150
1317
|
cwd: run.workspacePath,
|
|
1151
1318
|
env: this.buildAgentEnv(issue, run),
|
|
@@ -1300,7 +1467,7 @@ class Conductor {
|
|
|
1300
1467
|
* Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
|
|
1301
1468
|
* {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
1302
1469
|
*/
|
|
1303
|
-
buildPrompt(issue, attempt) {
|
|
1470
|
+
buildPrompt(issue, attempt, retryAdjustments) {
|
|
1304
1471
|
const templatePath = join(
|
|
1305
1472
|
homedir(),
|
|
1306
1473
|
".stackmemory",
|
|
@@ -1309,7 +1476,20 @@ class Conductor {
|
|
|
1309
1476
|
);
|
|
1310
1477
|
const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
|
|
1311
1478
|
const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
|
|
1312
|
-
const
|
|
1479
|
+
const contextParts = [];
|
|
1480
|
+
if (attempt > 1) {
|
|
1481
|
+
contextParts.push(
|
|
1482
|
+
`This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
if (retryAdjustments && retryAdjustments.length > 0) {
|
|
1486
|
+
contextParts.push(
|
|
1487
|
+
"## Retry Adjustments (from prior failure analysis)",
|
|
1488
|
+
"",
|
|
1489
|
+
...retryAdjustments.map((a) => `- ${a}`)
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
const priorContext = contextParts.join("\n");
|
|
1313
1493
|
if (existsSync(templatePath)) {
|
|
1314
1494
|
try {
|
|
1315
1495
|
let template = readFileSync(templatePath, "utf-8");
|
|
@@ -1469,14 +1649,7 @@ class Conductor {
|
|
|
1469
1649
|
if (elapsed < staleThresholdMs) continue;
|
|
1470
1650
|
const elapsedMin = Math.round(elapsed / 6e4);
|
|
1471
1651
|
const pid = run.process?.pid;
|
|
1472
|
-
|
|
1473
|
-
if (pid) {
|
|
1474
|
-
try {
|
|
1475
|
-
process.kill(pid, 0);
|
|
1476
|
-
alive = true;
|
|
1477
|
-
} catch {
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1652
|
+
const alive = pid ? isProcessAlive(pid) : false;
|
|
1480
1653
|
if (!alive) {
|
|
1481
1654
|
logger.warn("Stale agent process is dead, cleaning up", {
|
|
1482
1655
|
identifier: run.issue.identifier,
|
|
@@ -1553,6 +1726,7 @@ class Conductor {
|
|
|
1553
1726
|
tokensUsed: run.tokensUsed,
|
|
1554
1727
|
durationMs,
|
|
1555
1728
|
hasCommits,
|
|
1729
|
+
labels: run.issue.labels.map((l) => l.name),
|
|
1556
1730
|
errorTail
|
|
1557
1731
|
});
|
|
1558
1732
|
if (hasCommits) {
|
|
@@ -1637,6 +1811,9 @@ class Conductor {
|
|
|
1637
1811
|
}
|
|
1638
1812
|
export {
|
|
1639
1813
|
Conductor,
|
|
1814
|
+
estimateIssueComplexity,
|
|
1640
1815
|
getAgentStatusDir,
|
|
1641
|
-
getOutcomesLogPath
|
|
1816
|
+
getOutcomesLogPath,
|
|
1817
|
+
getRetryStrategy,
|
|
1818
|
+
selectModelForIssue
|
|
1642
1819
|
};
|
|
@@ -5,6 +5,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
8
9
|
const DEFAULT_DAEMON_CONFIG = {
|
|
9
10
|
version: "1.0.0",
|
|
10
11
|
context: {
|
|
@@ -139,9 +140,7 @@ function readDaemonStatus() {
|
|
|
139
140
|
try {
|
|
140
141
|
const pidContent = readFileSync(pidFile, "utf8").trim();
|
|
141
142
|
const pid = parseInt(pidContent, 10);
|
|
142
|
-
|
|
143
|
-
process.kill(pid, 0);
|
|
144
|
-
} catch {
|
|
143
|
+
if (!isProcessAlive(pid)) {
|
|
145
144
|
return defaultStatus;
|
|
146
145
|
}
|
|
147
146
|
if (!existsSync(statusFile)) {
|
|
@@ -6,6 +6,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
9
10
|
class SessionDaemon {
|
|
10
11
|
config;
|
|
11
12
|
state;
|
|
@@ -69,16 +70,14 @@ class SessionDaemon {
|
|
|
69
70
|
try {
|
|
70
71
|
const existingPid = fs.readFileSync(this.pidFile, "utf8").trim();
|
|
71
72
|
const pid = parseInt(existingPid, 10);
|
|
72
|
-
|
|
73
|
-
process.kill(pid, 0);
|
|
73
|
+
if (isProcessAlive(pid)) {
|
|
74
74
|
this.log("WARN", "Daemon already running for this session", {
|
|
75
75
|
existingPid: pid
|
|
76
76
|
});
|
|
77
77
|
return false;
|
|
78
|
-
} catch {
|
|
79
|
-
this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
|
|
80
|
-
fs.unlinkSync(this.pidFile);
|
|
81
78
|
}
|
|
79
|
+
this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
|
|
80
|
+
fs.unlinkSync(this.pidFile);
|
|
82
81
|
} catch {
|
|
83
82
|
try {
|
|
84
83
|
fs.unlinkSync(this.pidFile);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
appendFileSync,
|
|
11
11
|
readFileSync
|
|
12
12
|
} from "fs";
|
|
13
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
13
14
|
import {
|
|
14
15
|
loadDaemonConfig,
|
|
15
16
|
getDaemonPaths,
|
|
@@ -75,14 +76,12 @@ class UnifiedDaemon {
|
|
|
75
76
|
try {
|
|
76
77
|
const existingPid = readFileSync(this.paths.pidFile, "utf8").trim();
|
|
77
78
|
const pid = parseInt(existingPid, 10);
|
|
78
|
-
|
|
79
|
-
process.kill(pid, 0);
|
|
79
|
+
if (isProcessAlive(pid)) {
|
|
80
80
|
this.log("WARN", "daemon", "Daemon already running", { pid });
|
|
81
81
|
return false;
|
|
82
|
-
} catch {
|
|
83
|
-
this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
|
|
84
|
-
unlinkSync(this.paths.pidFile);
|
|
85
82
|
}
|
|
83
|
+
this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
|
|
84
|
+
unlinkSync(this.paths.pidFile);
|
|
86
85
|
} catch {
|
|
87
86
|
try {
|
|
88
87
|
unlinkSync(this.paths.pidFile);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./types.js";
|
|
11
11
|
import { createPredictionClient } from "./prediction-client.js";
|
|
12
12
|
import { logger } from "../../core/monitoring/logger.js";
|
|
13
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
13
14
|
const HOME = process.env["HOME"] || "/tmp";
|
|
14
15
|
const PID_FILE = join(HOME, ".stackmemory", "sweep", "server.pid");
|
|
15
16
|
const LOG_FILE = join(HOME, ".stackmemory", "sweep", "server.log");
|
|
@@ -149,9 +150,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
|
|
|
149
150
|
process.kill(status.pid, "SIGTERM");
|
|
150
151
|
await new Promise((resolve) => {
|
|
151
152
|
const checkInterval = setInterval(() => {
|
|
152
|
-
|
|
153
|
-
process.kill(status.pid, 0);
|
|
154
|
-
} catch {
|
|
153
|
+
if (!isProcessAlive(status.pid)) {
|
|
155
154
|
clearInterval(checkInterval);
|
|
156
155
|
resolve();
|
|
157
156
|
}
|
|
@@ -186,9 +185,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
|
|
|
186
185
|
try {
|
|
187
186
|
const data = JSON.parse(readFileSync(PID_FILE, "utf-8"));
|
|
188
187
|
const { pid, port, host, modelPath, startedAt } = data;
|
|
189
|
-
|
|
190
|
-
process.kill(pid, 0);
|
|
191
|
-
} catch {
|
|
188
|
+
if (!isProcessAlive(pid)) {
|
|
192
189
|
return { running: false };
|
|
193
190
|
}
|
|
194
191
|
const client = createPredictionClient({ port, host });
|
package/dist/src/hooks/daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ 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 { isProcessAlive } from "../utils/process-cleanup.js";
|
|
16
17
|
import {
|
|
17
18
|
hookEmitter
|
|
18
19
|
} from "./events.js";
|
|
@@ -51,13 +52,11 @@ async function startDaemon(options = {}) {
|
|
|
51
52
|
const pidFile = config.daemon.pid_file;
|
|
52
53
|
if (existsSync(pidFile)) {
|
|
53
54
|
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
54
|
-
|
|
55
|
-
process.kill(pid, 0);
|
|
55
|
+
if (isProcessAlive(pid)) {
|
|
56
56
|
log("warn", "Daemon already running", { pid });
|
|
57
57
|
return;
|
|
58
|
-
} catch {
|
|
59
|
-
unlinkSync(pidFile);
|
|
60
58
|
}
|
|
59
|
+
unlinkSync(pidFile);
|
|
61
60
|
}
|
|
62
61
|
if (!options.foreground) {
|
|
63
62
|
const child = spawn(
|
|
@@ -117,17 +116,15 @@ function getDaemonStatus() {
|
|
|
117
116
|
return { running: false };
|
|
118
117
|
}
|
|
119
118
|
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
120
|
-
|
|
121
|
-
process.kill(pid, 0);
|
|
119
|
+
if (isProcessAlive(pid)) {
|
|
122
120
|
return {
|
|
123
121
|
running: true,
|
|
124
122
|
pid,
|
|
125
123
|
uptime: state.running ? Date.now() - state.startTime : void 0,
|
|
126
124
|
eventsProcessed: state.eventsProcessed
|
|
127
125
|
};
|
|
128
|
-
} catch {
|
|
129
|
-
return { running: false };
|
|
130
126
|
}
|
|
127
|
+
return { running: false };
|
|
131
128
|
}
|
|
132
129
|
function setupLogStream() {
|
|
133
130
|
const logFile = config.daemon.log_file;
|
|
@@ -7,6 +7,14 @@ import * as fs from "fs";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
9
|
import { logger } from "../core/monitoring/logger.js";
|
|
10
|
+
function isProcessAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
const STACKMEMORY_PROCESS_PATTERNS = [
|
|
11
19
|
"stackmemory",
|
|
12
20
|
"ralph orchestrate",
|
|
@@ -131,5 +139,6 @@ export {
|
|
|
131
139
|
cleanupStaleProcesses,
|
|
132
140
|
findStaleProcesses,
|
|
133
141
|
getStackmemoryProcesses,
|
|
142
|
+
isProcessAlive,
|
|
134
143
|
killStaleProcesses
|
|
135
144
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"bin",
|
|
22
22
|
"dist/src",
|
|
23
|
-
"scripts/gepa",
|
|
24
23
|
"scripts/git-hooks",
|
|
25
24
|
"scripts/hooks",
|
|
26
25
|
"scripts/setup",
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
Sol is the monorepo for **Rize**, an automatic time tracking application. The stack consists of:
|
|
8
|
-
- **api/** - Rails 7.1 GraphQL backend (Ruby 3.3.5)
|
|
9
|
-
- **web/** - Next.js 14 React web app (Node 22)
|
|
10
|
-
- **electron/** - Electron desktop app (Node 22)
|
|
11
|
-
- **services/** - Bun-based TypeScript event consumers/workers
|
|
12
|
-
- **vanity/** - Webflow marketing site scripts (deprecated)
|
|
13
|
-
- **voyager/** - Marketing website for home and landing pageas (Next.js)
|
|
14
|
-
- **puppet/** - Puppeteer server for images/PDFs
|
|
15
|
-
- **chrome/** - Chrome browser extension
|
|
16
|
-
- **docs/** - Docusaurus documentation site
|
|
17
|
-
- **zapier/** - Zapier integration
|
|
18
|
-
|
|
19
|
-
## Development Commands
|
|
20
|
-
|
|
21
|
-
### Starting Development Environment
|
|
22
|
-
```bash
|
|
23
|
-
# Start all services (requires iTerm2 on macOS)
|
|
24
|
-
./scripts/run-dev.sh
|
|
25
|
-
|
|
26
|
-
# Or start individually:
|
|
27
|
-
cd api && hivemind Procfile.dev # Rails + AnyCable + Sidekiq + Clockwork
|
|
28
|
-
cd web && npm run dev # Next.js dev server
|
|
29
|
-
cd electron && npm run dev # Electron with hot reload
|
|
30
|
-
cd services && hivemind Procfile.dev # Bun services
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Docker Dependencies (api/docker-compose.yml)
|
|
34
|
-
```bash
|
|
35
|
-
cd api && docker-compose up -d
|
|
36
|
-
# TimescaleDB: localhost:15432
|
|
37
|
-
# Redis: localhost:16379
|
|
38
|
-
# Kafka: localhost:9092
|
|
39
|
-
# MySQL: localhost:13306
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Testing
|
|
43
|
-
```bash
|
|
44
|
-
# API (RSpec)
|
|
45
|
-
cd api && bundle exec rspec
|
|
46
|
-
cd api && bundle exec rspec spec/path/to/file_spec.rb # Single file
|
|
47
|
-
cd api && bundle exec rspec spec/path/to/file_spec.rb:42 # Single test at line
|
|
48
|
-
|
|
49
|
-
# Electron (Jest)
|
|
50
|
-
cd electron && npm test
|
|
51
|
-
cd electron && npm run test:watch
|
|
52
|
-
cd electron && npm run test:coverage
|
|
53
|
-
|
|
54
|
-
# Web - no active tests (exits 0)
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Building
|
|
58
|
-
```bash
|
|
59
|
-
cd api && bundle install && rake db:migrate
|
|
60
|
-
cd web && npm run build # Runs gql-gen, tailwind, next build
|
|
61
|
-
cd electron && npm run build # Electron Forge make
|
|
62
|
-
cd services && bun install
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### GraphQL Code Generation
|
|
66
|
-
```bash
|
|
67
|
-
cd web && npm run build # Includes gql codegen
|
|
68
|
-
cd electron && npm run dev # Runs gql codegen as part of dev
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Architecture
|
|
72
|
-
|
|
73
|
-
### GraphQL API Structure
|
|
74
|
-
The API exposes two GraphQL endpoints:
|
|
75
|
-
- **api/v1** - Public API (OAuth consumers, Zapier)
|
|
76
|
-
- **private/v1** - Private API (web, electron apps)
|
|
77
|
-
|
|
78
|
-
Located at `api/app/graphql/{api,private}/v1/`
|
|
79
|
-
|
|
80
|
-
### Real-time Communication
|
|
81
|
-
- **AnyCable** WebSocket server for subscriptions
|
|
82
|
-
- ActionCable channels in `api/app/channels/`
|
|
83
|
-
- WebSocket config: `api/config/cable.yml` and `api/config/anycable.yml`
|
|
84
|
-
|
|
85
|
-
### Background Jobs
|
|
86
|
-
- **Sidekiq** for async job processing (`api/config/sidekiq.yml`)
|
|
87
|
-
- **Clockwork** for scheduled jobs (`api/config/clock.rb`)
|
|
88
|
-
|
|
89
|
-
### Event Streaming
|
|
90
|
-
- **Kafka** for event publishing/consumption
|
|
91
|
-
- Services consume events via `services/consumers/`
|
|
92
|
-
- Kafka config: `api/config/initializers/kafka.rb`
|
|
93
|
-
|
|
94
|
-
### Databases
|
|
95
|
-
- **Primary PostgreSQL** - Main application data
|
|
96
|
-
- **TimescaleDB** - Time-series data (separate connection in `database.yml`)
|
|
97
|
-
- **MySQL** - Legacy/external integrations
|
|
98
|
-
- **Redis** - Caching, ActionCable, Sidekiq
|
|
99
|
-
|
|
100
|
-
## Style Guidelines
|
|
101
|
-
|
|
102
|
-
### JavaScript/TypeScript
|
|
103
|
-
- Use `test()` instead of `it()` in tests
|
|
104
|
-
- Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
|
|
105
|
-
|
|
106
|
-
## Key Configuration Files
|
|
107
|
-
|
|
108
|
-
- `api/config/database.yml` - Database connections (primary + timescale)
|
|
109
|
-
- `api/config/cable.yml` - AnyCable WebSocket config
|
|
110
|
-
- `api/Procfile.dev` - Development processes (rails, anycable, sidekiq, clockwork)
|
|
111
|
-
- `sol.code-workspace` - VS Code multi-folder workspace
|
|
112
|
-
- Each project requires its own `.env` file (not in repo)
|