replicas-engine 0.1.396 → 0.1.398

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 +129 -28
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -484,7 +484,7 @@ var WORKSPACE_SIZES = ["small", "large"];
484
484
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
485
485
 
486
486
  // ../shared/src/e2b.ts
487
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-04-v3";
487
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-04-v5";
488
488
 
489
489
  // ../shared/src/runtime-env.ts
490
490
  function parsePosixEnvFile(content) {
@@ -3261,6 +3261,21 @@ var monolithService = new MonolithService();
3261
3261
  // src/utils/file.ts
3262
3262
  import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
3263
3263
  import { dirname } from "path";
3264
+
3265
+ // src/utils/async-lock.ts
3266
+ var AsyncLock = class {
3267
+ chain = Promise.resolve();
3268
+ run(operation) {
3269
+ const result = this.chain.then(operation, operation);
3270
+ this.chain = result.then(() => void 0, () => void 0);
3271
+ return result;
3272
+ }
3273
+ async drain() {
3274
+ await this.chain;
3275
+ }
3276
+ };
3277
+
3278
+ // src/utils/file.ts
3264
3279
  async function atomicWriteFile(path5, data, options) {
3265
3280
  const tmpFile = `${path5}.${process.pid}.${Date.now()}.tmp`;
3266
3281
  try {
@@ -3277,9 +3292,9 @@ async function writeSecureCredentialFile(filePath, content, options) {
3277
3292
  }
3278
3293
  await atomicWriteFile(filePath, content, { mode: 384 });
3279
3294
  }
3280
- var credentialFileQueue = Promise.resolve();
3295
+ var credentialFileLock = new AsyncLock();
3281
3296
  function upsertCredentialFileLines(filePath, hosts, lines) {
3282
- const task = credentialFileQueue.then(async () => {
3297
+ return credentialFileLock.run(async () => {
3283
3298
  let existing = [];
3284
3299
  try {
3285
3300
  existing = (await readFile(filePath, "utf-8")).split("\n").filter(Boolean);
@@ -3289,11 +3304,9 @@ function upsertCredentialFileLines(filePath, hosts, lines) {
3289
3304
  const kept = existing.filter((line) => !hosts.some((host) => line.endsWith(`@${host}`)));
3290
3305
  await writeSecureCredentialFile(filePath, [...lines, ...kept, ""].join("\n"));
3291
3306
  });
3292
- credentialFileQueue = task.catch(() => void 0);
3293
- return task;
3294
3307
  }
3295
3308
  function removeCredentialFileLines(filePath, shouldRemove) {
3296
- const task = credentialFileQueue.then(async () => {
3309
+ return credentialFileLock.run(async () => {
3297
3310
  let existing = [];
3298
3311
  try {
3299
3312
  existing = (await readFile(filePath, "utf-8")).split("\n").filter(Boolean);
@@ -3303,8 +3316,6 @@ function removeCredentialFileLines(filePath, shouldRemove) {
3303
3316
  const kept = existing.filter((line) => !shouldRemove(line));
3304
3317
  await writeSecureCredentialFile(filePath, [...kept, ""].join("\n"));
3305
3318
  });
3306
- credentialFileQueue = task.catch(() => void 0);
3307
- return task;
3308
3319
  }
3309
3320
 
3310
3321
  // src/utils/git-identity.ts
@@ -3675,14 +3686,9 @@ var STATE_FILE = join3(STATE_DIR, "engine-state.json");
3675
3686
  var DEFAULT_STATE = {
3676
3687
  repos: {}
3677
3688
  };
3678
- var stateWriteChain = Promise.resolve();
3679
- function enqueueStateWrite(operation) {
3680
- const result = stateWriteChain.then(operation);
3681
- stateWriteChain = result.then(() => void 0, () => void 0);
3682
- return result;
3683
- }
3689
+ var stateWriteLock = new AsyncLock();
3684
3690
  async function updateEngineState(updater) {
3685
- await enqueueStateWrite(async () => {
3691
+ await stateWriteLock.run(async () => {
3686
3692
  await mkdir2(STATE_DIR, { recursive: true });
3687
3693
  const currentState = await loadEngineState();
3688
3694
  const nextState = updater(currentState);
@@ -5951,6 +5957,22 @@ var MessageQueueService = class {
5951
5957
  this.queue = [];
5952
5958
  return true;
5953
5959
  }
5960
+ takeFromQueue(messageId) {
5961
+ const index = this.queue.findIndex((m) => m.id === messageId);
5962
+ if (index === -1) return null;
5963
+ const [message] = this.queue.splice(index, 1);
5964
+ return { message, index };
5965
+ }
5966
+ restoreToQueue(message, index) {
5967
+ const clamped = Math.max(0, Math.min(index, this.queue.length));
5968
+ this.queue.splice(clamped, 0, message);
5969
+ }
5970
+ resumeIfIdle() {
5971
+ if (this.processing || this.queue.length === 0) return;
5972
+ void this.processNextInQueue().catch((error) => {
5973
+ console.error("[MessageQueue] Error resuming idle queue:", error);
5974
+ });
5975
+ }
5954
5976
  /**
5955
5977
  * Move a message to a new position in the queue
5956
5978
  * @returns true if the message was found and moved
@@ -6072,6 +6094,28 @@ var CodingAgentManager = class {
6072
6094
  await this.initialized;
6073
6095
  return this.messageQueue.enqueue(request);
6074
6096
  }
6097
+ // Take the message off the queue before steering: a turn completing mid-steer could otherwise pop it too.
6098
+ async steerFromQueue(messageId) {
6099
+ await this.initialized;
6100
+ const taken = this.messageQueue.takeFromQueue(messageId);
6101
+ if (!taken) {
6102
+ return { success: false, queue: this.getQueue() };
6103
+ }
6104
+ let steered = false;
6105
+ try {
6106
+ steered = await this.steerRequest(taken.message);
6107
+ } catch (error) {
6108
+ console.error("[CodingAgentManager] steerRequest threw, keeping message queued:", error);
6109
+ }
6110
+ if (!steered) {
6111
+ this.messageQueue.restoreToQueue(taken.message, taken.index);
6112
+ this.messageQueue.resumeIfIdle();
6113
+ }
6114
+ return { success: steered, queue: this.getQueue() };
6115
+ }
6116
+ steerRequest(_request) {
6117
+ return Promise.resolve(false);
6118
+ }
6075
6119
  emitContextUsage(payload) {
6076
6120
  const event = {
6077
6121
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6574,15 +6618,17 @@ var CodexHistoryFile = class {
6574
6618
  this.filePath = filePath;
6575
6619
  }
6576
6620
  filePath;
6577
- writeChain = Promise.resolve();
6621
+ writeLock = new AsyncLock();
6578
6622
  /** Best-effort ordered append; failures must not disrupt the turn. */
6579
6623
  append(event) {
6580
- this.writeChain = this.writeChain.then(() => appendFile2(this.filePath, JSON.stringify(event) + "\n", "utf-8")).catch((error) => {
6581
- console.error("[CodexHistoryFile] Failed to append event:", error);
6582
- });
6624
+ void this.writeLock.run(
6625
+ () => appendFile2(this.filePath, JSON.stringify(event) + "\n", "utf-8").catch((error) => {
6626
+ console.error("[CodexHistoryFile] Failed to append event:", error);
6627
+ })
6628
+ );
6583
6629
  }
6584
6630
  async flush() {
6585
- await this.writeChain;
6631
+ await this.writeLock.drain();
6586
6632
  }
6587
6633
  async load() {
6588
6634
  try {
@@ -7893,7 +7939,7 @@ var AspClient = class {
7893
7939
  // src/managers/codex-asp/app-server-process.ts
7894
7940
  var DEFAULT_CODEX_BINARY = "codex";
7895
7941
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
7896
- var ENGINE_PACKAGE_VERSION = "0.1.396";
7942
+ var ENGINE_PACKAGE_VERSION = "0.1.398";
7897
7943
  var INITIALIZE_METHOD = "initialize";
7898
7944
  var INITIALIZED_NOTIFICATION = "initialized";
7899
7945
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -8146,6 +8192,7 @@ var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
8146
8192
  var THREAD_SETTINGS_UPDATE_METHOD = "thread/settings/update";
8147
8193
  var TURN_START_METHOD = "turn/start";
8148
8194
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
8195
+ var TURN_STEER_METHOD = "turn/steer";
8149
8196
  var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
8150
8197
  var MODEL_LIST_METHOD = "model/list";
8151
8198
  var SKILLS_LIST_METHOD = "skills/list";
@@ -8759,6 +8806,8 @@ var CodexAspManager = class extends CodingAgentManager {
8759
8806
  activeServiceTier;
8760
8807
  slashCommandsCache = null;
8761
8808
  slashCommandsRequest = null;
8809
+ steeredTempImagePaths = [];
8810
+ steerLock = new AsyncLock();
8762
8811
  constructor(options) {
8763
8812
  super(options);
8764
8813
  this.historyFile = options.historyFilePath ? new CodexHistoryFile(options.historyFilePath) : null;
@@ -8789,6 +8838,36 @@ var CodexAspManager = class extends CodingAgentManager {
8789
8838
  console.warn("[CodexAspManager] Failed to interrupt active turn:", error);
8790
8839
  }
8791
8840
  }
8841
+ // Serialize so concurrent steers can't race one `turn/steer` RPC ahead of another.
8842
+ steerRequest(request) {
8843
+ return this.steerLock.run(() => this.steerActiveTurnLocked(request));
8844
+ }
8845
+ async steerActiveTurnLocked(request) {
8846
+ const threadId = this.currentThreadId;
8847
+ const expectedTurnId = this.activeTurnId;
8848
+ if (!threadId || !expectedTurnId || !this.messageQueue.isProcessing()) return false;
8849
+ if (this.isCompacting()) return false;
8850
+ if (getGoalCommand(request.message, request.goalMode)) return false;
8851
+ const { input, tempImagePaths } = await buildTurnInput(request);
8852
+ try {
8853
+ const host = await getCodexAspHost();
8854
+ await host.client.request(
8855
+ TURN_STEER_METHOD,
8856
+ { threadId, expectedTurnId, input }
8857
+ );
8858
+ } catch (error) {
8859
+ await removeTempImageFiles(tempImagePaths);
8860
+ console.warn("[CodexAspManager] Failed to steer active turn, keeping message queued:", error);
8861
+ return false;
8862
+ }
8863
+ if (this.activeTurnId === expectedTurnId) {
8864
+ this.steeredTempImagePaths.push(...tempImagePaths);
8865
+ } else {
8866
+ await removeTempImageFiles(tempImagePaths);
8867
+ }
8868
+ this.recordUserMessageEvent(request);
8869
+ return true;
8870
+ }
8792
8871
  async getHistory() {
8793
8872
  if (!this.currentThreadId) {
8794
8873
  return { thread_id: null, events: [], goal: null };
@@ -8879,18 +8958,21 @@ var CodexAspManager = class extends CodingAgentManager {
8879
8958
  this.recordGoalChange(null, true);
8880
8959
  return null;
8881
8960
  }
8961
+ recordUserMessageEvent(request, extraPayload = {}) {
8962
+ const images = imageContentToUserMessageImages(request.images);
8963
+ this.recordHistoryEvent("event_msg", {
8964
+ type: "user_message",
8965
+ message: request.message,
8966
+ ...images ? { images } : {},
8967
+ ...extraPayload
8968
+ });
8969
+ }
8882
8970
  async processMessageInternal(request) {
8883
8971
  let userMessageRecorded = false;
8884
8972
  const recordUserMessage = (extraPayload = {}) => {
8885
8973
  if (userMessageRecorded) return;
8886
8974
  userMessageRecorded = true;
8887
- const images = imageContentToUserMessageImages(request.images);
8888
- this.recordHistoryEvent("event_msg", {
8889
- type: "user_message",
8890
- message: request.message,
8891
- ...images ? { images } : {},
8892
- ...extraPayload
8893
- });
8975
+ this.recordUserMessageEvent(request, extraPayload);
8894
8976
  };
8895
8977
  const goalCommand = getGoalCommand(request.message, request.goalMode);
8896
8978
  const dispatch = async () => {
@@ -8924,6 +9006,7 @@ var CodexAspManager = class extends CodingAgentManager {
8924
9006
  this.transcriptUpdateCoalescer.flushPending();
8925
9007
  this.transcriptUpdateCoalescer.dispose();
8926
9008
  this.activeTurnId = null;
9009
+ await removeTempImageFiles(this.steeredTempImagePaths.splice(0));
8927
9010
  await this.historyFile?.flush();
8928
9011
  await this.onTurnComplete();
8929
9012
  }
@@ -11129,6 +11212,9 @@ var RelayManager = class {
11129
11212
  reorderQueue(messageId, newPosition) {
11130
11213
  return this.inner.reorderQueue(messageId, newPosition);
11131
11214
  }
11215
+ steerFromQueue(messageId) {
11216
+ return this.inner.steerFromQueue(messageId);
11217
+ }
11132
11218
  };
11133
11219
 
11134
11220
  // src/services/keep-alive-service.ts
@@ -11808,6 +11894,10 @@ var ChatService = class {
11808
11894
  queue: chat.provider.getQueue()
11809
11895
  };
11810
11896
  }
11897
+ steerFromQueue(chatId, messageId) {
11898
+ const chat = this.requireChat(chatId);
11899
+ return chat.provider.steerFromQueue(messageId);
11900
+ }
11811
11901
  async respondToToolInput(chatId, requestId, selectionId) {
11812
11902
  const chat = this.requireChat(chatId);
11813
11903
  if (!chat.provider.respondToToolInput) {
@@ -13162,6 +13252,17 @@ function createV1Routes(deps) {
13162
13252
  return c.json(jsonError("Failed to remove from queue", error instanceof Error ? error.message : "Unknown error"), 500);
13163
13253
  }
13164
13254
  });
13255
+ app2.post("/chats/:chatId/queue/:messageId/steer", async (c) => {
13256
+ try {
13257
+ const result = await deps.chatService.steerFromQueue(c.req.param("chatId"), c.req.param("messageId"));
13258
+ return c.json(result);
13259
+ } catch (error) {
13260
+ if (error instanceof ChatNotFoundError) {
13261
+ return c.json(jsonError("Chat not found", error.message), 404);
13262
+ }
13263
+ return c.json(jsonError("Failed to steer queued message", error instanceof Error ? error.message : "Unknown error"), 500);
13264
+ }
13265
+ });
13165
13266
  app2.patch("/chats/:chatId/queue/reorder", async (c) => {
13166
13267
  try {
13167
13268
  const body = await c.req.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.396",
3
+ "version": "0.1.398",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",