@stackmemoryai/stackmemory 1.5.9 → 1.6.0
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 +257 -31
- package/dist/src/cli/commands/orchestrator.js +2 -8
- 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 -1
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
mkdirSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
readFileSync,
|
|
12
|
-
readdirSync
|
|
12
|
+
readdirSync,
|
|
13
|
+
copyFileSync
|
|
13
14
|
} from "fs";
|
|
14
15
|
import { join } from "path";
|
|
15
16
|
import { homedir, tmpdir } from "os";
|
|
16
17
|
import Database from "better-sqlite3";
|
|
17
18
|
import { logger } from "../../core/monitoring/logger.js";
|
|
19
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
18
20
|
import { Conductor } from "./orchestrator.js";
|
|
19
21
|
import {
|
|
20
22
|
getAgentStatusDir,
|
|
@@ -123,14 +125,6 @@ const phaseColor = {
|
|
|
123
125
|
testing: c.pink,
|
|
124
126
|
committing: c.green
|
|
125
127
|
};
|
|
126
|
-
function isProcessAlive(pid) {
|
|
127
|
-
try {
|
|
128
|
-
process.kill(pid, 0);
|
|
129
|
-
return true;
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
128
|
function phaseProgress(phase, toolCalls, stale, alive) {
|
|
135
129
|
const basePct = {
|
|
136
130
|
reading: 10,
|
|
@@ -231,6 +225,205 @@ function printUsageSummary(u) {
|
|
|
231
225
|
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
|
|
232
226
|
);
|
|
233
227
|
}
|
|
228
|
+
const DEFAULT_PROMPT_TEMPLATE = `# Agent Prompt \u2014 {{ISSUE_ID}}
|
|
229
|
+
|
|
230
|
+
You are working on Linear issue **{{ISSUE_ID}}**: {{TITLE}}
|
|
231
|
+
|
|
232
|
+
## Description
|
|
233
|
+
|
|
234
|
+
{{DESCRIPTION}}
|
|
235
|
+
|
|
236
|
+
## Context
|
|
237
|
+
|
|
238
|
+
- Priority: {{PRIORITY}}
|
|
239
|
+
- Labels: {{LABELS}}
|
|
240
|
+
{{PRIOR_CONTEXT}}
|
|
241
|
+
|
|
242
|
+
## Instructions
|
|
243
|
+
|
|
244
|
+
1. Read the issue description and related code carefully
|
|
245
|
+
2. Plan your approach before writing code
|
|
246
|
+
3. Implement the requested changes
|
|
247
|
+
4. Run \`npm run lint\` and fix any errors
|
|
248
|
+
5. Run \`npm run test:run\` and fix any failures
|
|
249
|
+
6. Commit your changes with format: \`type(scope): message\`
|
|
250
|
+
|
|
251
|
+
## Rules
|
|
252
|
+
|
|
253
|
+
- Follow existing code conventions (ESM imports with .js extension, TypeScript strict)
|
|
254
|
+
- Keep changes focused \u2014 only modify what the issue requires
|
|
255
|
+
- Write or update tests for any new functionality
|
|
256
|
+
- Do not skip pre-commit hooks
|
|
257
|
+
- If stuck, leave a comment in the code explaining the blocker
|
|
258
|
+
|
|
259
|
+
Work in the current directory. All changes will be on a dedicated branch.
|
|
260
|
+
`;
|
|
261
|
+
function ensureDefaultPromptTemplate() {
|
|
262
|
+
const templatePath = join(
|
|
263
|
+
homedir(),
|
|
264
|
+
".stackmemory",
|
|
265
|
+
"conductor",
|
|
266
|
+
"prompt-template.md"
|
|
267
|
+
);
|
|
268
|
+
if (!existsSync(templatePath)) {
|
|
269
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
270
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
271
|
+
writeFileSync(templatePath, DEFAULT_PROMPT_TEMPLATE);
|
|
272
|
+
console.log(
|
|
273
|
+
` ${c.d}Created default prompt template: ${templatePath}${c.r}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return templatePath;
|
|
277
|
+
}
|
|
278
|
+
function spawnClaudePrint(prompt, timeoutMs = 12e4) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const child = cpSpawn("claude", ["--print"], {
|
|
281
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
282
|
+
env: { ...process.env }
|
|
283
|
+
});
|
|
284
|
+
let stdout = "";
|
|
285
|
+
let stderr = "";
|
|
286
|
+
let killed = false;
|
|
287
|
+
const timer = setTimeout(() => {
|
|
288
|
+
killed = true;
|
|
289
|
+
child.kill("SIGTERM");
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
292
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
293
|
+
child.on("close", (code) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
if (killed)
|
|
296
|
+
return reject(new Error(`claude timed out after ${timeoutMs}ms`));
|
|
297
|
+
if (code !== 0 && !stdout)
|
|
298
|
+
return reject(new Error(stderr || `claude exited ${code}`));
|
|
299
|
+
resolve(stdout);
|
|
300
|
+
});
|
|
301
|
+
child.on("error", (err) => {
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
reject(err);
|
|
304
|
+
});
|
|
305
|
+
child.stdin.write(prompt);
|
|
306
|
+
child.stdin.end();
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function evolvePromptTemplate(input) {
|
|
310
|
+
const {
|
|
311
|
+
templatePath,
|
|
312
|
+
successRate,
|
|
313
|
+
failures,
|
|
314
|
+
failPhases,
|
|
315
|
+
errorPatterns,
|
|
316
|
+
recs,
|
|
317
|
+
outcomes
|
|
318
|
+
} = input;
|
|
319
|
+
let currentTemplate;
|
|
320
|
+
if (existsSync(templatePath)) {
|
|
321
|
+
currentTemplate = readFileSync(templatePath, "utf-8");
|
|
322
|
+
} else {
|
|
323
|
+
currentTemplate = DEFAULT_PROMPT_TEMPLATE;
|
|
324
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
325
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
326
|
+
writeFileSync(templatePath, currentTemplate);
|
|
327
|
+
}
|
|
328
|
+
const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
|
|
329
|
+
const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
|
|
330
|
+
const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
|
|
331
|
+
const errorTails = failedOutcomes.map(
|
|
332
|
+
(o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
|
|
333
|
+
${o.errorTail}`
|
|
334
|
+
).join("\n\n");
|
|
335
|
+
const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
|
|
336
|
+
|
|
337
|
+
CURRENT PROMPT TEMPLATE:
|
|
338
|
+
\`\`\`markdown
|
|
339
|
+
${currentTemplate}
|
|
340
|
+
\`\`\`
|
|
341
|
+
|
|
342
|
+
PERFORMANCE DATA:
|
|
343
|
+
- Success rate: ${successRate}%
|
|
344
|
+
- Total failures: ${failures}
|
|
345
|
+
|
|
346
|
+
FAILURE PHASE BREAKDOWN:
|
|
347
|
+
${failPhaseSummary || " (none)"}
|
|
348
|
+
|
|
349
|
+
ERROR PATTERNS:
|
|
350
|
+
${errorSummary || " (none)"}
|
|
351
|
+
|
|
352
|
+
SAMPLE ERROR TAILS FROM RECENT FAILURES:
|
|
353
|
+
${errorTails || " (none)"}
|
|
354
|
+
|
|
355
|
+
RECOMMENDATIONS FROM ANALYSIS:
|
|
356
|
+
${recs.map((r) => `- ${r}`).join("\n")}
|
|
357
|
+
|
|
358
|
+
YOUR TASK:
|
|
359
|
+
Improve the prompt template to reduce failures. Focus on:
|
|
360
|
+
1. Adding specific instructions that address the most common failure modes
|
|
361
|
+
2. Making implicit requirements explicit (lint rules, test commands, commit format)
|
|
362
|
+
3. Adding guardrails for the error patterns seen (e.g., if lint failures are common, add lint-specific instructions)
|
|
363
|
+
4. Keeping the template concise \u2014 agents work better with clear, structured prompts
|
|
364
|
+
5. Preserving all {{VARIABLE}} placeholders exactly as-is
|
|
365
|
+
|
|
366
|
+
REQUIREMENTS:
|
|
367
|
+
- Output ONLY the improved markdown template
|
|
368
|
+
- Keep all {{VARIABLE}} placeholders: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}}, {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
369
|
+
- Do not add commentary, explanations, or markdown fences around the output
|
|
370
|
+
- Target similar length to the current template (no bloat)
|
|
371
|
+
|
|
372
|
+
OUTPUT THE IMPROVED TEMPLATE:`;
|
|
373
|
+
try {
|
|
374
|
+
console.log(
|
|
375
|
+
` ${c.d}Calling Claude to generate improved template...${c.r}`
|
|
376
|
+
);
|
|
377
|
+
const evolved = await spawnClaudePrint(mutationPrompt);
|
|
378
|
+
if (!evolved.trim()) {
|
|
379
|
+
console.log(` ${c.red}Empty response from Claude \u2014 skipping.${c.r}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const requiredVars = ["{{ISSUE_ID}}", "{{TITLE}}", "{{DESCRIPTION}}"];
|
|
383
|
+
const missing = requiredVars.filter((v) => !evolved.includes(v));
|
|
384
|
+
if (missing.length > 0) {
|
|
385
|
+
console.log(
|
|
386
|
+
` ${c.red}Evolved template missing variables: ${missing.join(", ")} \u2014 skipping.${c.r}`
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
391
|
+
const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
|
|
392
|
+
copyFileSync(templatePath, backupPath);
|
|
393
|
+
console.log(` ${c.d}Backed up to ${backupPath}${c.r}`);
|
|
394
|
+
writeFileSync(templatePath, evolved.trim() + "\n");
|
|
395
|
+
console.log(
|
|
396
|
+
` ${c.green}Evolved template written to ${templatePath}${c.r}`
|
|
397
|
+
);
|
|
398
|
+
const evolutionLog = join(
|
|
399
|
+
homedir(),
|
|
400
|
+
".stackmemory",
|
|
401
|
+
"conductor",
|
|
402
|
+
"evolution-log.jsonl"
|
|
403
|
+
);
|
|
404
|
+
const entry = {
|
|
405
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
406
|
+
successRate,
|
|
407
|
+
failures,
|
|
408
|
+
failPhases,
|
|
409
|
+
errorPatterns,
|
|
410
|
+
backupPath
|
|
411
|
+
};
|
|
412
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
413
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
414
|
+
writeFileSync(
|
|
415
|
+
evolutionLog,
|
|
416
|
+
(existsSync(evolutionLog) ? readFileSync(evolutionLog, "utf-8") : "") + JSON.stringify(entry) + "\n"
|
|
417
|
+
);
|
|
418
|
+
console.log(` ${c.d}Evolution logged to ${evolutionLog}${c.r}`);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
421
|
+
console.log(` ${c.red}Evolution failed: ${msg}${c.r}`);
|
|
422
|
+
console.log(
|
|
423
|
+
` ${c.d}Tip: Ensure 'claude' CLI is available and authenticated.${c.r}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
234
427
|
function createConductorCommands() {
|
|
235
428
|
const cmd = new Command("conductor");
|
|
236
429
|
cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
|
|
@@ -538,12 +731,25 @@ function createConductorCommands() {
|
|
|
538
731
|
const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
|
|
539
732
|
let hasCommits = false;
|
|
540
733
|
if (s.workspacePath && existsSync(s.workspacePath)) {
|
|
734
|
+
let baseBranch = "main";
|
|
541
735
|
try {
|
|
542
|
-
const
|
|
736
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
543
737
|
cwd: s.workspacePath,
|
|
544
738
|
encoding: "utf-8",
|
|
545
|
-
timeout:
|
|
546
|
-
});
|
|
739
|
+
timeout: 5e3
|
|
740
|
+
}).trim();
|
|
741
|
+
baseBranch = ref.replace("refs/remotes/origin/", "");
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const log = execSync(
|
|
746
|
+
`git log origin/${baseBranch}..HEAD --oneline`,
|
|
747
|
+
{
|
|
748
|
+
cwd: s.workspacePath,
|
|
749
|
+
encoding: "utf-8",
|
|
750
|
+
timeout: 1e4
|
|
751
|
+
}
|
|
752
|
+
);
|
|
547
753
|
hasCommits = log.trim().length > 0;
|
|
548
754
|
} catch {
|
|
549
755
|
}
|
|
@@ -600,20 +806,26 @@ function createConductorCommands() {
|
|
|
600
806
|
const lines = parseInt(options.lines, 10);
|
|
601
807
|
const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
|
|
602
808
|
const tail = cpSpawn("tail", args, { stdio: "inherit" });
|
|
809
|
+
const forward = () => {
|
|
810
|
+
tail.kill("SIGTERM");
|
|
811
|
+
};
|
|
812
|
+
process.on("SIGINT", forward);
|
|
813
|
+
process.on("SIGTERM", forward);
|
|
603
814
|
await new Promise((resolve) => {
|
|
604
815
|
tail.on("close", () => {
|
|
816
|
+
process.removeListener("SIGINT", forward);
|
|
817
|
+
process.removeListener("SIGTERM", forward);
|
|
605
818
|
resolve();
|
|
606
819
|
});
|
|
607
|
-
const forward = () => {
|
|
608
|
-
tail.kill("SIGTERM");
|
|
609
|
-
};
|
|
610
|
-
process.on("SIGINT", forward);
|
|
611
|
-
process.on("SIGTERM", forward);
|
|
612
820
|
});
|
|
613
821
|
});
|
|
614
822
|
cmd.command("learn").description(
|
|
615
823
|
"Analyze agent outcomes and generate improved prompt templates"
|
|
616
|
-
).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).
|
|
824
|
+
).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).option(
|
|
825
|
+
"--evolve",
|
|
826
|
+
"Auto-mutate prompt template using GEPA-style evolution from failure data",
|
|
827
|
+
false
|
|
828
|
+
).action(async (options) => {
|
|
617
829
|
const logPath = getOutcomesLogPath();
|
|
618
830
|
if (!existsSync(logPath)) {
|
|
619
831
|
console.log(
|
|
@@ -792,6 +1004,20 @@ function createConductorCommands() {
|
|
|
792
1004
|
console.log(`
|
|
793
1005
|
${c.d}Using custom template: ${templatePath}${c.r}`);
|
|
794
1006
|
}
|
|
1007
|
+
if (options.evolve) {
|
|
1008
|
+
console.log(`
|
|
1009
|
+
${c.b}${c.cyan}Evolving prompt template...${c.r}
|
|
1010
|
+
`);
|
|
1011
|
+
await evolvePromptTemplate({
|
|
1012
|
+
templatePath,
|
|
1013
|
+
successRate,
|
|
1014
|
+
failures,
|
|
1015
|
+
failPhases,
|
|
1016
|
+
errorPatterns,
|
|
1017
|
+
recs,
|
|
1018
|
+
outcomes
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
795
1021
|
console.log("");
|
|
796
1022
|
});
|
|
797
1023
|
cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
|
|
@@ -859,6 +1085,7 @@ function createConductorCommands() {
|
|
|
859
1085
|
'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
|
|
860
1086
|
"cli"
|
|
861
1087
|
).action(async (options) => {
|
|
1088
|
+
ensureDefaultPromptTemplate();
|
|
862
1089
|
const conductor = new Conductor({
|
|
863
1090
|
teamId: options.team,
|
|
864
1091
|
activeStates: options.states.split(",").map((s) => s.trim()),
|
|
@@ -1017,14 +1244,14 @@ function createConductorCommands() {
|
|
|
1017
1244
|
if (!paused) await render();
|
|
1018
1245
|
}, refreshInterval);
|
|
1019
1246
|
await new Promise((resolve) => {
|
|
1020
|
-
|
|
1021
|
-
clearInterval(timer);
|
|
1022
|
-
resolve();
|
|
1023
|
-
});
|
|
1024
|
-
process.on("SIGTERM", () => {
|
|
1247
|
+
const cleanup = () => {
|
|
1025
1248
|
clearInterval(timer);
|
|
1249
|
+
process.removeListener("SIGINT", cleanup);
|
|
1250
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1026
1251
|
resolve();
|
|
1027
|
-
}
|
|
1252
|
+
};
|
|
1253
|
+
process.on("SIGINT", cleanup);
|
|
1254
|
+
process.on("SIGTERM", cleanup);
|
|
1028
1255
|
});
|
|
1029
1256
|
return;
|
|
1030
1257
|
}
|
|
@@ -1137,16 +1364,15 @@ function createConductorCommands() {
|
|
|
1137
1364
|
}
|
|
1138
1365
|
});
|
|
1139
1366
|
await new Promise((resolve) => {
|
|
1140
|
-
|
|
1367
|
+
const cleanup = () => {
|
|
1141
1368
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1142
1369
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1370
|
+
process.removeListener("SIGINT", cleanup);
|
|
1371
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1143
1372
|
resolve();
|
|
1144
|
-
}
|
|
1145
|
-
process.on("
|
|
1146
|
-
|
|
1147
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1148
|
-
resolve();
|
|
1149
|
-
});
|
|
1373
|
+
};
|
|
1374
|
+
process.on("SIGINT", cleanup);
|
|
1375
|
+
process.on("SIGTERM", cleanup);
|
|
1150
1376
|
});
|
|
1151
1377
|
});
|
|
1152
1378
|
return cmd;
|
|
@@ -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";
|
|
@@ -1469,14 +1470,7 @@ class Conductor {
|
|
|
1469
1470
|
if (elapsed < staleThresholdMs) continue;
|
|
1470
1471
|
const elapsedMin = Math.round(elapsed / 6e4);
|
|
1471
1472
|
const pid = run.process?.pid;
|
|
1472
|
-
|
|
1473
|
-
if (pid) {
|
|
1474
|
-
try {
|
|
1475
|
-
process.kill(pid, 0);
|
|
1476
|
-
alive = true;
|
|
1477
|
-
} catch {
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1473
|
+
const alive = pid ? isProcessAlive(pid) : false;
|
|
1480
1474
|
if (!alive) {
|
|
1481
1475
|
logger.warn("Stale agent process is dead, cleaning up", {
|
|
1482
1476
|
identifier: run.issue.identifier,
|
|
@@ -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.0",
|
|
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",
|