@stackmemoryai/stackmemory 1.5.1 → 1.5.3
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
CHANGED
|
@@ -24,7 +24,8 @@ StackMemory is a **production-ready memory runtime** for AI coding tools that pr
|
|
|
24
24
|
- **Automatic hooks** for task tracking, Linear sync, and spec progress
|
|
25
25
|
- **Snapshot capture** — post-run context snapshots for session handoff (`snapshot save`)
|
|
26
26
|
- **Pre-flight overlap check** — predict file conflicts before parallel task dispatch (`preflight`)
|
|
27
|
-
- **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency
|
|
27
|
+
- **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency and auto team detection
|
|
28
|
+
- **Loop/Watch command** — poll any shell command until a condition is met (monitor CI, deploys, logs)
|
|
28
29
|
- **Memory monitor daemon** with automatic capture/clear on RAM pressure
|
|
29
30
|
- **Auto-save service** for periodic context persistence
|
|
30
31
|
- **Comprehensive test coverage** across all core modules
|
|
@@ -68,6 +69,8 @@ Tools forget decisions and constraints between sessions. StackMemory makes conte
|
|
|
68
69
|
- **Safe branches**: worktree isolation with `--worktree` or `-w`
|
|
69
70
|
- **Snapshot**: post-run context capture — records what changed, commits, and decisions (`stackmemory snapshot`)
|
|
70
71
|
- **Pre-flight check**: file overlap prediction before parallel task dispatch (`stackmemory preflight`)
|
|
72
|
+
- **Loop/Watch**: poll shell commands until conditions are met — monitor CI, deploys, logs (`stackmemory loop`)
|
|
73
|
+
- **Conductor**: autonomous orchestrator — polls Linear, creates worktrees, spawns agents with auto team detection
|
|
71
74
|
- **Persistent context**: frames, anchors, decisions, retrieval
|
|
72
75
|
- **Integrations**: Linear (API key + OAuth), DiffMem, Browser MCP, log-mcp (log analysis)
|
|
73
76
|
|
|
@@ -389,6 +392,26 @@ stackmemory pf "auth work" "db migration" -f "task1:src/auth.ts;task2:src/db.ts"
|
|
|
389
392
|
stackmemory pf "task A" "task B" "task C" --json
|
|
390
393
|
```
|
|
391
394
|
|
|
395
|
+
### Loop / Watch (`loop` / `watch`)
|
|
396
|
+
|
|
397
|
+
Poll a shell command until a condition is met. Useful for monitoring CI runs, deploy logs, inboxes, or any external state.
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# Monitor GitHub Actions until complete
|
|
401
|
+
stackmemory loop "gh run view <id> --json status -q .status" --until "completed"
|
|
402
|
+
|
|
403
|
+
# Watch deploy logs for success
|
|
404
|
+
stackmemory watch "railway logs --latest" --until "deployed" -i 5s
|
|
405
|
+
|
|
406
|
+
# Wait for health check to pass
|
|
407
|
+
stackmemory loop "curl -s http://localhost:3000/health" --until "ok" -i 3s -t 5m
|
|
408
|
+
|
|
409
|
+
# JSON output for programmatic use
|
|
410
|
+
stackmemory loop "gh pr checks 42 --json state -q '.[0].state'" --until "SUCCESS" --json
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Options: `--until`, `--until-not`, `--until-empty`, `--until-non-empty`, `--until-exit`, `-i/--interval` (default 10s), `-t/--timeout` (default 30m), `--json`, `-q/--quiet`
|
|
414
|
+
|
|
392
415
|
---
|
|
393
416
|
|
|
394
417
|
## Documentation
|
|
@@ -8,7 +8,7 @@ import Table from "cli-table3";
|
|
|
8
8
|
import { SessionManager } from "../../core/session/session-manager.js";
|
|
9
9
|
import Database from "better-sqlite3";
|
|
10
10
|
import { join } from "path";
|
|
11
|
-
import { existsSync } from "fs";
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
12
|
import { getModelTokenLimit } from "../../core/models/model-router.js";
|
|
13
13
|
const dashboardCommand = {
|
|
14
14
|
command: "dashboard",
|
|
@@ -150,6 +150,52 @@ const dashboardCommand = {
|
|
|
150
150
|
console.log(chalk.yellow.bold("\u{1F4BE} Context Usage"));
|
|
151
151
|
console.log(`${usageBar} ${contextUsage}%`);
|
|
152
152
|
console.log();
|
|
153
|
+
const conductorStatusPath = join(
|
|
154
|
+
projectRoot,
|
|
155
|
+
".stackmemory",
|
|
156
|
+
"conductor-status.json"
|
|
157
|
+
);
|
|
158
|
+
if (existsSync(conductorStatusPath)) {
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(conductorStatusPath, "utf-8");
|
|
161
|
+
const conductorStatus = JSON.parse(raw);
|
|
162
|
+
const stale = Date.now() - conductorStatus.updatedAt > 12e4;
|
|
163
|
+
const header = stale ? chalk.gray.bold("\u2699 Conductor (stale)") : chalk.yellow.bold("\u2699 Conductor");
|
|
164
|
+
console.log(header);
|
|
165
|
+
if (conductorStatus.running.length > 0) {
|
|
166
|
+
const conductorTable = new Table({
|
|
167
|
+
head: [
|
|
168
|
+
chalk.white("Issue"),
|
|
169
|
+
chalk.white("Status"),
|
|
170
|
+
chalk.white("Title"),
|
|
171
|
+
chalk.white("Runtime")
|
|
172
|
+
],
|
|
173
|
+
style: { head: [], border: [] },
|
|
174
|
+
colWidths: [12, 14, 36, 10]
|
|
175
|
+
});
|
|
176
|
+
conductorStatus.running.forEach((r) => {
|
|
177
|
+
const mins = Math.round(r.runtime / 6e4);
|
|
178
|
+
const statusColor = r.status === "running" ? chalk.green : r.status === "completed" ? chalk.cyan : chalk.red;
|
|
179
|
+
conductorTable.push([
|
|
180
|
+
r.identifier,
|
|
181
|
+
statusColor(r.status),
|
|
182
|
+
r.title.slice(0, 34),
|
|
183
|
+
`${mins}m`
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
console.log(conductorTable.toString());
|
|
187
|
+
} else {
|
|
188
|
+
console.log(chalk.gray(" No agents running"));
|
|
189
|
+
}
|
|
190
|
+
console.log(
|
|
191
|
+
chalk.gray(
|
|
192
|
+
` Completed: ${conductorStatus.completed} Failed: ${conductorStatus.failed} Max: ${conductorStatus.maxConcurrent}`
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
console.log();
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
153
199
|
db.close();
|
|
154
200
|
if (argv.watch) {
|
|
155
201
|
console.log(
|
|
@@ -3,13 +3,20 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { execSync } from "child_process";
|
|
7
|
-
import {
|
|
6
|
+
import { execSync, spawn as cpSpawn } from "child_process";
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync
|
|
13
|
+
} from "fs";
|
|
8
14
|
import { join } from "path";
|
|
9
15
|
import { homedir, tmpdir } from "os";
|
|
10
16
|
import Database from "better-sqlite3";
|
|
11
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
12
18
|
import { Conductor } from "./orchestrator.js";
|
|
19
|
+
import { getAgentStatusDir } from "./orchestrator.js";
|
|
13
20
|
function getGlobalStorePath() {
|
|
14
21
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
15
22
|
if (!existsSync(dir)) {
|
|
@@ -43,6 +50,16 @@ function getGlobalDb() {
|
|
|
43
50
|
`);
|
|
44
51
|
return db;
|
|
45
52
|
}
|
|
53
|
+
function formatElapsed(ms) {
|
|
54
|
+
const seconds = Math.floor(ms / 1e3);
|
|
55
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
56
|
+
const minutes = Math.floor(seconds / 60);
|
|
57
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
58
|
+
const hours = Math.floor(minutes / 60);
|
|
59
|
+
if (hours < 24) return `${hours}h ago`;
|
|
60
|
+
const days = Math.floor(hours / 24);
|
|
61
|
+
return `${days}d ago`;
|
|
62
|
+
}
|
|
46
63
|
function createConductorCommands() {
|
|
47
64
|
const cmd = new Command("conductor");
|
|
48
65
|
cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
|
|
@@ -263,6 +280,60 @@ function createConductorCommands() {
|
|
|
263
280
|
}
|
|
264
281
|
globalDb.close();
|
|
265
282
|
});
|
|
283
|
+
cmd.command("status").description("Show running agent status table").action(async () => {
|
|
284
|
+
const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
285
|
+
if (!existsSync(agentsDir)) {
|
|
286
|
+
console.log("No agent status files found");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
290
|
+
const statuses = [];
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (!entry.isDirectory()) continue;
|
|
293
|
+
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
294
|
+
if (!existsSync(statusPath)) continue;
|
|
295
|
+
try {
|
|
296
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
297
|
+
statuses.push(data);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (statuses.length === 0) {
|
|
302
|
+
console.log("No agent status files found");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
statuses.sort(
|
|
306
|
+
(a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
307
|
+
);
|
|
308
|
+
const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
|
|
309
|
+
console.log(header);
|
|
310
|
+
for (const s of statuses) {
|
|
311
|
+
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
312
|
+
const lastUpdate = formatElapsed(elapsed);
|
|
313
|
+
const line = `${s.issue.padEnd(12)}${s.phase.padEnd(16)}${String(s.toolCalls).padStart(7)}${String(s.filesModified).padStart(7)}${String(s.tokensUsed).padStart(9)} ${lastUpdate}`;
|
|
314
|
+
console.log(line);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
cmd.command("logs").description("Tail agent output log").argument("<issue-id>", "Issue identifier (e.g., STA-499)").option("-f, --follow", "Follow the log (tail -f)", false).option("-n, --lines <n>", "Number of lines to show", "50").action(async (issueId, options) => {
|
|
318
|
+
const logPath = join(getAgentStatusDir(issueId), "output.log");
|
|
319
|
+
if (!existsSync(logPath)) {
|
|
320
|
+
console.error(`No log file found for ${issueId} at ${logPath}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const lines = parseInt(options.lines, 10);
|
|
324
|
+
const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
|
|
325
|
+
const tail = cpSpawn("tail", args, { stdio: "inherit" });
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
tail.on("close", () => {
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
const forward = () => {
|
|
331
|
+
tail.kill("SIGTERM");
|
|
332
|
+
};
|
|
333
|
+
process.on("SIGINT", forward);
|
|
334
|
+
process.on("SIGTERM", forward);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
266
337
|
cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
|
|
267
338
|
"--states <states>",
|
|
268
339
|
"Comma-separated issue states to pick up",
|
|
@@ -294,5 +365,6 @@ function createConductorCommands() {
|
|
|
294
365
|
return cmd;
|
|
295
366
|
}
|
|
296
367
|
export {
|
|
297
|
-
createConductorCommands
|
|
368
|
+
createConductorCommands,
|
|
369
|
+
formatElapsed
|
|
298
370
|
};
|
|
@@ -3,10 +3,17 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { spawn, execSync } from "child_process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
createWriteStream
|
|
12
|
+
} from "fs";
|
|
7
13
|
import { join, dirname } from "path";
|
|
8
|
-
import { tmpdir } from "os";
|
|
14
|
+
import { tmpdir, homedir } from "os";
|
|
9
15
|
import { fileURLToPath } from "url";
|
|
16
|
+
import { Transform } from "stream";
|
|
10
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
11
18
|
import {
|
|
12
19
|
LinearClient
|
|
@@ -26,6 +33,65 @@ function findPackageRoot() {
|
|
|
26
33
|
}
|
|
27
34
|
return dirname(currentFile);
|
|
28
35
|
}
|
|
36
|
+
function getAgentStatusDir(issueIdentifier) {
|
|
37
|
+
return join(
|
|
38
|
+
homedir(),
|
|
39
|
+
".stackmemory",
|
|
40
|
+
"conductor",
|
|
41
|
+
"agents",
|
|
42
|
+
issueIdentifier
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function ensureAgentStatusDir(issueIdentifier) {
|
|
46
|
+
const dir = getAgentStatusDir(issueIdentifier);
|
|
47
|
+
if (!existsSync(dir)) {
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
class TeeTransform extends Transform {
|
|
53
|
+
logStream;
|
|
54
|
+
constructor(logStream) {
|
|
55
|
+
super();
|
|
56
|
+
this.logStream = logStream;
|
|
57
|
+
}
|
|
58
|
+
_transform(chunk, _encoding, callback) {
|
|
59
|
+
this.logStream.write(chunk);
|
|
60
|
+
this.push(chunk);
|
|
61
|
+
callback();
|
|
62
|
+
}
|
|
63
|
+
_flush(callback) {
|
|
64
|
+
this.logStream.end();
|
|
65
|
+
callback();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function inferPhase(msg) {
|
|
69
|
+
const method = msg.method;
|
|
70
|
+
const params = msg.params;
|
|
71
|
+
if (method === "item/commandExecution/started") {
|
|
72
|
+
const tool = params?.tool || params?.name || "";
|
|
73
|
+
const toolLower = tool.toLowerCase();
|
|
74
|
+
if (toolLower.includes("read") || toolLower.includes("glob") || toolLower.includes("grep") || toolLower.includes("search")) {
|
|
75
|
+
return "reading";
|
|
76
|
+
}
|
|
77
|
+
if (toolLower.includes("todowrite") || toolLower.includes("todo")) {
|
|
78
|
+
return "planning";
|
|
79
|
+
}
|
|
80
|
+
if (toolLower.includes("edit") || toolLower.includes("write") || toolLower.includes("bash")) {
|
|
81
|
+
return "implementing";
|
|
82
|
+
}
|
|
83
|
+
if (toolLower.includes("test")) {
|
|
84
|
+
return "testing";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (method === "item/commandExecution/started") {
|
|
88
|
+
const command = params?.command || "";
|
|
89
|
+
if (command.includes("git commit") || command.includes("git add")) {
|
|
90
|
+
return "committing";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
29
95
|
const DEFAULT_CONFIG = {
|
|
30
96
|
activeStates: ["Todo"],
|
|
31
97
|
terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
|
|
@@ -105,6 +171,21 @@ class Conductor {
|
|
|
105
171
|
mkdirSync(this.config.workspaceRoot, { recursive: true });
|
|
106
172
|
}
|
|
107
173
|
this.client = await this.createLinearClient();
|
|
174
|
+
if (!this.config.teamId && this.client) {
|
|
175
|
+
try {
|
|
176
|
+
const team = await this.client.getTeam();
|
|
177
|
+
this.config.teamId = team.id;
|
|
178
|
+
logger.info("Auto-detected Linear team", {
|
|
179
|
+
id: team.id,
|
|
180
|
+
name: team.name,
|
|
181
|
+
key: team.key
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logger.warn("Failed to auto-detect team", {
|
|
185
|
+
error: err.message
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
108
189
|
await this.cacheWorkflowStates();
|
|
109
190
|
logger.info("Orchestrator started", {
|
|
110
191
|
activeStates: this.config.activeStates,
|
|
@@ -115,6 +196,7 @@ class Conductor {
|
|
|
115
196
|
console.log(
|
|
116
197
|
`Orchestrator started \u2014 polling every ${this.config.pollIntervalMs / 1e3}s, max ${this.config.maxConcurrent} concurrent`
|
|
117
198
|
);
|
|
199
|
+
this.writeStatusFile();
|
|
118
200
|
const shutdown = () => this.stop();
|
|
119
201
|
process.on("SIGINT", shutdown);
|
|
120
202
|
process.on("SIGTERM", shutdown);
|
|
@@ -160,6 +242,7 @@ class Conductor {
|
|
|
160
242
|
}
|
|
161
243
|
this.running.clear();
|
|
162
244
|
this.claimed.clear();
|
|
245
|
+
this.clearStatusFile();
|
|
163
246
|
console.log(
|
|
164
247
|
`Orchestrator stopped. Completed: ${this.completeCount}, Failed: ${this.failCount}`
|
|
165
248
|
);
|
|
@@ -183,6 +266,82 @@ class Conductor {
|
|
|
183
266
|
issues
|
|
184
267
|
};
|
|
185
268
|
}
|
|
269
|
+
// ── Status File ──
|
|
270
|
+
/**
|
|
271
|
+
* Write current conductor state to .stackmemory/conductor-status.json
|
|
272
|
+
* for consumption by `stackmemory dashboard` and other tools.
|
|
273
|
+
*/
|
|
274
|
+
writeStatusFile() {
|
|
275
|
+
const statusDir = join(this.config.repoRoot, ".stackmemory");
|
|
276
|
+
if (!existsSync(statusDir)) return;
|
|
277
|
+
const status = {
|
|
278
|
+
pid: process.pid,
|
|
279
|
+
startedAt: this.startedAt,
|
|
280
|
+
updatedAt: Date.now(),
|
|
281
|
+
running: Array.from(this.running.values()).map((r) => ({
|
|
282
|
+
identifier: r.issue.identifier,
|
|
283
|
+
title: r.issue.title,
|
|
284
|
+
status: r.status,
|
|
285
|
+
attempt: r.attempt,
|
|
286
|
+
startedAt: r.startedAt,
|
|
287
|
+
runtime: Date.now() - r.startedAt
|
|
288
|
+
})),
|
|
289
|
+
queued: Array.from(this.claimed).filter(
|
|
290
|
+
(id) => !this.running.has(id) && !this.completed.has(id)
|
|
291
|
+
).length,
|
|
292
|
+
completed: this.completeCount,
|
|
293
|
+
failed: this.failCount,
|
|
294
|
+
totalAttempts: this.totalAttempts,
|
|
295
|
+
maxConcurrent: this.config.maxConcurrent,
|
|
296
|
+
stopping: this.stopping
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
writeFileSync(
|
|
300
|
+
join(statusDir, "conductor-status.json"),
|
|
301
|
+
JSON.stringify(status, null, 2)
|
|
302
|
+
);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
clearStatusFile() {
|
|
307
|
+
const statusPath = join(
|
|
308
|
+
this.config.repoRoot,
|
|
309
|
+
".stackmemory",
|
|
310
|
+
"conductor-status.json"
|
|
311
|
+
);
|
|
312
|
+
try {
|
|
313
|
+
if (existsSync(statusPath)) rmSync(statusPath);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ── Agent Status Files ──
|
|
318
|
+
/**
|
|
319
|
+
* Write per-agent status to ~/.stackmemory/conductor/agents/<issue-id>/status.json
|
|
320
|
+
*/
|
|
321
|
+
writeAgentStatus(issueIdentifier, run) {
|
|
322
|
+
try {
|
|
323
|
+
const dir = ensureAgentStatusDir(issueIdentifier);
|
|
324
|
+
const status = {
|
|
325
|
+
issue: issueIdentifier,
|
|
326
|
+
pid: run.process?.pid || process.pid,
|
|
327
|
+
started: new Date(run.startedAt).toISOString(),
|
|
328
|
+
lastUpdate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
329
|
+
phase: run.phase,
|
|
330
|
+
filesModified: run.filesModified,
|
|
331
|
+
toolCalls: run.toolCalls,
|
|
332
|
+
tokensUsed: run.tokensUsed
|
|
333
|
+
};
|
|
334
|
+
writeFileSync(join(dir, "status.json"), JSON.stringify(status, null, 2));
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Open a log file write stream for an agent.
|
|
340
|
+
*/
|
|
341
|
+
openAgentLogStream(issueIdentifier) {
|
|
342
|
+
const dir = ensureAgentStatusDir(issueIdentifier);
|
|
343
|
+
return createWriteStream(join(dir, "output.log"), { flags: "a" });
|
|
344
|
+
}
|
|
186
345
|
// ── Polling ──
|
|
187
346
|
async schedulePoll() {
|
|
188
347
|
while (!this.stopping) {
|
|
@@ -195,6 +354,7 @@ class Conductor {
|
|
|
195
354
|
} catch (err) {
|
|
196
355
|
logger.error("Poll cycle failed", { error: err.message });
|
|
197
356
|
}
|
|
357
|
+
this.writeStatusFile();
|
|
198
358
|
}
|
|
199
359
|
}
|
|
200
360
|
async poll() {
|
|
@@ -322,8 +482,13 @@ class Conductor {
|
|
|
322
482
|
process: null,
|
|
323
483
|
attempt: 1,
|
|
324
484
|
startedAt: Date.now(),
|
|
325
|
-
status: "starting"
|
|
485
|
+
status: "starting",
|
|
486
|
+
phase: "reading",
|
|
487
|
+
toolCalls: 0,
|
|
488
|
+
filesModified: 0,
|
|
489
|
+
tokensUsed: 0
|
|
326
490
|
};
|
|
491
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
327
492
|
this.running.set(issueId, run);
|
|
328
493
|
this.totalAttempts++;
|
|
329
494
|
console.log(`[${issue.identifier}] Dispatching: ${issue.title}`);
|
|
@@ -332,23 +497,26 @@ class Conductor {
|
|
|
332
497
|
run.workspacePath = workspacePath;
|
|
333
498
|
await this.transitionIssue(issue, this.config.inProgressState);
|
|
334
499
|
await this.runHook("after-create", workspacePath, issue);
|
|
335
|
-
|
|
336
|
-
await this.runAgent(issue, run);
|
|
337
|
-
run.status = "completed";
|
|
338
|
-
this.completeCount++;
|
|
339
|
-
await this.runHook("after-run", workspacePath, issue, run.attempt);
|
|
340
|
-
this.takeSnapshot(workspacePath, issue);
|
|
341
|
-
await this.transitionIssue(issue, this.config.inReviewState);
|
|
342
|
-
console.log(`[${issue.identifier}] Completed successfully`);
|
|
500
|
+
await this.attemptRun(issue, run);
|
|
343
501
|
} catch (err) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
502
|
+
this.failCount++;
|
|
503
|
+
console.log(`[${issue.identifier}] Failed: ${err.message}`);
|
|
504
|
+
} finally {
|
|
505
|
+
this.running.delete(issueId);
|
|
506
|
+
this.writeStatusFile();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Run the agent with retry logic. Throws on final failure.
|
|
511
|
+
*/
|
|
512
|
+
async attemptRun(issue, run) {
|
|
513
|
+
const maxAttempts = this.config.maxRetries + 1;
|
|
514
|
+
while (run.attempt <= maxAttempts) {
|
|
515
|
+
try {
|
|
516
|
+
run.status = "running";
|
|
517
|
+
await this.runAgent(issue, run);
|
|
518
|
+
run.status = "completed";
|
|
519
|
+
this.completeCount++;
|
|
352
520
|
await this.runHook(
|
|
353
521
|
"after-run",
|
|
354
522
|
run.workspacePath,
|
|
@@ -356,47 +524,44 @@ class Conductor {
|
|
|
356
524
|
run.attempt
|
|
357
525
|
).catch(() => {
|
|
358
526
|
});
|
|
359
|
-
|
|
360
|
-
|
|
527
|
+
this.takeSnapshot(run.workspacePath, issue);
|
|
528
|
+
await this.transitionIssue(issue, this.config.inReviewState);
|
|
361
529
|
console.log(
|
|
362
|
-
`[${issue.identifier}]
|
|
530
|
+
run.attempt === 1 ? `[${issue.identifier}] Completed successfully` : `[${issue.identifier}] Completed on retry ${run.attempt}`
|
|
363
531
|
);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
532
|
+
return;
|
|
533
|
+
} catch (err) {
|
|
534
|
+
run.status = "failed";
|
|
535
|
+
run.error = err.message;
|
|
536
|
+
logger.error("Agent run failed", {
|
|
537
|
+
identifier: issue.identifier,
|
|
538
|
+
error: run.error,
|
|
539
|
+
attempt: run.attempt
|
|
540
|
+
});
|
|
541
|
+
if (run.workspacePath) {
|
|
542
|
+
await this.runHook(
|
|
543
|
+
"after-run",
|
|
544
|
+
run.workspacePath,
|
|
545
|
+
issue,
|
|
546
|
+
run.attempt
|
|
547
|
+
).catch(() => {
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (run.attempt < maxAttempts && !this.stopping) {
|
|
551
|
+
console.log(
|
|
552
|
+
`[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
|
|
553
|
+
);
|
|
554
|
+
run.attempt++;
|
|
555
|
+
this.totalAttempts++;
|
|
556
|
+
const backoffMs = Math.min(
|
|
557
|
+
1e3 * Math.pow(2, run.attempt - 1),
|
|
558
|
+
3e5
|
|
559
|
+
);
|
|
560
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
561
|
+
} else {
|
|
562
|
+
throw err;
|
|
393
563
|
}
|
|
394
|
-
} else {
|
|
395
|
-
this.failCount++;
|
|
396
|
-
console.log(`[${issue.identifier}] Failed: ${run.error}`);
|
|
397
564
|
}
|
|
398
|
-
} finally {
|
|
399
|
-
this.running.delete(issueId);
|
|
400
565
|
}
|
|
401
566
|
}
|
|
402
567
|
// ── Workspace Management ──
|
|
@@ -492,6 +657,11 @@ class Conductor {
|
|
|
492
657
|
stdio: ["pipe", "pipe", "pipe"]
|
|
493
658
|
});
|
|
494
659
|
run.process = proc;
|
|
660
|
+
const logStream = this.openAgentLogStream(issue.identifier);
|
|
661
|
+
run.logStream = logStream;
|
|
662
|
+
const tee = new TeeTransform(logStream);
|
|
663
|
+
proc.stdout.pipe(tee);
|
|
664
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
495
665
|
let stderr = "";
|
|
496
666
|
let turnCompleted = false;
|
|
497
667
|
const timer = setTimeout(() => {
|
|
@@ -510,7 +680,7 @@ class Conductor {
|
|
|
510
680
|
proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
511
681
|
};
|
|
512
682
|
let lineBuffer = "";
|
|
513
|
-
|
|
683
|
+
tee.on("data", (chunk) => {
|
|
514
684
|
lineBuffer += chunk.toString();
|
|
515
685
|
const lines = lineBuffer.split("\n");
|
|
516
686
|
lineBuffer = lines.pop() || "";
|
|
@@ -519,11 +689,33 @@ class Conductor {
|
|
|
519
689
|
try {
|
|
520
690
|
const msg = JSON.parse(line);
|
|
521
691
|
this.handleAgentMessage(msg, issue, run);
|
|
692
|
+
const phase = inferPhase(msg);
|
|
693
|
+
if (phase) {
|
|
694
|
+
run.phase = phase;
|
|
695
|
+
}
|
|
696
|
+
if (msg.method === "item/commandExecution/started" || msg.method === "item/toolUse/started") {
|
|
697
|
+
run.toolCalls++;
|
|
698
|
+
}
|
|
699
|
+
const params = msg.params;
|
|
700
|
+
if (msg.method === "item/commandExecution/started" && params) {
|
|
701
|
+
const tool = (params.tool || params.name || "").toLowerCase();
|
|
702
|
+
if (tool.includes("edit") || tool.includes("write")) {
|
|
703
|
+
run.filesModified++;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (msg.method === "item/text" && params?.text) {
|
|
707
|
+
run.tokensUsed += Math.ceil(params.text.length / 4);
|
|
708
|
+
}
|
|
709
|
+
if (run.toolCalls % 5 === 0 || phase) {
|
|
710
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
711
|
+
}
|
|
522
712
|
if (msg.method === "turn/completed") {
|
|
523
713
|
turnCompleted = true;
|
|
714
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
524
715
|
}
|
|
525
716
|
if (msg.method === "turn/failed") {
|
|
526
717
|
turnCompleted = true;
|
|
718
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
527
719
|
const errMsg = msg.params?.error?.message || "Agent turn failed";
|
|
528
720
|
clearTimeout(timer);
|
|
529
721
|
reject(new Error(errMsg));
|
|
@@ -547,6 +739,10 @@ class Conductor {
|
|
|
547
739
|
proc.on("close", (code) => {
|
|
548
740
|
clearTimeout(timer);
|
|
549
741
|
run.process = null;
|
|
742
|
+
if (run.logStream && !run.logStream.destroyed) {
|
|
743
|
+
run.logStream.end();
|
|
744
|
+
}
|
|
745
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
550
746
|
if (turnCompleted) {
|
|
551
747
|
resolve();
|
|
552
748
|
} else if (code === 0) {
|
|
@@ -785,5 +981,6 @@ class Conductor {
|
|
|
785
981
|
}
|
|
786
982
|
}
|
|
787
983
|
export {
|
|
788
|
-
Conductor
|
|
984
|
+
Conductor,
|
|
985
|
+
getAgentStatusDir
|
|
789
986
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.5.3",
|
|
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",
|
|
7
7
|
"npm": ">=10.0.0"
|