@stackmemoryai/stackmemory 1.5.4 → 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 +97 -0
- package/dist/src/cli/commands/orchestrator.js +291 -5
- package/package.json +2 -1
- package/scripts/gepa/.before-optimize.md +110 -142
- package/scripts/gepa/generations/gen-000/baseline.md +111 -147
- package/scripts/gepa/generations/gen-001/baseline.md +111 -147
- package/scripts/gepa/generations/gen-001/variant-a.md +1 -176
- package/scripts/gepa/generations/gen-001/variant-b.md +1 -280
- package/scripts/gepa/generations/gen-001/variant-c.md +1 -122
- package/scripts/gepa/generations/gen-001/variant-d.md +1 -138
- package/scripts/gepa/results/eval-1-baseline.json +80 -80
- package/scripts/gepa/results/eval-1-variant-a.json +65 -65
- package/scripts/gepa/results/eval-1-variant-b.json +78 -78
- package/scripts/gepa/results/eval-1-variant-c.json +69 -69
- package/scripts/gepa/results/eval-1-variant-d.json +71 -71
- package/scripts/gepa/state.json +6 -22
|
@@ -60,6 +60,55 @@ function formatElapsed(ms) {
|
|
|
60
60
|
const days = Math.floor(hours / 24);
|
|
61
61
|
return `${days}d ago`;
|
|
62
62
|
}
|
|
63
|
+
function fmtTokens(n) {
|
|
64
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
65
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
66
|
+
return String(n);
|
|
67
|
+
}
|
|
68
|
+
function budgetBar(pct, width = 30) {
|
|
69
|
+
const filled = Math.min(Math.round(pct / 100 * width), width);
|
|
70
|
+
const empty = width - filled;
|
|
71
|
+
const color = pct >= 75 ? "\x1B[31m" : pct >= 50 ? "\x1B[33m" : "\x1B[32m";
|
|
72
|
+
const dim = "\x1B[2m";
|
|
73
|
+
const rst = "\x1B[0m";
|
|
74
|
+
return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
|
|
75
|
+
}
|
|
76
|
+
function fmtMinutes(m) {
|
|
77
|
+
if (m < 0) return "N/A";
|
|
78
|
+
if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
79
|
+
return `${m}m`;
|
|
80
|
+
}
|
|
81
|
+
function printUsageSummary(u) {
|
|
82
|
+
const totalTokens = u.totalTokens || 0;
|
|
83
|
+
const inputTokens = u.inputTokens || 0;
|
|
84
|
+
const outputTokens = u.outputTokens || 0;
|
|
85
|
+
const estMessages = u.estimatedMessages || 0;
|
|
86
|
+
const tokensPerMin = u.tokensPerMin || 0;
|
|
87
|
+
const budgetPct5x = u.budgetPct5x || 0;
|
|
88
|
+
const budgetPct20x = u.budgetPct20x || 0;
|
|
89
|
+
const mins5x = u.minutesRemaining5x ?? -1;
|
|
90
|
+
const mins20x = u.minutesRemaining20x ?? -1;
|
|
91
|
+
const cacheHitRate = u.cacheHitRate || 0;
|
|
92
|
+
const b = "\x1B[1m";
|
|
93
|
+
const d = "\x1B[2m";
|
|
94
|
+
const w = "\x1B[37m";
|
|
95
|
+
const r = "\x1B[0m";
|
|
96
|
+
console.log(`${b}Token Usage${r}`);
|
|
97
|
+
console.log(
|
|
98
|
+
` Input ${w}${fmtTokens(inputTokens)}${r} ${d}|${r} Output ${w}${fmtTokens(outputTokens)}${r} ${d}|${r} Total ${w}${fmtTokens(totalTokens)}${r}`
|
|
99
|
+
);
|
|
100
|
+
console.log(
|
|
101
|
+
` Rate ${w}${fmtTokens(tokensPerMin)}/min${r} ${d}|${r} Messages ${w}${estMessages}${r} ${d}|${r} Cache hit ${w}${cacheHitRate}%${r}`
|
|
102
|
+
);
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(`${b}Budget (Max plan, 5h window)${r}`);
|
|
105
|
+
console.log(
|
|
106
|
+
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${d}~${fmtMinutes(mins5x)} left${r}`
|
|
107
|
+
);
|
|
108
|
+
console.log(
|
|
109
|
+
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${d}~${fmtMinutes(mins20x)} left${r}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
63
112
|
function createConductorCommands() {
|
|
64
113
|
const cmd = new Command("conductor");
|
|
65
114
|
cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
|
|
@@ -334,6 +383,54 @@ function createConductorCommands() {
|
|
|
334
383
|
process.on("SIGTERM", forward);
|
|
335
384
|
});
|
|
336
385
|
});
|
|
386
|
+
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) => {
|
|
387
|
+
const statusPath = join(
|
|
388
|
+
process.cwd(),
|
|
389
|
+
".stackmemory",
|
|
390
|
+
"conductor-status.json"
|
|
391
|
+
);
|
|
392
|
+
if (options.scan) {
|
|
393
|
+
const conductor = new Conductor({ repoRoot: process.cwd() });
|
|
394
|
+
await conductor.scanUsageLogs();
|
|
395
|
+
const summary = conductor.getUsageSummary();
|
|
396
|
+
if (options.json) {
|
|
397
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
printUsageSummary(summary);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (!existsSync(statusPath)) {
|
|
404
|
+
console.log(
|
|
405
|
+
"No conductor-status.json found. Run with --scan to check JSONL logs, or start the conductor first."
|
|
406
|
+
);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
411
|
+
const usage = data.usage || {};
|
|
412
|
+
if (options.json) {
|
|
413
|
+
console.log(JSON.stringify(usage, null, 2));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
printUsageSummary(usage);
|
|
417
|
+
const rl = data.rateLimit;
|
|
418
|
+
if (rl) {
|
|
419
|
+
console.log("");
|
|
420
|
+
if (rl.inBackoff) {
|
|
421
|
+
console.log(
|
|
422
|
+
`Rate limit: \x1B[31mBACKOFF\x1B[0m (${rl.backoffRemainingSec}s remaining, ${rl.totalHits} total hits)`
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
console.log(
|
|
426
|
+
`Rate limit: \x1B[32mOK\x1B[0m (${rl.totalHits} total hits)`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error("Failed to read status file:", err.message);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
337
434
|
cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
|
|
338
435
|
"--states <states>",
|
|
339
436
|
"Comma-separated issue states to pick up",
|
|
@@ -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";
|
|
@@ -128,7 +131,7 @@ const DEFAULT_CONFIG = {
|
|
|
128
131
|
inProgressState: "In Progress",
|
|
129
132
|
inReviewState: "In Review",
|
|
130
133
|
pollIntervalMs: 3e4,
|
|
131
|
-
maxConcurrent:
|
|
134
|
+
maxConcurrent: 5,
|
|
132
135
|
workspaceRoot: join(tmpdir(), "conductor_workspaces"),
|
|
133
136
|
repoRoot: process.cwd(),
|
|
134
137
|
baseBranch: "main",
|
|
@@ -158,6 +161,23 @@ class Conductor {
|
|
|
158
161
|
stateCache = /* @__PURE__ */ new Map();
|
|
159
162
|
activeStatesLower;
|
|
160
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
|
+
};
|
|
161
181
|
constructor(config = {}) {
|
|
162
182
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
163
183
|
this.activeStatesLower = this.config.activeStates.map(
|
|
@@ -294,7 +314,9 @@ class Conductor {
|
|
|
294
314
|
failed: this.failCount,
|
|
295
315
|
totalAttempts: this.totalAttempts,
|
|
296
316
|
uptime: Date.now() - this.startedAt,
|
|
297
|
-
issues
|
|
317
|
+
issues,
|
|
318
|
+
rateLimit: { ...this.rateLimit },
|
|
319
|
+
usage: { ...this.usage, perAgent: new Map(this.usage.perAgent) }
|
|
298
320
|
};
|
|
299
321
|
}
|
|
300
322
|
// ── Status File ──
|
|
@@ -324,7 +346,31 @@ class Conductor {
|
|
|
324
346
|
failed: this.failCount,
|
|
325
347
|
totalAttempts: this.totalAttempts,
|
|
326
348
|
maxConcurrent: this.config.maxConcurrent,
|
|
327
|
-
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
|
+
})()
|
|
328
374
|
};
|
|
329
375
|
try {
|
|
330
376
|
writeFileSync(
|
|
@@ -390,6 +436,21 @@ class Conductor {
|
|
|
390
436
|
}
|
|
391
437
|
async poll() {
|
|
392
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
|
+
}
|
|
393
454
|
await this.reconcile();
|
|
394
455
|
const available = this.config.maxConcurrent - this.running.size;
|
|
395
456
|
if (available <= 0) {
|
|
@@ -407,6 +468,29 @@ class Conductor {
|
|
|
407
468
|
if (eligible.length === 0) return;
|
|
408
469
|
const sorted = eligible.sort((a, b) => (a.priority || 4) - (b.priority || 4)).slice(0, available);
|
|
409
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
|
+
}
|
|
410
494
|
logger.info("Dispatching issues", {
|
|
411
495
|
count: toDispatch.length,
|
|
412
496
|
identifiers: toDispatch.map((i) => i.identifier),
|
|
@@ -569,6 +653,9 @@ class Conductor {
|
|
|
569
653
|
error: run.error,
|
|
570
654
|
attempt: run.attempt
|
|
571
655
|
});
|
|
656
|
+
if (this.handleRateLimitError(run.error, issue.identifier)) {
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
572
659
|
if (run.workspacePath) {
|
|
573
660
|
await this.runHook(
|
|
574
661
|
"after-run",
|
|
@@ -595,6 +682,179 @@ class Conductor {
|
|
|
595
682
|
}
|
|
596
683
|
}
|
|
597
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
|
+
}
|
|
598
858
|
// ── Workspace Management ──
|
|
599
859
|
async createWorkspace(issue) {
|
|
600
860
|
const wsKey = this.sanitizeIdentifier(issue.identifier);
|
|
@@ -672,6 +932,21 @@ class Conductor {
|
|
|
672
932
|
sanitizeIdentifier(identifier) {
|
|
673
933
|
return identifier.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
674
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
|
+
}
|
|
675
950
|
// ── Agent Execution ──
|
|
676
951
|
async runAgent(issue, run) {
|
|
677
952
|
if (this.config.agentMode === "cli") {
|
|
@@ -679,7 +954,10 @@ class Conductor {
|
|
|
679
954
|
return await this.runAgentCLI(issue, run);
|
|
680
955
|
} catch (err) {
|
|
681
956
|
const msg = err.message || "";
|
|
682
|
-
if (
|
|
957
|
+
if (this.handleRateLimitError(msg, issue.identifier)) {
|
|
958
|
+
throw err;
|
|
959
|
+
}
|
|
960
|
+
if (msg.includes("usage limits") || msg.includes("overloaded")) {
|
|
683
961
|
logger.warn("CLI mode hit limits, falling back to adapter", {
|
|
684
962
|
identifier: issue.identifier,
|
|
685
963
|
error: msg.slice(0, 200)
|
|
@@ -702,13 +980,16 @@ class Conductor {
|
|
|
702
980
|
runAgentCLI(issue, run) {
|
|
703
981
|
return new Promise((resolve, reject) => {
|
|
704
982
|
const prompt = this.buildPrompt(issue, run.attempt);
|
|
983
|
+
const claudeBin = this.findClaudeBinary();
|
|
705
984
|
const proc = spawn(
|
|
706
|
-
|
|
985
|
+
claudeBin,
|
|
707
986
|
[
|
|
708
987
|
"-p",
|
|
709
988
|
"--output-format",
|
|
710
989
|
"stream-json",
|
|
711
990
|
"--dangerously-skip-permissions",
|
|
991
|
+
"--settings",
|
|
992
|
+
'{"hooks":{}}',
|
|
712
993
|
prompt
|
|
713
994
|
],
|
|
714
995
|
{
|
|
@@ -729,6 +1010,7 @@ class Conductor {
|
|
|
729
1010
|
}
|
|
730
1011
|
);
|
|
731
1012
|
run.process = proc;
|
|
1013
|
+
proc.stdin.end();
|
|
732
1014
|
const logStream = this.openAgentLogStream(issue.identifier);
|
|
733
1015
|
run.logStream = logStream;
|
|
734
1016
|
const tee = new TeeTransform(logStream);
|
|
@@ -759,6 +1041,10 @@ class Conductor {
|
|
|
759
1041
|
}
|
|
760
1042
|
if (event.type === "assistant") {
|
|
761
1043
|
const message = event.message;
|
|
1044
|
+
const msgUsage = message?.usage;
|
|
1045
|
+
if (msgUsage) {
|
|
1046
|
+
this.trackUsage(issue.identifier, msgUsage);
|
|
1047
|
+
}
|
|
762
1048
|
const content = message?.content || [];
|
|
763
1049
|
for (const block of content) {
|
|
764
1050
|
if (block.type === "tool_use") {
|
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",
|