replicas-engine 0.1.16 → 0.1.17

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 +212 -40
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -490,13 +490,120 @@ function getGitStatus(workingDirectory) {
490
490
  };
491
491
  }
492
492
 
493
+ // src/services/message-queue.ts
494
+ var MessageQueue = class {
495
+ queue = [];
496
+ processing = false;
497
+ currentMessageId = null;
498
+ messageIdCounter = 0;
499
+ processMessage;
500
+ constructor(processMessage) {
501
+ this.processMessage = processMessage;
502
+ }
503
+ generateMessageId() {
504
+ return `msg_${Date.now()}_${++this.messageIdCounter}`;
505
+ }
506
+ /**
507
+ * Add a message to the queue or start processing immediately if not busy
508
+ * @returns Object indicating whether the message was queued or started processing
509
+ */
510
+ async enqueue(message, model, customInstructions) {
511
+ const messageId = this.generateMessageId();
512
+ const queuedMessage = {
513
+ id: messageId,
514
+ message,
515
+ model,
516
+ customInstructions,
517
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
518
+ };
519
+ if (this.processing) {
520
+ this.queue.push(queuedMessage);
521
+ return {
522
+ queued: true,
523
+ messageId,
524
+ position: this.queue.length
525
+ };
526
+ }
527
+ this.startProcessing(queuedMessage);
528
+ return {
529
+ queued: false,
530
+ messageId,
531
+ position: 0
532
+ };
533
+ }
534
+ async startProcessing(queuedMessage) {
535
+ this.processing = true;
536
+ this.currentMessageId = queuedMessage.id;
537
+ try {
538
+ await this.processMessage(
539
+ queuedMessage.message,
540
+ queuedMessage.model,
541
+ queuedMessage.customInstructions
542
+ );
543
+ } catch (error) {
544
+ console.error("[MessageQueue] Error processing message:", error);
545
+ } finally {
546
+ this.processing = false;
547
+ this.currentMessageId = null;
548
+ await this.processNextInQueue();
549
+ }
550
+ }
551
+ async processNextInQueue() {
552
+ const nextMessage = this.queue.shift();
553
+ if (nextMessage) {
554
+ await this.startProcessing(nextMessage);
555
+ }
556
+ }
557
+ /**
558
+ * Check if currently processing a message
559
+ */
560
+ isProcessing() {
561
+ return this.processing;
562
+ }
563
+ /**
564
+ * Get the current queue length
565
+ */
566
+ getQueueLength() {
567
+ return this.queue.length;
568
+ }
569
+ /**
570
+ * Get full queue status
571
+ */
572
+ getStatus() {
573
+ return {
574
+ isProcessing: this.processing,
575
+ queueLength: this.queue.length,
576
+ currentMessageId: this.currentMessageId,
577
+ queuedMessages: this.queue.map((msg, index) => ({
578
+ id: msg.id,
579
+ queuedAt: msg.queuedAt,
580
+ position: index + 1
581
+ }))
582
+ };
583
+ }
584
+ /**
585
+ * Clear the queue (does not stop current processing)
586
+ */
587
+ clearQueue() {
588
+ this.queue = [];
589
+ }
590
+ /**
591
+ * Reset everything including clearing processing state
592
+ */
593
+ reset() {
594
+ this.queue = [];
595
+ this.processing = false;
596
+ this.currentMessageId = null;
597
+ }
598
+ };
599
+
493
600
  // src/services/codex-manager.ts
