replicas-engine 0.1.35 → 0.1.37

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.
Files changed (2) hide show
  1. package/dist/src/index.js +183 -6
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -716,6 +716,21 @@ var MessageQueue = class {
716
716
  clearQueue() {
717
717
  this.queue = [];
718
718
  }
719
+ drainQueue(options) {
720
+ const maxItems = options?.maxItems ?? this.queue.length;
721
+ const maxChars = options?.maxChars ?? Number.POSITIVE_INFINITY;
722
+ const drained = [];
723
+ let totalChars = 0;
724
+ for (const message of this.queue) {
725
+ if (drained.length >= maxItems) break;
726
+ const text = message.message;
727
+ if (totalChars + text.length > maxChars) break;
728
+ drained.push(text);
729
+ totalChars += text.length;
730
+ }
731
+ this.queue = [];
732
+ return drained;
733
+ }
719
734
  /**
720
735
  * Reset everything including clearing processing state
721
736
  */
@@ -1016,6 +1031,8 @@ var replicasConfigService = new ReplicasConfigService();
1016
1031
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
1017
1032
  var DEFAULT_MODEL = "gpt-5.2-codex";
1018
1033
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
1034
+ var MAX_INTERRUPT_QUEUE_ITEMS = 1e3;
1035
+ var MAX_INTERRUPT_QUEUE_CHARS = 2e5;
1019
1036
  var CodexManager = class {
1020
1037
  codex;
1021
1038
  currentThreadId = null;
@@ -1025,6 +1042,7 @@ var CodexManager = class {
1025
1042
  baseSystemPrompt;
1026
1043
  tempImageDir;
1027
1044
  initialized;
1045
+ activeAbortController = null;
1028
1046
  constructor(workingDirectory) {
1029
1047
  this.codex = new Codex();
1030
1048
  if (workingDirectory) {
@@ -1067,6 +1085,33 @@ var CodexManager = class {
1067
1085
  getQueueStatus() {
1068
1086
  return this.messageQueue.getStatus();
1069
1087
  }
1088
+ async interrupt() {
1089
+ const queue = this.messageQueue.drainQueue({
1090
+ maxItems: MAX_INTERRUPT_QUEUE_ITEMS,
1091
+ maxChars: MAX_INTERRUPT_QUEUE_CHARS
1092
+ });
1093
+ const isProcessing = this.messageQueue.isProcessing();
1094
+ if (isProcessing && this.activeAbortController) {
1095
+ try {
1096
+ this.activeAbortController.abort();
1097
+ } catch {
1098
+ }
1099
+ }
1100
+ const linearSessionId = process.env.LINEAR_SESSION_ID;
1101
+ if (linearSessionId && queue.length > 0) {
1102
+ const body = ["Interrupted. Cleared queue:", ...queue].join("\n");
1103
+ const thoughtEvent = {
1104
+ linearSessionId,
1105
+ content: {
1106
+ type: "thought",
1107
+ body
1108
+ }
1109
+ };
1110
+ monolithService.sendEvent({ type: "agent_update", payload: thoughtEvent }).catch(() => {
1111
+ });
1112
+ }
1113
+ return { interrupted: isProcessing || queue.length > 0, queue };
1114
+ }
1070
1115
  /**
1071
1116
  * Set the base system prompt from replicas.json
1072
1117
  * This will be combined with any custom instructions passed to individual messages
@@ -1172,11 +1217,13 @@ var CodexManager = class {
1172
1217
  model: model || DEFAULT_MODEL,
1173
1218
  webSearchMode: "live"
1174
1219
  };
1220
+ const abortController = new AbortController();
1221
+ this.activeAbortController = abortController;
1175
1222
  if (this.currentThreadId) {
1176
1223
  this.currentThread = this.codex.resumeThread(this.currentThreadId, threadOptions);
1177
1224
  } else {
1178
1225
  this.currentThread = this.codex.startThread(threadOptions);
1179
- const { events: events2 } = await this.currentThread.runStreamed("Hello");
1226
+ const { events: events2 } = await this.currentThread.runStreamed("Hello", { signal: abortController.signal });
1180
1227
  for await (const event of events2) {
1181
1228
  if (event.type === "thread.started") {
1182
1229
  this.currentThreadId = event.thread_id;
@@ -1200,7 +1247,7 @@ var CodexManager = class {
1200
1247
  } else {
1201
1248
  input = message;
1202
1249
  }
1203
- const { events } = await this.currentThread.runStreamed(input);
1250
+ const { events } = await this.currentThread.runStreamed(input, { signal: abortController.signal });
1204
1251
  let latestThoughtEvent = null;
1205
1252
  for await (const event of events) {
1206
1253
  if (linearSessionId) {
@@ -1226,6 +1273,7 @@ var CodexManager = class {
1226
1273
  });
1227
1274
  }
1228
1275
  } finally {
1276
+ this.activeAbortController = null;
1229
1277
  const status = await getGitStatus(this.workingDirectory);
1230
1278
  const payload = linearSessionId ? { linearSessionId, status } : { status };
1231
1279
  monolithService.sendEvent({ type: "agent_turn_complete", payload }).catch(() => {
@@ -1514,6 +1562,22 @@ codex.post("/reset", async (c) => {
1514
1562
  );
1515
1563
  }
1516
1564
  });
1565
+ codex.post("/interrupt", async (c) => {
1566
+ try {
1567
+ const result = await codexManager.interrupt();
1568
+ const response = result;
1569
+ return c.json(response);
1570
+ } catch (error) {
1571
+ console.error("Error in /codex/interrupt:", error);
1572
+ return c.json(
1573
+ {
1574
+ error: "Failed to interrupt thread",
1575
+ details: error instanceof Error ? error.message : "Unknown error"
1576
+ },
1577
+ 500
1578
+ );
1579
+ }
1580
+ });
1517
1581
  var codex_default = codex;
1518
1582
 
1519
1583
  // src/routes/claude.ts
@@ -1526,6 +1590,46 @@ import {
1526
1590
  import { join as join4 } from "path";
1527
1591
  import { mkdir as mkdir4, appendFile as appendFile2, rm } from "fs/promises";
1528
1592
  import { homedir as homedir4 } from "os";
1593
+ var MAX_INTERRUPT_QUEUE_ITEMS2 = 1e3;
1594
+ var MAX_INTERRUPT_QUEUE_CHARS2 = 2e5;
1595
+ var PromptStream = class {
1596
+ queue = [];
1597
+ closed = false;
1598
+ waiters = [];
1599
+ push(message) {
1600
+ if (this.closed) return;
1601
+ const waiter = this.waiters.shift();
1602
+ if (waiter) {
1603
+ waiter({ value: message, done: false });
1604
+ return;
1605
+ }
1606
+ this.queue.push(message);
1607
+ }
1608
+ close() {
1609
+ if (this.closed) return;
1610
+ this.closed = true;
1611
+ for (const waiter of this.waiters) {
1612
+ waiter({ value: void 0, done: true });
1613
+ }
1614
+ this.waiters = [];
1615
+ }
1616
+ [Symbol.asyncIterator]() {
1617
+ return {
1618
+ next: () => {
1619
+ const nextItem = this.queue.shift();
1620
+ if (nextItem) {
1621
+ return Promise.resolve({ value: nextItem, done: false });
1622
+ }
1623
+ if (this.closed) {
1624
+ return Promise.resolve({ value: void 0, done: true });
1625
+ }
1626
+ return new Promise((resolve) => {
1627
+ this.waiters.push(resolve);
1628
+ });
1629
+ }
1630
+ };
1631
+ }
1632
+ };
1529
1633
  var ClaudeManager = class {
1530
1634
  workingDirectory;
1531
1635
  historyFile;
@@ -1533,6 +1637,9 @@ var ClaudeManager = class {
1533
1637
  initialized;
1534
1638
  messageQueue;
1535
1639
  baseSystemPrompt;
1640
+ activeQuery = null;
1641
+ activePromptStream = null;
1642
+ pendingInterrupt = false;
1536
1643
  constructor(workingDirectory) {
1537
1644
  if (workingDirectory) {
1538
1645
  this.workingDirectory = workingDirectory;
@@ -1567,6 +1674,37 @@ var ClaudeManager = class {
1567
1674
  getQueueStatus() {
1568
1675
  return this.messageQueue.getStatus();
1569
1676
  }
1677
+ async interrupt() {
1678
+ const queue = this.messageQueue.drainQueue({
1679
+ maxItems: MAX_INTERRUPT_QUEUE_ITEMS2,
1680
+ maxChars: MAX_INTERRUPT_QUEUE_CHARS2
1681
+ });
1682
+ const isProcessing = this.messageQueue.isProcessing();
1683
+ if (isProcessing) {
1684
+ this.pendingInterrupt = true;
1685
+ this.activePromptStream?.close();
1686
+ if (this.activeQuery) {
1687
+ try {
1688
+ await this.activeQuery.interrupt();
1689
+ } catch {
1690
+ }
1691
+ }
1692
+ }
1693
+ const linearSessionId = process.env.LINEAR_SESSION_ID;
1694
+ if (linearSessionId && queue.length > 0) {
1695
+ const body = ["Interrupted. Cleared queue:", ...queue].join("\n");
1696
+ const thoughtEvent = {
1697
+ linearSessionId,
1698
+ content: {
1699
+ type: "thought",
1700
+ body
1701
+ }
1702
+ };
1703
+ monolithService.sendEvent({ type: "agent_update", payload: thoughtEvent }).catch(() => {
1704
+ });
1705
+ }
1706
+ return { interrupted: isProcessing || queue.length > 0, queue };
1707
+ }
1570
1708
  /**
1571
1709
  * Set the base system prompt from replicas.json
1572
1710
  * This will be combined with any custom instructions passed to individual messages
@@ -1640,9 +1778,9 @@ var ClaudeManager = class {
1640
1778
  session_id: this.sessionId ?? ""
1641
1779
  };
1642
1780
  await this.recordEvent(userMessage);
1643
- const promptIterable = (async function* () {
1644
- yield userMessage;
1645
- })();
1781
+ const promptStream = new PromptStream();
1782
+ promptStream.push(userMessage);
1783
+ this.activePromptStream = promptStream;
1646
1784
  const startHooksInstruction = this.getStartHooksInstruction();
1647
1785
  const parts = [];
1648
1786
  if (this.baseSystemPrompt) {
@@ -1656,7 +1794,7 @@ var ClaudeManager = class {
1656
1794
  }
1657
1795
  const combinedInstructions = parts.length > 0 ? parts.join("\n\n") : void 0;
1658
1796
  const response = query({
1659
- prompt: promptIterable,
1797
+ prompt: promptStream,
1660
1798
  options: {
1661
1799
  resume: this.sessionId || void 0,
1662
1800
  cwd: this.workingDirectory,
@@ -1674,6 +1812,15 @@ var ClaudeManager = class {
1674
1812
  model: model || "opus"
1675
1813
  }
1676
1814
  });
1815
+ this.activeQuery = response;
1816
+ if (this.pendingInterrupt) {
1817
+ this.pendingInterrupt = false;
1818
+ this.activePromptStream?.close();
1819
+ try {
1820
+ await this.activeQuery.interrupt();
1821
+ } catch {
1822
+ }
1823
+ }
1677
1824
  let latestThoughtEvent = null;
1678
1825
  for await (const msg of response) {
1679
1826
  await this.handleMessage(msg);
@@ -1693,6 +1840,9 @@ var ClaudeManager = class {
1693
1840
  }
1694
1841
  }
1695
1842
  }
1843
+ if (msg.type === "result") {
1844
+ this.activePromptStream?.close();
1845
+ }
1696
1846
  }
1697
1847
  if (linearSessionId && latestThoughtEvent) {
1698
1848
  const responseEvent = linearThoughtToResponse(latestThoughtEvent);
@@ -1700,6 +1850,10 @@ var ClaudeManager = class {
1700
1850
  });
1701
1851
  }
1702
1852
  } finally {
1853
+ this.activeQuery = null;
1854
+ this.activePromptStream?.close();
1855
+ this.activePromptStream = null;
1856
+ this.pendingInterrupt = false;
1703
1857
  const status = await getGitStatus(this.workingDirectory);
1704
1858
  const payload = linearSessionId ? { linearSessionId, status } : { status };
1705
1859
  monolithService.sendEvent({ type: "agent_turn_complete", payload }).catch(() => {
@@ -1926,6 +2080,22 @@ claude.post("/reset", async (c) => {
1926
2080
  );
1927
2081
  }
1928
2082
  });
2083
+ claude.post("/interrupt", async (c) => {
2084
+ try {
2085
+ const result = await claudeManager.interrupt();
2086
+ const response = result;
2087
+ return c.json(response);
2088
+ } catch (error) {
2089
+ console.error("Error in /claude/interrupt:", error);
2090
+ return c.json(
2091
+ {
2092
+ error: "Failed to interrupt session",
2093
+ details: error instanceof Error ? error.message : "Unknown error"
2094
+ },
2095
+ 500
2096
+ );
2097
+ }
2098
+ });
1929
2099
  var claude_default = claude;
1930
2100
 
1931
2101
  // src/routes/plans.ts
@@ -2111,6 +2281,10 @@ var ClaudeTokenManager = class {
2111
2281
  REFRESH_INTERVAL_MS = 45 * 60 * 1e3;
2112
2282
  // 45 minutes
2113
2283
  async start() {
2284
+ if (process.env.ANTHROPIC_API_KEY) {
2285
+ console.log("[ClaudeTokenManager] Skipping: ANTHROPIC_API_KEY is set");
2286
+ return;
2287
+ }
2114
2288
  const monolithUrl = process.env.MONOLITH_URL;
2115
2289
  const workspaceId = process.env.WORKSPACE_ID;
2116
2290
  const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
@@ -2135,6 +2309,9 @@ var ClaudeTokenManager = class {
2135
2309
  }
2136
2310
  }
2137
2311
  async refreshCredentials() {
2312
+ if (process.env.ANTHROPIC_API_KEY) {
2313
+ return;
2314
+ }
2138
2315
  const monolithUrl = process.env.MONOLITH_URL;
2139
2316
  const workspaceId = process.env.WORKSPACE_ID;
2140
2317
  const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",