@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.
@@ -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 command = params?.command || "";
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: 3,
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
- ...process.env,
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",
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",