494
601
  var CodexManager = class {
495
602
  codex;
496
603
  currentThreadId = null;
497
604
  currentThread = null;
498
605
  workingDirectory;
499
- processing = false;
606
+ messageQueue;
500
607
  constructor(workingDirectory) {
501
608
  this.codex = new Codex();
502
609
  if (workingDirectory) {
@@ -510,14 +617,38 @@ var CodexManager = class {
510
617
  this.workingDirectory = workspaceHome;
511
618
  }
512
619
  }
620
+ this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
513
621
  }
514
622
  isProcessing() {
515
- return this.processing;
623
+ return this.messageQueue.isProcessing();
624
+ }
625
+ /**
626
+ * Enqueue a message for processing. If not currently processing, starts immediately.
627
+ * If already processing, adds to queue.
628
+ * @returns Object with queued status, messageId, and position in queue
629
+ */
630
+ async enqueueMessage(message, model, customInstructions) {
631
+ return this.messageQueue.enqueue(message, model, customInstructions);
632
+ }
633
+ /**
634
+ * Get the current queue status
635
+ */
636
+ getQueueStatus() {
637
+ return this.messageQueue.getStatus();
516
638
  }
639
+ /**
640
+ * Legacy sendMessage method - now uses the queue internally
641
+ * @deprecated Use enqueueMessage for better control over queue status
642
+ */
517
643
  async sendMessage(message, model, customInstructions) {
644
+ await this.enqueueMessage(message, model, customInstructions);
645
+ }
646
+ /**
647
+ * Internal method that actually processes the message
648
+ */
649
+ async processMessageInternal(message, model, customInstructions) {
518
650
  const linearSessionId = process.env.LINEAR_SESSION_ID;
519
651
  try {
520
- this.processing = true;
521
652
  if (!this.currentThread) {
522
653
  if (this.currentThreadId) {
523
654
  this.currentThread = this.codex.resumeThread(this.currentThreadId, {
@@ -559,7 +690,6 @@ var CodexManager = class {
559
690
  }
560
691
  }
561
692
  } finally {
562
- this.processing = false;
563
693
  if (linearSessionId) {
564
694
  const status = getGitStatus(this.workingDirectory);
565
695
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
@@ -602,7 +732,7 @@ var CodexManager = class {
602
732
  async reset() {
603
733
  this.currentThread = null;
604
734
  this.currentThreadId = null;
605
- this.processing = false;
735
+ this.messageQueue.reset();
606
736
  }
607
737
  getThreadId() {
608
738
  return this.currentThreadId;
@@ -684,21 +814,15 @@ codex.post("/send", async (c) => {
684
814
  if (!message || typeof message !== "string") {
685
815
  return c.json({ error: "Message is required and must be a string" }, 400);
686
816
  }
687
- if (codexManager.isProcessing()) {
688
- return c.json(
689
- {
690
- error: "A turn is already in progress. Please wait."
691
- },
692
- 400
693
- );
694
- }
695
- codexManager.sendMessage(message, model, customInstructions).catch((error) => {
696
- console.error("[Codex Route] Error in background message processing:", error);
697
- });
698
- return c.json({
817
+ const result = await codexManager.enqueueMessage(message, model, customInstructions);
818
+ const response = {
699
819
  success: true,
700
- message: "Message sent successfully"
701
- });
820
+ message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
821
+ queued: result.queued,
822
+ messageId: result.messageId,
823
+ position: result.position
824
+ };
825
+ return c.json(response);
702
826
  } catch (error) {
703
827
  console.error("Error in /codex/send:", error);
704
828
  return c.json(
@@ -766,6 +890,21 @@ codex.get("/status", async (c) => {
766
890
  );
767
891
  }
768
892
  });
893
+ codex.get("/queue", async (c) => {
894
+ try {
895
+ const queueStatus = codexManager.getQueueStatus();
896
+ return c.json(queueStatus);
897
+ } catch (error) {
898
+ console.error("Error in /codex/queue:", error);
899
+ return c.json(
900
+ {
901
+ error: "Failed to retrieve queue status",
902
+ details: error instanceof Error ? error.message : "Unknown error"
903
+ },
904
+ 500
905
+ );
906
+ }
907
+ });
769
908
  codex.post("/reset", async (c) => {
770
909
  try {
771
910
  await codexManager.reset();
@@ -801,7 +940,7 @@ var ClaudeManager = class {
801
940
  historyFile;
802
941
  sessionId = null;
803
942
  initialized;
804
- processing = false;
943
+ messageQueue;
805
944
  constructor(workingDirectory) {
806
945
  if (workingDirectory) {
807
946
  this.workingDirectory = workingDirectory;
@@ -816,17 +955,42 @@ var ClaudeManager = class {
816
955
  }
817
956
  this.historyFile = join2(homedir2(), ".replicas", "claude", "history.jsonl");
818
957
  this.initialized = this.initialize();
958
+ this.messageQueue = new MessageQueue(this.processMessageInternal.bind(this));
819
959
  }
820
960
  isProcessing() {
821
- return this.processing;
961
+ return this.messageQueue.isProcessing();
962
+ }
963
+ /**
964
+ * Enqueue a message for processing. If not currently processing, starts immediately.
965
+ * If already processing, adds to queue.
966
+ * @returns Object with queued status, messageId, and position in queue
967
+ */
968
+ async enqueueMessage(message, model, customInstructions) {
969
+ await this.initialized;
970
+ return this.messageQueue.enqueue(message, model, customInstructions);
822
971
  }
972
+ /**
973
+ * Get the current queue status
974
+ */
975
+ getQueueStatus() {
976
+ return this.messageQueue.getStatus();
977
+ }
978
+ /**
979
+ * Legacy sendMessage method - now uses the queue internally
980
+ * @deprecated Use enqueueMessage for better control over queue status
981
+ */
823
982
  async sendMessage(message, model, customInstructions) {
983
+ await this.enqueueMessage(message, model, customInstructions);
984
+ }
985
+ /**
986
+ * Internal method that actually processes the message
987
+ */
988
+ async processMessageInternal(message, model, customInstructions) {
824
989
  const linearSessionId = process.env.LINEAR_SESSION_ID;
825
990
  if (!message || !message.trim()) {
826
991
  throw new Error("Message cannot be empty");
827
992
  }
828
993
  await this.initialized;
829
- this.processing = true;
830
994
  try {
831
995
  const userMessage = {
832
996
  type: "user",
@@ -874,7 +1038,6 @@ var ClaudeManager = class {
874
1038
  }
875
1039
  }
876
1040
  } finally {
877
- this.processing = false;
878
1041
  if (linearSessionId) {
879
1042
  const status = getGitStatus(this.workingDirectory);
880
1043
  monolithService.sendEvent({ type: "agent_turn_complete", payload: { linearSessionId, status } }).catch(() => {
@@ -893,7 +1056,7 @@ var ClaudeManager = class {
893
1056
  async getStatus() {
894
1057
  await this.initialized;
895
1058
  const status = {
896
- has_active_thread: this.processing,
1059
+ has_active_thread: this.messageQueue.isProcessing(),
897
1060
  thread_id: this.sessionId,
898
1061
  working_directory: this.workingDirectory
899
1062
  };
@@ -903,13 +1066,13 @@ var ClaudeManager = class {
903
1066
  await this.initialized;
904
1067
  const allEvents = await readJSONL(this.historyFile);
905
1068
  const events = allEvents.filter((event) => event.timestamp > since);
906
- const isComplete = !this.processing;
1069
+ const isComplete = !this.messageQueue.isProcessing();
907
1070
  return { events, isComplete };
908
1071
  }
909
1072
  async reset() {
910
1073
  await this.initialized;
911
1074
  this.sessionId = null;
912
- this.processing = false;
1075
+ this.messageQueue.reset();
913
1076
  try {
914
1077
  await rm(this.historyFile, { force: true });
915
1078
  } catch {
@@ -946,21 +1109,15 @@ claude.post("/send", async (c) => {
946
1109
  if (!message || typeof message !== "string") {
947
1110
  return c.json({ error: "Message is required and must be a string" }, 400);
948
1111
  }
949
- if (claudeManager.isProcessing()) {
950
- return c.json(
951
- {
952
- error: "A turn is already in progress. Please wait."
953
- },
954
- 400
955
- );
956
- }
957
- claudeManager.sendMessage(message, model, customInstructions).catch((error) => {
958
- console.error("[Claude Route] Error in background message processing:", error);
959
- });
960
- return c.json({
1112
+ const result = await claudeManager.enqueueMessage(message, model, customInstructions);
1113
+ const response = {
961
1114
  success: true,
962
- message: "Message sent successfully"
963
- });
1115
+ message: result.queued ? `Message queued at position ${result.position}` : "Message sent successfully",
1116
+ queued: result.queued,
1117
+ messageId: result.messageId,
1118
+ position: result.position
1119
+ };
1120
+ return c.json(response);
964
1121
  } catch (error) {
965
1122
  console.error("Error in /claude/send:", error);
966
1123
  return c.json(
@@ -1028,6 +1185,21 @@ claude.get("/status", async (c) => {
1028
1185
  );
1029
1186
  }
1030
1187
  });
1188
+ claude.get("/queue", async (c) => {
1189
+ try {
1190
+ const queueStatus = claudeManager.getQueueStatus();
1191
+ return c.json(queueStatus);
1192
+ } catch (error) {
1193
+ console.error("Error in /claude/queue:", error);
1194
+ return c.json(
1195
+ {
1196
+ error: "Failed to retrieve queue status",
1197
+ details: error instanceof Error ? error.message : "Unknown error"
1198
+ },
1199
+ 500
1200
+ );
1201
+ }
1202
+ });
1031
1203
  claude.post("/reset", async (c) => {
1032
1204
  try {
1033
1205
  await claudeManager.reset();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",