replicas-engine 0.1.397 → 0.1.399

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 +130 -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-v4";
487
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-06-v1";
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);
@@ -4674,6 +4680,7 @@ var EnvironmentDetailsService = class {
4674
4680
  detectGitLabAccessConfigured()
4675
4681
  ]);
4676
4682
  details.engineVersion = E2B_TEMPLATE_NAME;
4683
+ details.supportsMidTurnSteering = true;
4677
4684
  details.claudeAuthMethod = detectClaudeAuthMethod();
4678
4685
  details.codexAuthMethod = detectCodexAuthMethod();
4679
4686
  details.cursorAuthMethod = detectCursorAuthMethod();
@@ -5951,6 +5958,22 @@ var MessageQueueService = class {
5951
5958
  this.queue = [];
5952
5959
  return true;
5953
5960
  }
5961
+ takeFromQueue(messageId) {
5962
+ const index = this.queue.findIndex((m) => m.id === messageId);
5963
+ if (index === -1) return null;
5964
+ const [message] = this.queue.splice(index, 1);
5965
+ return { message, index };
5966
+ }
5967
+ restoreToQueue(message, index) {
5968
+ const clamped = Math.max(0, Math.min(index, this.queue.length));
5969
+ this.queue.splice(clamped, 0, message);
5970
+ }
5971
+ resumeIfIdle() {
5972
+ if (this.processing || this.queue.length === 0) return;
5973
+ void this.processNextInQueue().catch((error) => {
5974
+ console.error("[MessageQueue] Error resuming idle queue:", error);
5975
+ });
5976
+ }
5954
5977
  /**
5955
5978
  * Move a message to a new position in the queue
5956
5979
  * @returns true if the message was found and moved
@@ -6072,6 +6095,28 @@ var CodingAgentManager = class {
6072
6095
  await this.initialized;
6073
6096
  return this.messageQueue.enqueue(request);
6074
6097
  }
6098
+ // Take the message off the queue before steering: a turn completing mid-steer could otherwise pop it too.
6099
+ async steerFromQueue(messageId) {
6100
+ await this.initialized;
6101
+ const taken = this.messageQueue.takeFromQueue(messageId);
6102
+ if (!taken) {
6103
+ return { success: false, queue: this.getQueue() };
6104
+ }
6105
+ let steered = false;
6106
+ try {
6107
+ steered = await this.steerRequest(taken.message);
6108
+ } catch (error) {
6109
+ console.error("[CodingAgentManager] steerRequest threw, keeping message queued:", error);
6110
+ }
6111
+ if (!steered) {
6112
+ this.messageQueue.restoreToQueue(taken.message, taken.index);
6113
+ this.messageQueue.resumeIfIdle();
6114
+ }
6115
+ return { success: steered, queue: this.getQueue() };
6116
+ }
6117
+ steerRequest(_request) {
6118
+ return Promise.resolve(false);
6119
+ }
6075
6120
  emitContextUsage(payload) {
6076
6121
  const event = {
6077
6122
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6574,15 +6619,17 @@ var CodexHistoryFile = class {
6574
6619
  this.filePath = filePath;
6575
6620
  }
6576
6621
  filePath;
6577
- writeChain = Promise.resolve();
6622
+ writeLock = new AsyncLock();
6578
6623
  /** Best-effort ordered append; failures must not disrupt the turn. */
6579
6624
  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
- });
6625
+ void this.writeLock.run(
6626
+ () => appendFile2(this.filePath, JSON.stringify(event) + "\n", "utf-8").catch((error) => {
6627
+ console.error("[CodexHistoryFile] Failed to append event:", error);
6628
+ })
6629
+ );
6583
6630
  }
6584
6631
  async flush() {
6585
- await this.writeChain;
6632
+ await this.writeLock.drain();
6586
6633
  }
