@stackmemoryai/stackmemory 1.5.3 → 1.5.5
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 +104 -2
- package/dist/src/cli/commands/orchestrator.js +478 -7
- package/package.json +2 -1
- package/scripts/gepa/.before-optimize.md +111 -107
- package/scripts/gepa/generations/gen-000/baseline.md +111 -139
- package/scripts/gepa/generations/gen-001/baseline.md +111 -139
- package/scripts/gepa/generations/gen-001/variant-a.md +1 -176
- package/scripts/gepa/generations/gen-001/variant-b.md +1 -266
- package/scripts/gepa/generations/gen-001/variant-c.md +1 -142
- package/scripts/gepa/generations/gen-001/variant-d.md +1 -172
- package/scripts/gepa/results/eval-1-baseline.json +66 -66
- package/scripts/gepa/results/eval-1-variant-a.json +64 -64
- package/scripts/gepa/results/eval-1-variant-b.json +73 -73
- package/scripts/gepa/results/eval-1-variant-c.json +82 -62
- package/scripts/gepa/results/eval-1-variant-d.json +95 -75
- package/scripts/gepa/state.json +8 -24
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
mkdirSync,
|
|
9
9
|
rmSync,
|
|
10
10
|
writeFileSync,
|
|
11
|
+
readdirSync,
|
|
11
12
|
createWriteStream
|
|
12
13
|
} from "fs";
|
|
13
14
|
import { join, dirname } from "path";
|
|
14
15
|
import { tmpdir, homedir } from "os";
|
|
16
|
+
import { createReadStream } from "fs";
|
|
17
|
+
import { createInterface } from "readline";
|
|
15
18
|
import { fileURLToPath } from "url";
|
|
16
19
|
import { Transform } from "stream";
|
|
17
20
|
import { logger } from "../../core/monitoring/logger.js";
|
|
@@ -65,6 +68,35 @@ class TeeTransform extends Transform {
|
|
|
65
68
|
callback();
|
|
66
69
|
}
|
|
67
70
|
}
|
|
71
|
+
function inferPhaseFromStreamJson(event) {
|
|
72
|
+
if (event.type !== "assistant") return null;
|
|
73
|
+
const message = event.message;
|
|
74
|
+
const content = message?.content || [];
|
|
75
|
+
for (const block of content) {
|
|
76
|
+
if (block.type !== "tool_use") continue;
|
|
77
|
+
const toolLower = (block.name || "").toLowerCase();
|
|
78
|
+
if (toolLower.includes("read") || toolLower.includes("glob") || toolLower.includes("grep") || toolLower.includes("search")) {
|
|
79
|
+
return "reading";
|
|
80
|
+
}
|
|
81
|
+
if (toolLower.includes("todowrite") || toolLower.includes("todo")) {
|
|
82
|
+
return "planning";
|
|
83
|
+
}
|
|
84
|
+
if (toolLower.includes("edit") || toolLower.includes("write") || toolLower.includes("bash")) {
|
|
85
|
+
if (toolLower === "bash") {
|
|
86
|
+
const input = block.input;
|
|
87
|
+
const command = (input?.command ?? "").toLowerCase();
|
|
88
|
+
if (command.includes("git commit") || command.includes("git add")) {
|
|
89
|
+
return "committing";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return "implementing";
|
|
93
|
+
}
|
|
94
|
+
if (toolLower.includes("test")) {
|
|
95
|
+
return "testing";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
68
100
|
function inferPhase(msg) {
|
|
69
101
|
const method = msg.method;
|
|
70
102
|
const params = msg.params;
|
|
@@ -85,7 +117,8 @@ function inferPhase(msg) {
|
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
if (method === "item/commandExecution/started") {
|
|
88
|
-
const
|
|
120
|
+
const args = params?.arguments;
|
|
121
|
+
const command = (args?.command ?? params?.command) || "";
|
|
89
122
|
if (command.includes("git commit") || command.includes("git add")) {
|
|
90
123
|
return "committing";
|
|
91
124
|
}
|
|
@@ -98,7 +131,7 @@ const DEFAULT_CONFIG = {
|
|
|
98
131
|
inProgressState: "In Progress",
|
|
99
132
|
inReviewState: "In Review",
|
|
100
133
|
pollIntervalMs: 3e4,
|
|
101
|
-
maxConcurrent:
|
|
134
|
+
maxConcurrent: 5,
|
|
102
135
|
workspaceRoot: join(tmpdir(), "conductor_workspaces"),
|
|
103
136
|
repoRoot: process.cwd(),
|
|
104
137
|
baseBranch: "main",
|
|
@@ -110,7 +143,8 @@ const DEFAULT_CONFIG = {
|
|
|
110
143
|
),
|
|
111
144
|
turnTimeoutMs: 36e5,
|
|
112
145
|
maxRetries: 1,
|
|
113
|
-
hookTimeoutMs: 6e4
|
|
146
|
+
hookTimeoutMs: 6e4,
|
|
147
|
+
agentMode: "cli"
|
|
114
148
|
};
|
|
115
149
|
class Conductor {
|
|
116
150
|
config;
|
|
@@ -127,6 +161,23 @@ class Conductor {
|
|
|
127
161
|
stateCache = /* @__PURE__ */ new Map();
|
|
128
162
|
activeStatesLower;
|
|
129
163
|
terminalStatesLower;
|
|
164
|
+
/** Global rate limit backoff state */
|
|
165
|
+
rateLimit = {
|
|
166
|
+
inBackoff: false,
|
|
167
|
+
backoffUntil: 0,
|
|
168
|
+
totalHits: 0,
|
|
169
|
+
consecutiveHits: 0,
|
|
170
|
+
lastHitAt: 0
|
|
171
|
+
};
|
|
172
|
+
/** Aggregated usage stats */
|
|
173
|
+
usage = {
|
|
174
|
+
inputTokens: 0,
|
|
175
|
+
outputTokens: 0,
|
|
176
|
+
cacheCreationTokens: 0,
|
|
177
|
+
cacheReadTokens: 0,
|
|
178
|
+
estimatedMessages: 0,
|
|
179
|
+
perAgent: /* @__PURE__ */ new Map()
|
|
180
|
+
};
|
|
130
181
|
constructor(config = {}) {
|
|
131
182
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
132
183
|
this.activeStatesLower = this.config.activeStates.map(
|
|
@@ -263,7 +314,9 @@ class Conductor {
|
|
|
263
314
|
failed: this.failCount,
|
|
264
315
|
totalAttempts: this.totalAttempts,
|
|
265
316
|
uptime: Date.now() - this.startedAt,
|
|
266
|
-
issues
|
|
317
|
+
issues,
|
|
318
|
+
rateLimit: { ...this.rateLimit },
|
|
319
|
+
usage: { ...this.usage, perAgent: new Map(this.usage.perAgent) }
|
|
267
320
|
};
|
|
268
321
|
}
|
|
269
322
|
// ── Status File ──
|
|
@@ -293,7 +346,31 @@ class Conductor {
|
|
|
293
346
|
failed: this.failCount,
|
|
294
347
|
totalAttempts: this.totalAttempts,
|
|
295
348
|
maxConcurrent: this.config.maxConcurrent,
|
|
296
|
-
stopping: this.stopping
|
|
349
|
+
stopping: this.stopping,
|
|
350
|
+
rateLimit: {
|
|
351
|
+
inBackoff: this.rateLimit.inBackoff,
|
|
352
|
+
backoffUntil: this.rateLimit.backoffUntil,
|
|
353
|
+
backoffRemainingSec: this.rateLimit.inBackoff ? Math.max(
|
|
354
|
+
0,
|
|
355
|
+
Math.ceil((this.rateLimit.backoffUntil - Date.now()) / 1e3)
|
|
356
|
+
) : 0,
|
|
357
|
+
totalHits: this.rateLimit.totalHits
|
|
358
|
+
},
|
|
359
|
+
usage: (() => {
|
|
360
|
+
const summary = this.getUsageSummary();
|
|
361
|
+
return {
|
|
362
|
+
inputTokens: summary.inputTokens,
|
|
363
|
+
outputTokens: summary.outputTokens,
|
|
364
|
+
totalTokens: summary.totalTokens,
|
|
365
|
+
estimatedMessages: summary.estimatedMessages,
|
|
366
|
+
tokensPerMin: summary.tokensPerMin,
|
|
367
|
+
budgetPct5x: summary.budgetPct5x,
|
|
368
|
+
budgetPct20x: summary.budgetPct20x,
|
|
369
|
+
minutesRemaining5x: summary.minutesRemaining5x,
|
|
370
|
+
minutesRemaining20x: summary.minutesRemaining20x,
|
|
371
|
+
cacheHitRate: summary.cacheHitRate
|
|
372
|
+
};
|
|
373
|
+
})()
|
|
297
374
|
};
|
|
298
375
|
try {
|
|
299
376
|
writeFileSync(
|
|
@@ -359,6 +436,21 @@ class Conductor {
|
|
|
359
436
|
}
|
|
360
437
|
async poll() {
|
|
361
438
|
if (!this.client || this.stopping) return;
|
|
439
|
+
if (this.rateLimit.inBackoff) {
|
|
440
|
+
const remaining = this.rateLimit.backoffUntil - Date.now();
|
|
441
|
+
if (remaining > 0) {
|
|
442
|
+
logger.info("Rate limit backoff active, skipping poll", {
|
|
443
|
+
remainingMs: remaining,
|
|
444
|
+
remainingSec: Math.ceil(remaining / 1e3),
|
|
445
|
+
totalHits: this.rateLimit.totalHits
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
this.rateLimit.inBackoff = false;
|
|
450
|
+
this.rateLimit.backoffUntil = 0;
|
|
451
|
+
logger.info("Rate limit backoff expired, resuming dispatch");
|
|
452
|
+
console.log("[rate-limit] Backoff expired, resuming dispatch");
|
|
453
|
+
}
|
|
362
454
|
await this.reconcile();
|
|
363
455
|
const available = this.config.maxConcurrent - this.running.size;
|
|
364
456
|
if (available <= 0) {
|
|
@@ -376,6 +468,29 @@ class Conductor {
|
|
|
376
468
|
if (eligible.length === 0) return;
|
|
377
469
|
const sorted = eligible.sort((a, b) => (a.priority || 4) - (b.priority || 4)).slice(0, available);
|
|
378
470
|
const toDispatch = this.preflightFilter(sorted);
|
|
471
|
+
if (this.usage.inputTokens > 0) {
|
|
472
|
+
const summary = this.getUsageSummary();
|
|
473
|
+
logger.info("Usage summary", {
|
|
474
|
+
totalTokens: summary.totalTokens,
|
|
475
|
+
estimatedMessages: summary.estimatedMessages,
|
|
476
|
+
tokensPerMin: summary.tokensPerMin,
|
|
477
|
+
budgetPct5x: `${summary.budgetPct5x}%`,
|
|
478
|
+
budgetPct20x: `${summary.budgetPct20x}%`,
|
|
479
|
+
minutesRemaining5x: summary.minutesRemaining5x,
|
|
480
|
+
minutesRemaining20x: summary.minutesRemaining20x,
|
|
481
|
+
rateLimitHits: this.rateLimit.totalHits
|
|
482
|
+
});
|
|
483
|
+
if (summary.budgetPct5x >= 75 && summary.budgetPct5x < 100) {
|
|
484
|
+
console.log(
|
|
485
|
+
`[usage] \u26A0 ${summary.budgetPct5x}% of Max 5x budget used \u2014 ~${summary.minutesRemaining5x}min remaining at ${summary.tokensPerMin} tok/min`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (summary.budgetPct5x >= 100) {
|
|
489
|
+
console.log(
|
|
490
|
+
`[usage] \u{1F6D1} Max 5x budget likely exhausted (${summary.estimatedMessages} est. messages / 225 limit). Expect 429s.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
379
494
|
logger.info("Dispatching issues", {
|
|
380
495
|
count: toDispatch.length,
|
|
381
496
|
identifiers: toDispatch.map((i) => i.identifier),
|
|
@@ -538,6 +653,9 @@ class Conductor {
|
|
|
538
653
|
error: run.error,
|
|
539
654
|
attempt: run.attempt
|
|
540
655
|
});
|
|
656
|
+
if (this.handleRateLimitError(run.error, issue.identifier)) {
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
541
659
|
if (run.workspacePath) {
|
|
542
660
|
await this.runHook(
|
|
543
661
|
"after-run",
|
|
@@ -564,6 +682,179 @@ class Conductor {
|
|
|
564
682
|
}
|
|
565
683
|
}
|
|
566
684
|
}
|
|
685
|
+
// ── Rate Limit Detection ──
|
|
686
|
+
/**
|
|
687
|
+
* Check if an error indicates a rate limit (429) or usage cap.
|
|
688
|
+
* Triggers global backoff so no new agents are dispatched.
|
|
689
|
+
*
|
|
690
|
+
* Claude Max limits (approximate, shared between claude.ai + Claude Code):
|
|
691
|
+
* - Max 5x: ~225 messages per 5h window
|
|
692
|
+
* - Max 20x: ~900 messages per 5h window
|
|
693
|
+
* Messages are token-weighted: heavy agent usage ≈ 5-10x a casual message.
|
|
694
|
+
*/
|
|
695
|
+
handleRateLimitError(error, identifier) {
|
|
696
|
+
const rateLimitPatterns = [
|
|
697
|
+
"usage limits",
|
|
698
|
+
"rate_limit",
|
|
699
|
+
"rate limit",
|
|
700
|
+
"overloaded",
|
|
701
|
+
"429",
|
|
702
|
+
"too many requests",
|
|
703
|
+
"quota exceeded",
|
|
704
|
+
"capacity"
|
|
705
|
+
];
|
|
706
|
+
const isRateLimit = rateLimitPatterns.some(
|
|
707
|
+
(p) => error.toLowerCase().includes(p)
|
|
708
|
+
);
|
|
709
|
+
if (!isRateLimit) return false;
|
|
710
|
+
this.rateLimit.totalHits++;
|
|
711
|
+
this.rateLimit.lastHitAt = Date.now();
|
|
712
|
+
const timeSinceLast = Date.now() - this.rateLimit.lastHitAt;
|
|
713
|
+
if (timeSinceLast < 6e5) {
|
|
714
|
+
this.rateLimit.consecutiveHits++;
|
|
715
|
+
} else {
|
|
716
|
+
this.rateLimit.consecutiveHits = 1;
|
|
717
|
+
}
|
|
718
|
+
const backoffSec = Math.min(
|
|
719
|
+
60 * Math.pow(2, this.rateLimit.consecutiveHits - 1),
|
|
720
|
+
900
|
|
721
|
+
);
|
|
722
|
+
this.rateLimit.inBackoff = true;
|
|
723
|
+
this.rateLimit.backoffUntil = Date.now() + backoffSec * 1e3;
|
|
724
|
+
logger.warn("Rate limit hit \u2014 global backoff", {
|
|
725
|
+
identifier,
|
|
726
|
+
backoffSec,
|
|
727
|
+
consecutiveHits: this.rateLimit.consecutiveHits,
|
|
728
|
+
totalHits: this.rateLimit.totalHits,
|
|
729
|
+
error: error.slice(0, 200)
|
|
730
|
+
});
|
|
731
|
+
console.log(
|
|
732
|
+
`[rate-limit] Hit rate limit on ${identifier} \u2014 backing off ${backoffSec}s (hit #${this.rateLimit.totalHits})`
|
|
733
|
+
);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
// ── Usage Tracking ──
|
|
737
|
+
/**
|
|
738
|
+
* Track token usage from a stream-json event (CLI mode) or JSON-RPC message (adapter mode).
|
|
739
|
+
* Updates both global and per-agent counters.
|
|
740
|
+
*/
|
|
741
|
+
trackUsage(identifier, usage) {
|
|
742
|
+
const input = usage.input_tokens || 0;
|
|
743
|
+
const output = usage.output_tokens || 0;
|
|
744
|
+
const cacheCreate = usage.cache_creation_input_tokens || 0;
|
|
745
|
+
const cacheRead = usage.cache_read_input_tokens || 0;
|
|
746
|
+
this.usage.inputTokens += input;
|
|
747
|
+
this.usage.outputTokens += output;
|
|
748
|
+
this.usage.cacheCreationTokens += cacheCreate;
|
|
749
|
+
this.usage.cacheReadTokens += cacheRead;
|
|
750
|
+
this.usage.estimatedMessages = Math.ceil(
|
|
751
|
+
(this.usage.inputTokens + this.usage.outputTokens) / 1e4
|
|
752
|
+
);
|
|
753
|
+
const agent = this.usage.perAgent.get(identifier) || {
|
|
754
|
+
inputTokens: 0,
|
|
755
|
+
outputTokens: 0
|
|
756
|
+
};
|
|
757
|
+
agent.inputTokens += input;
|
|
758
|
+
agent.outputTokens += output;
|
|
759
|
+
this.usage.perAgent.set(identifier, agent);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Scan Claude Code JSONL logs for token usage from conductor-spawned sessions.
|
|
763
|
+
* Reads logs from ~/.claude/projects/ matching conductor workspace paths.
|
|
764
|
+
*/
|
|
765
|
+
async scanUsageLogs() {
|
|
766
|
+
const logDirs = [
|
|
767
|
+
join(homedir(), ".claude", "projects"),
|
|
768
|
+
join(homedir(), ".config", "claude", "projects")
|
|
769
|
+
];
|
|
770
|
+
for (const logDir of logDirs) {
|
|
771
|
+
if (!existsSync(logDir)) continue;
|
|
772
|
+
try {
|
|
773
|
+
const entries = readdirSync(logDir);
|
|
774
|
+
const conductorDirs = entries.filter(
|
|
775
|
+
(e) => e.includes("conductor_workspaces") || e.includes("conductor-")
|
|
776
|
+
);
|
|
777
|
+
for (const dir of conductorDirs) {
|
|
778
|
+
const projectDir = join(logDir, dir);
|
|
779
|
+
if (!existsSync(projectDir)) continue;
|
|
780
|
+
const jsonlFiles = readdirSync(projectDir).filter(
|
|
781
|
+
(f) => f.endsWith(".jsonl")
|
|
782
|
+
);
|
|
783
|
+
for (const file of jsonlFiles) {
|
|
784
|
+
await this.parseUsageFromJsonl(join(projectDir, file));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
logger.debug("Usage log scan failed", {
|
|
789
|
+
error: err.message
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return this.usage;
|
|
794
|
+
}
|
|
795
|
+
async parseUsageFromJsonl(filePath) {
|
|
796
|
+
try {
|
|
797
|
+
const rl = createInterface({
|
|
798
|
+
input: createReadStream(filePath),
|
|
799
|
+
crlfDelay: Infinity
|
|
800
|
+
});
|
|
801
|
+
for await (const line of rl) {
|
|
802
|
+
if (!line.trim()) continue;
|
|
803
|
+
try {
|
|
804
|
+
const entry = JSON.parse(line);
|
|
805
|
+
if (entry.type === "assistant" && entry.message?.usage) {
|
|
806
|
+
const usage = entry.message.usage;
|
|
807
|
+
const identifier = entry.sessionId?.slice(0, 8) || "unknown";
|
|
808
|
+
this.trackUsage(identifier, usage);
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get current usage summary with time-to-exhaustion estimate.
|
|
818
|
+
*
|
|
819
|
+
* Claude Max limits (approximate, shared between claude.ai + Claude Code):
|
|
820
|
+
* - Max 5x: ~225 messages per 5h window (~10k tokens per "message")
|
|
821
|
+
* - Max 20x: ~900 messages per 5h window
|
|
822
|
+
*
|
|
823
|
+
* Heavy agent usage burns 5-10x faster than casual chat.
|
|
824
|
+
*/
|
|
825
|
+
getUsageSummary() {
|
|
826
|
+
const totalCache = this.usage.cacheCreationTokens + this.usage.cacheReadTokens;
|
|
827
|
+
const cacheHitRate = totalCache > 0 ? this.usage.cacheReadTokens / totalCache : 0;
|
|
828
|
+
const uptimeMin = Math.max(1, (Date.now() - this.startedAt) / 6e4);
|
|
829
|
+
const totalTokens = this.usage.inputTokens + this.usage.outputTokens;
|
|
830
|
+
const tokensPerMin = Math.round(totalTokens / uptimeMin);
|
|
831
|
+
const estMessages = this.usage.estimatedMessages;
|
|
832
|
+
const MAX_5X_MESSAGES = 225;
|
|
833
|
+
const MAX_20X_MESSAGES = 900;
|
|
834
|
+
const budgetPct5x = Math.round(estMessages / MAX_5X_MESSAGES * 100);
|
|
835
|
+
const budgetPct20x = Math.round(estMessages / MAX_20X_MESSAGES * 100);
|
|
836
|
+
const msgsPerMin = tokensPerMin > 0 ? tokensPerMin / 1e4 : 0;
|
|
837
|
+
const minutesRemaining5x = msgsPerMin > 0 ? Math.round((MAX_5X_MESSAGES - estMessages) / msgsPerMin) : -1;
|
|
838
|
+
const minutesRemaining20x = msgsPerMin > 0 ? Math.round((MAX_20X_MESSAGES - estMessages) / msgsPerMin) : -1;
|
|
839
|
+
return {
|
|
840
|
+
totalTokens,
|
|
841
|
+
inputTokens: this.usage.inputTokens,
|
|
842
|
+
outputTokens: this.usage.outputTokens,
|
|
843
|
+
estimatedMessages: estMessages,
|
|
844
|
+
cacheHitRate: Math.round(cacheHitRate * 100),
|
|
845
|
+
tokensPerMin,
|
|
846
|
+
budgetPct5x,
|
|
847
|
+
budgetPct20x,
|
|
848
|
+
minutesRemaining5x: minutesRemaining5x < 0 ? -1 : minutesRemaining5x,
|
|
849
|
+
minutesRemaining20x: minutesRemaining20x < 0 ? -1 : minutesRemaining20x,
|
|
850
|
+
perAgent: Array.from(this.usage.perAgent.entries()).map(
|
|
851
|
+
([id, stats]) => ({
|
|
852
|
+
id,
|
|
853
|
+
...stats
|
|
854
|
+
})
|
|
855
|
+
)
|
|
856
|
+
};
|
|
857
|
+
}
|
|
567
858
|
// ── Workspace Management ──
|
|
568
859
|
async createWorkspace(issue) {
|
|
569
860
|
const wsKey = this.sanitizeIdentifier(issue.identifier);
|
|
@@ -641,14 +932,194 @@ class Conductor {
|
|
|
641
932
|
sanitizeIdentifier(identifier) {
|
|
642
933
|
return identifier.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
643
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Find the real claude binary, skipping shell wrappers (cmux, claude-smd).
|
|
937
|
+
* Wrappers inject --settings with hooks that block headless -p mode.
|
|
938
|
+
*/
|
|
939
|
+
findClaudeBinary() {
|
|
940
|
+
const candidates = [
|
|
941
|
+
join(homedir(), ".local", "bin", "claude"),
|
|
942
|
+
"/usr/local/bin/claude",
|
|
943
|
+
"/opt/homebrew/bin/claude"
|
|
944
|
+
];
|
|
945
|
+
for (const c of candidates) {
|
|
946
|
+
if (existsSync(c)) return c;
|
|
947
|
+
}
|
|
948
|
+
return "claude";
|
|
949
|
+
}
|
|
644
950
|
// ── Agent Execution ──
|
|
645
|
-
runAgent(issue, run) {
|
|
951
|
+
async runAgent(issue, run) {
|
|
952
|
+
if (this.config.agentMode === "cli") {
|
|
953
|
+
try {
|
|
954
|
+
return await this.runAgentCLI(issue, run);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
const msg = err.message || "";
|
|
957
|
+
if (this.handleRateLimitError(msg, issue.identifier)) {
|
|
958
|
+
throw err;
|
|
959
|
+
}
|
|
960
|
+
if (msg.includes("usage limits") || msg.includes("overloaded")) {
|
|
961
|
+
logger.warn("CLI mode hit limits, falling back to adapter", {
|
|
962
|
+
identifier: issue.identifier,
|
|
963
|
+
error: msg.slice(0, 200)
|
|
964
|
+
});
|
|
965
|
+
run.toolCalls = 0;
|
|
966
|
+
run.filesModified = 0;
|
|
967
|
+
run.tokensUsed = 0;
|
|
968
|
+
run.phase = "reading";
|
|
969
|
+
return this.runAgentAdapter(issue, run);
|
|
970
|
+
}
|
|
971
|
+
throw err;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return this.runAgentAdapter(issue, run);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* CLI mode: spawn `claude -p --output-format stream-json` directly.
|
|
978
|
+
* Uses whatever auth the environment provides (session quota, API key, etc).
|
|
979
|
+
*/
|
|
980
|
+
runAgentCLI(issue, run) {
|
|
646
981
|
return new Promise((resolve, reject) => {
|
|
647
982
|
const prompt = this.buildPrompt(issue, run.attempt);
|
|
983
|
+
const claudeBin = this.findClaudeBinary();
|
|
984
|
+
const proc = spawn(
|
|
985
|
+
claudeBin,
|
|
986
|
+
[
|
|
987
|
+
"-p",
|
|
988
|
+
"--output-format",
|
|
989
|
+
"stream-json",
|
|
990
|
+
"--dangerously-skip-permissions",
|
|
991
|
+
"--settings",
|
|
992
|
+
'{"hooks":{}}',
|
|
993
|
+
prompt
|
|
994
|
+
],
|
|
995
|
+
{
|
|
996
|
+
cwd: run.workspacePath,
|
|
997
|
+
env: (() => {
|
|
998
|
+
const env = { ...process.env };
|
|
999
|
+
delete env.CLAUDECODE;
|
|
1000
|
+
delete env.ANTHROPIC_API_KEY;
|
|
1001
|
+
return {
|
|
1002
|
+
...env,
|
|
1003
|
+
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
1004
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
1005
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
1006
|
+
SYMPHONY_ATTEMPT: String(run.attempt)
|
|
1007
|
+
};
|
|
1008
|
+
})(),
|
|
1009
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
run.process = proc;
|
|
1013
|
+
proc.stdin.end();
|
|
1014
|
+
const logStream = this.openAgentLogStream(issue.identifier);
|
|
1015
|
+
run.logStream = logStream;
|
|
1016
|
+
const tee = new TeeTransform(logStream);
|
|
1017
|
+
proc.stdout.pipe(tee);
|
|
1018
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
1019
|
+
let stderr = "";
|
|
1020
|
+
let lastResultText = "";
|
|
1021
|
+
const timer = setTimeout(() => {
|
|
1022
|
+
logger.warn("Agent turn timeout (cli)", {
|
|
1023
|
+
identifier: issue.identifier,
|
|
1024
|
+
timeoutMs: this.config.turnTimeoutMs
|
|
1025
|
+
});
|
|
1026
|
+
proc.kill("SIGTERM");
|
|
1027
|
+
reject(new Error(`Agent timeout after ${this.config.turnTimeoutMs}ms`));
|
|
1028
|
+
}, this.config.turnTimeoutMs);
|
|
1029
|
+
let lineBuffer = "";
|
|
1030
|
+
tee.on("data", (chunk) => {
|
|
1031
|
+
lineBuffer += chunk.toString();
|
|
1032
|
+
const lines = lineBuffer.split("\n");
|
|
1033
|
+
lineBuffer = lines.pop() || "";
|
|
1034
|
+
for (const line of lines) {
|
|
1035
|
+
if (!line.trim()) continue;
|
|
1036
|
+
try {
|
|
1037
|
+
const event = JSON.parse(line);
|
|
1038
|
+
const phase = inferPhaseFromStreamJson(event);
|
|
1039
|
+
if (phase) {
|
|
1040
|
+
run.phase = phase;
|
|
1041
|
+
}
|
|
1042
|
+
if (event.type === "assistant") {
|
|
1043
|
+
const message = event.message;
|
|
1044
|
+
const msgUsage = message?.usage;
|
|
1045
|
+
if (msgUsage) {
|
|
1046
|
+
this.trackUsage(issue.identifier, msgUsage);
|
|
1047
|
+
}
|
|
1048
|
+
const content = message?.content || [];
|
|
1049
|
+
for (const block of content) {
|
|
1050
|
+
if (block.type === "tool_use") {
|
|
1051
|
+
run.toolCalls++;
|
|
1052
|
+
const toolLower = (block.name || "").toLowerCase();
|
|
1053
|
+
if (toolLower.includes("edit") || toolLower.includes("write")) {
|
|
1054
|
+
run.filesModified++;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (block.type === "text" && block.text) {
|
|
1058
|
+
run.tokensUsed += Math.ceil(
|
|
1059
|
+
block.text.length / 4
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (event.type === "result" && event.result) {
|
|
1065
|
+
lastResultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
1066
|
+
}
|
|
1067
|
+
if (run.toolCalls % 5 === 0 || phase) {
|
|
1068
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
1069
|
+
}
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
proc.stderr.on("data", (data) => {
|
|
1075
|
+
stderr += data.toString();
|
|
1076
|
+
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
1077
|
+
for (const line of lines) {
|
|
1078
|
+
logger.debug(`[${issue.identifier}] ${line}`);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
proc.on("error", (err) => {
|
|
1082
|
+
clearTimeout(timer);
|
|
1083
|
+
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
1084
|
+
});
|
|
1085
|
+
proc.on("close", (code) => {
|
|
1086
|
+
clearTimeout(timer);
|
|
1087
|
+
run.process = null;
|
|
1088
|
+
if (run.logStream && !run.logStream.destroyed) {
|
|
1089
|
+
run.logStream.end();
|
|
1090
|
+
}
|
|
1091
|
+
this.writeAgentStatus(issue.identifier, run);
|
|
1092
|
+
if (code === 0) {
|
|
1093
|
+
logger.info("Agent completed (cli)", {
|
|
1094
|
+
identifier: issue.identifier,
|
|
1095
|
+
toolCalls: run.toolCalls,
|
|
1096
|
+
resultLength: lastResultText.length
|
|
1097
|
+
});
|
|
1098
|
+
resolve();
|
|
1099
|
+
} else {
|
|
1100
|
+
reject(
|
|
1101
|
+
new Error(
|
|
1102
|
+
`Claude exited with code ${code}: ${stderr.slice(0, 500)}`
|
|
1103
|
+
)
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Adapter mode: spawn claude-app-server.cjs via JSON-RPC protocol.
|
|
1111
|
+
* Uses ANTHROPIC_API_KEY for auth.
|
|
1112
|
+
*/
|
|
1113
|
+
runAgentAdapter(issue, run) {
|
|
1114
|
+
return new Promise((resolve, reject) => {
|
|
1115
|
+
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1116
|
+
const env = { ...process.env };
|
|
1117
|
+
delete env.CLAUDECODE;
|
|
1118
|
+
delete env.ANTHROPIC_API_KEY;
|
|
648
1119
|
const proc = spawn("node", [this.config.appServerPath], {
|
|
649
1120
|
cwd: run.workspacePath,
|
|
650
1121
|
env: {
|
|
651
|
-
...
|
|
1122
|
+
...env,
|
|
652
1123
|
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
653
1124
|
SYMPHONY_ISSUE_ID: issue.id,
|
|
654
1125
|
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
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",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"postinstall": "node scripts/install-claude-hooks-auto.js || true",
|
|
101
101
|
"init": "node dist/scripts/initialize.js",
|
|
102
102
|
"build": "rm -rf dist && node esbuild.config.js",
|
|
103
|
+
"typecheck": "NODE_OPTIONS='--max-old-space-size=8192' tsc --noEmit",
|
|
103
104
|
"lint": "eslint 'src/**/*.ts' 'scripts/**/*.ts'",
|
|
104
105
|
"lint:fix": "eslint 'src/**/*.ts' 'scripts/**/*.ts' --fix --max-warnings=-1",
|
|
105
106
|
"lint:fast": "oxlint src scripts",
|