6587
6634
  async load() {
6588
6635
  try {
@@ -7893,7 +7940,7 @@ var AspClient = class {
7893
7940
  // src/managers/codex-asp/app-server-process.ts
7894
7941
  var DEFAULT_CODEX_BINARY = "codex";
7895
7942
  var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
7896
- var ENGINE_PACKAGE_VERSION = "0.1.397";
7943
+ var ENGINE_PACKAGE_VERSION = "0.1.399";
7897
7944
  var INITIALIZE_METHOD = "initialize";
7898
7945
  var INITIALIZED_NOTIFICATION = "initialized";
7899
7946
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -8146,6 +8193,7 @@ var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
8146
8193
  var THREAD_SETTINGS_UPDATE_METHOD = "thread/settings/update";
8147
8194
  var TURN_START_METHOD = "turn/start";
8148
8195
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
8196
+ var TURN_STEER_METHOD = "turn/steer";
8149
8197
  var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
8150
8198
  var MODEL_LIST_METHOD = "model/list";
8151
8199
  var SKILLS_LIST_METHOD = "skills/list";
@@ -8759,6 +8807,8 @@ var CodexAspManager = class extends CodingAgentManager {
8759
8807
  activeServiceTier;
8760
8808
  slashCommandsCache = null;
8761
8809
  slashCommandsRequest = null;
8810
+ steeredTempImagePaths = [];
8811
+ steerLock = new AsyncLock();
8762
8812
  constructor(options) {
8763
8813
  super(options);
8764
8814
  this.historyFile = options.historyFilePath ? new CodexHistoryFile(options.historyFilePath) : null;
@@ -8789,6 +8839,36 @@ var CodexAspManager = class extends CodingAgentManager {
8789
8839
  console.warn("[CodexAspManager] Failed to interrupt active turn:", error);
8790
8840
  }
8791
8841
  }
8842
+ // Serialize so concurrent steers can't race one `turn/steer` RPC ahead of another.
8843
+ steerRequest(request) {
8844
+ return this.steerLock.run(() => this.steerActiveTurnLocked(request));
8845
+ }
8846
+ async steerActiveTurnLocked(request) {
8847
+ const threadId = this.currentThreadId;
8848
+ const expectedTurnId = this.activeTurnId;
8849
+ if (!threadId || !expectedTurnId || !this.messageQueue.isProcessing()) return false;
8850
+ if (this.isCompacting()) return false;
8851
+ if (getGoalCommand(request.message, request.goalMode)) return false;
8852
+ const { input, tempImagePaths } = await buildTurnInput(request);
8853
+ try {
8854
+ const host = await getCodexAspHost();
8855
+ await host.client.request(
8856
+ TURN_STEER_METHOD,
8857
+ { threadId, expectedTurnId, input }
8858
+ );
8859
+ } catch (error) {
8860
+ await removeTempImageFiles(tempImagePaths);
8861
+ console.warn("[CodexAspManager] Failed to steer active turn, keeping message queued:", error);
8862
+ return false;
8863
+ }
8864
+ if (this.activeTurnId === expectedTurnId) {
8865
+ this.steeredTempImagePaths.push(...tempImagePaths);
8866
+ } else {
8867
+ await removeTempImageFiles(tempImagePaths);
8868
+ }
8869
+ this.recordUserMessageEvent(request);
8870
+ return true;
8871
+ }
8792
8872
  async getHistory() {
8793
8873
  if (!this.currentThreadId) {
8794
8874
  return { thread_id: null, events: [], goal: null };
@@ -8879,18 +8959,21 @@ var CodexAspManager = class extends CodingAgentManager {
8879
8959
  this.recordGoalChange(null, true);
8880
8960
  return null;
8881
8961
  }
8962
+ recordUserMessageEvent(request, extraPayload = {}) {
8963
+ const images = imageContentToUserMessageImages(request.images);
8964
+ this.recordHistoryEvent("event_msg", {
8965
+ type: "user_message",
8966
+ message: request.message,
8967
+ ...images ? { images } : {},
8968
+ ...extraPayload
8969
+ });
8970
+ }
8882
8971
  async processMessageInternal(request) {
8883
8972
  let userMessageRecorded = false;
8884
8973
  const recordUserMessage = (extraPayload = {}) => {
8885
8974
  if (userMessageRecorded) return;
8886
8975
  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
- });
8976
+ this.recordUserMessageEvent(request, extraPayload);
8894
8977
  };
8895
8978
  const goalCommand = getGoalCommand(request.message, request.goalMode);
8896
8979
  const dispatch = async () => {
@@ -8924,6 +9007,7 @@ var CodexAspManager = class extends CodingAgentManager {
8924
9007
  this.transcriptUpdateCoalescer.flushPending();
8925
9008
  this.transcriptUpdateCoalescer.dispose();
8926
9009
  this.activeTurnId = null;
9010
+ await removeTempImageFiles(this.steeredTempImagePaths.splice(0));
8927
9011
  await this.historyFile?.flush();
8928
9012
  await this.onTurnComplete();
8929
9013
  }
@@ -11129,6 +11213,9 @@ var RelayManager = class {
11129
11213
  reorderQueue(messageId, newPosition) {
11130
11214
  return this.inner.reorderQueue(messageId, newPosition);
11131
11215
  }
11216
+ steerFromQueue(messageId) {
11217
+ return this.inner.steerFromQueue(messageId);
11218
+ }
11132
11219
  };
11133
11220
 
11134
11221
  // src/services/keep-alive-service.ts
@@ -11808,6 +11895,10 @@ var ChatService = class {
11808
11895
  queue: chat.provider.getQueue()
11809
11896
  };
11810
11897
  }
11898
+ steerFromQueue(chatId, messageId) {
11899
+ const chat = this.requireChat(chatId);
11900
+ return chat.provider.steerFromQueue(messageId);
11901
+ }
11811
11902
  async respondToToolInput(chatId, requestId, selectionId) {
11812
11903
  const chat = this.requireChat(chatId);
11813
11904
  if (!chat.provider.respondToToolInput) {
@@ -13162,6 +13253,17 @@ function createV1Routes(deps) {
13162
13253
  return c.json(jsonError("Failed to remove from queue", error instanceof Error ? error.message : "Unknown error"), 500);
13163
13254
  }
13164
13255
  });
13256
+ app2.post("/chats/:chatId/queue/:messageId/steer", async (c) => {
13257
+ try {
13258
+ const result = await deps.chatService.steerFromQueue(c.req.param("chatId"), c.req.param("messageId"));
13259
+ return c.json(result);
13260
+ } catch (error) {
13261
+ if (error instanceof ChatNotFoundError) {
13262
+ return c.json(jsonError("Chat not found", error.message), 404);
13263
+ }
13264
+ return c.json(jsonError("Failed to steer queued message", error instanceof Error ? error.message : "Unknown error"), 500);
13265
+ }
13266
+ });
13165
13267
  app2.patch("/chats/:chatId/queue/reorder", async (c) => {
13166
13268
  try {
13167
13269
  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.397",
3
+ "version": "0.1.399",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",