replicas-engine 0.1.697 → 0.1.698

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.
@@ -724,6 +724,10 @@ function coerceClaudePartialMessagePayload(payload) {
724
724
  status: payload.status === "completed" ? "completed" : "in_progress"
725
725
  };
726
726
  }
727
+ function getClaudePartialMessageStreamId(event) {
728
+ if (event.type !== CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE) return null;
729
+ return typeof event.payload.streamId === "string" && event.payload.streamId ? event.payload.streamId : null;
730
+ }
727
731
  var ACCEPTED_USER_MESSAGE_SOURCE = "replicas-chat-turn-accepted";
728
732
  var USER_MESSAGE_ID_PAYLOAD_KEY = "replicasMessageId";
729
733
  var CODEX_ASP_ITEM_ID_PAYLOAD_KEY = "codexAspItemId";
@@ -4059,10 +4063,11 @@ function getEventTimestampMs(event) {
4059
4063
  function areSameUserMessageEvents(a, b) {
4060
4064
  const aMessage = getUserMessage(a);
4061
4065
  const bMessage = getUserMessage(b);
4062
- if (!aMessage || aMessage !== bMessage) return false;
4066
+ if (!aMessage || !bMessage) return false;
4063
4067
  const aMessageId = getUserMessageId(a);
4064
4068
  const bMessageId = getUserMessageId(b);
4065
4069
  if (aMessageId || bMessageId) return aMessageId === bMessageId;
4070
+ if (aMessage !== bMessage) return false;
4066
4071
  const aItemId = getUserMessageItemId(a);
4067
4072
  const bItemId = getUserMessageItemId(b);
4068
4073
  if (aItemId || bItemId) return aItemId === bItemId;
@@ -4562,7 +4567,7 @@ function parseCursorEvents(events) {
4562
4567
  const message = event.payload.message;
4563
4568
  if (typeof message === "string" && message.trim()) {
4564
4569
  messages.push({
4565
- id: `cursor-user-${event.timestamp}-${messages.length}`,
4570
+ id: getUserMessageId(event) ?? `cursor-user-${event.timestamp}-${messages.length}`,
4566
4571
  type: "user",
4567
4572
  content: message,
4568
4573
  timestamp: event.timestamp
@@ -5143,7 +5148,7 @@ function parsePiEvents(events) {
5143
5148
  let activeMessageId = "assistant";
5144
5149
  for (const event of events) {
5145
5150
  if (event.type === "event_msg" && event.payload.type === "user_message" && typeof event.payload.message === "string") {
5146
- messages.push({ id: `pi-user-${event.timestamp}-${messages.length}`, type: "user", content: event.payload.message, timestamp: event.timestamp });
5151
+ messages.push({ id: getUserMessageId(event) ?? `pi-user-${event.timestamp}-${messages.length}`, type: "user", content: event.payload.message, timestamp: event.timestamp });
5147
5152
  if (typeof event.payload.skillName === "string" && event.payload.skillName) {
5148
5153
  messages.push({
5149
5154
  id: `pi-skill-${event.timestamp}-${messages.length}`,
@@ -6184,7 +6189,12 @@ function parseAgentEvents(events, agentType) {
6184
6189
  function parseDisplayMessages(events, agentType, codexAspTranscript, options = {}) {
6185
6190
  const shouldFilter = options.filter ?? true;
6186
6191
  const parsedEvents = agentType === "claude" || agentType === "relay" ? parseClaudeEvents(events, options.parentToolUseId) : parseAgentEvents(events, agentType);
6187
- const legacyMessages = shouldFilter ? filterDisplayMessages(parsedEvents, agentType) : parsedEvents;
6192
+ const reconciledEvents = [];
6193
+ for (const message of parsedEvents) {
6194
+ if (message.type === "user") upsertDisplayMessage(reconciledEvents, message);
6195
+ else reconciledEvents.push(message);
6196
+ }
6197
+ const legacyMessages = shouldFilter ? filterDisplayMessages(reconciledEvents, agentType) : reconciledEvents;
6188
6198
  const applySyntheticNotices = (messages) => shouldFilter ? applyAuthFallbackNotices(applyInterruptions(messages, events), events) : messages;
6189
6199
  if (agentType !== "codex" || !codexAspTranscript) {
6190
6200
  return applySyntheticNotices(legacyMessages);
@@ -6765,7 +6775,7 @@ var DEFAULT_CODEX_ARGS = [
6765
6775
  var MIN_CODEX_CLI_VERSION = "0.144.6";
6766
6776
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
6767
6777
  var codexCliVersionEnsured = null;
6768
- var ENGINE_PACKAGE_VERSION = "0.1.697";
6778
+ var ENGINE_PACKAGE_VERSION = "0.1.698";
6769
6779
  var INITIALIZE_METHOD = "initialize";
6770
6780
  var INITIALIZED_NOTIFICATION = "initialized";
6771
6781
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -7037,6 +7047,8 @@ export {
7037
7047
  isAgentChatSkillActivityRecord,
7038
7048
  isAgentChatMcpActivityRecord,
7039
7049
  CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE,
7050
+ getClaudePartialMessageStreamId,
7051
+ ACCEPTED_USER_MESSAGE_SOURCE,
7040
7052
  USER_MESSAGE_ID_PAYLOAD_KEY,
7041
7053
  CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE,
7042
7054
  CODEX_QUOTA_STATUS_EVENT_TYPE,
@@ -7,7 +7,7 @@ import {
7
7
  headlessAgentRequestSchema,
8
8
  putPresignedFile,
9
9
  recoverCompletedTurn
10
- } from "./chunk-2YKP6MBQ.js";
10
+ } from "./chunk-2BBE2BVQ.js";
11
11
 
12
12
  // src/headless-agent.ts
13
13
  import { createHash } from "crypto";
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ ACCEPTED_USER_MESSAGE_SOURCE,
3
4
  ACCOUNT_RATE_LIMITS_UPDATED_METHOD,
4
5
  AGENT,
5
6
  AGENT_MESSAGE_DELTA_METHOD,
@@ -131,6 +132,7 @@ import {
131
132
  getChatHistoryPageWindow,
132
133
  getChatTranscriptHistorySize,
133
134
  getClaudeModelContextWindow,
135
+ getClaudePartialMessageStreamId,
134
136
  getCodexAspTurnResponse,
135
137
  getDeepseekAssistantMessageText,
136
138
  getDefaultAgentModel,
@@ -192,13 +194,13 @@ import {
192
194
  serializeCanvasContentResponse,
193
195
  shellQuotePosix,
194
196
  stripAgentDiagnosticErrors
195
- } from "./chunk-2YKP6MBQ.js";
197
+ } from "./chunk-2BBE2BVQ.js";
196
198
 
197
199
  // src/index.ts
198
200
  import { serve } from "@hono/node-server";
199
201
  import { Hono as Hono2 } from "hono";
200
202
  import { existsSync as existsSync11 } from "fs";
201
- import { randomUUID as randomUUID10 } from "crypto";
203
+ import { randomUUID as randomUUID11 } from "crypto";
202
204
  import { connect } from "net";
203
205
 
204
206
  // src/managers/github-token-manager.ts
@@ -569,6 +571,7 @@ var MonolithService = class {
569
571
  var monolithService = new MonolithService();
570
572
 
571
573
  // src/utils/file.ts
574
+ import { randomUUID } from "crypto";
572
575
  import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
573
576
  import { dirname, join as join3 } from "path";
574
577
 
@@ -587,7 +590,7 @@ var AsyncLock = class {
587
590
 
588
591
  // src/utils/file.ts
589
592
  async function atomicWriteFile(path6, data, options) {
590
- const tmpFile = `${path6}.${process.pid}.${Date.now()}.tmp`;
593
+ const tmpFile = `${path6}.${process.pid}.${randomUUID()}.tmp`;
591
594
  try {
592
595
  await writeFile(tmpFile, data, { encoding: "utf-8", mode: options?.mode });
593
596
  await rename(tmpFile, path6);
@@ -2663,7 +2666,7 @@ var replicasConfigService = new ReplicasConfigService();
2663
2666
  import { mkdir as mkdir7 } from "fs/promises";
2664
2667
  import { homedir as homedir9 } from "os";
2665
2668
  import { join as join12 } from "path";
2666
- import { randomUUID } from "crypto";
2669
+ import { randomUUID as randomUUID2 } from "crypto";
2667
2670
  var ENGINE_DIR = join12(homedir9(), ".replicas", "engine");
2668
2671
  var EVENTS_FILE = join12(ENGINE_DIR, "events.jsonl");
2669
2672
  var EventService = class {
@@ -2674,7 +2677,7 @@ var EventService = class {
2674
2677
  this.writer.open(EVENTS_FILE);
2675
2678
  }
2676
2679
  subscribe(subscriber) {
2677
- const id = randomUUID();
2680
+ const id = randomUUID2();
2678
2681
  this.subscribers.set(id, subscriber);
2679
2682
  return () => {
2680
2683
  this.subscribers.delete(id);
@@ -2698,7 +2701,7 @@ var eventService = new EventService();
2698
2701
  // src/services/preview-service.ts
2699
2702
  import { mkdir as mkdir8, readFile as readFile7 } from "fs/promises";
2700
2703
  import { existsSync as existsSync4 } from "fs";
2701
- import { randomUUID as randomUUID2 } from "crypto";
2704
+ import { randomUUID as randomUUID3 } from "crypto";
2702
2705
  import { homedir as homedir10 } from "os";
2703
2706
  import { dirname as dirname2, join as join13 } from "path";
2704
2707
  var PREVIEW_PORTS_FILE = join13(homedir10(), ".replicas", "preview-ports.json");
@@ -2737,7 +2740,7 @@ var PreviewService = class {
2737
2740
  data.previews.push(preview);
2738
2741
  await writePreviewsFile(data);
2739
2742
  eventService.publish({
2740
- id: randomUUID2(),
2743
+ id: randomUUID3(),
2741
2744
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2742
2745
  type: "preview.changed",
2743
2746
  payload: { previews: data.previews }
@@ -2757,7 +2760,7 @@ var PreviewService = class {
2757
2760
  }
2758
2761
  await writePreviewsFile(data);
2759
2762
  eventService.publish({
2760
- id: randomUUID2(),
2763
+ id: randomUUID3(),
2761
2764
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2762
2765
  type: "preview.changed",
2763
2766
  payload: { previews: data.previews }
@@ -2814,13 +2817,138 @@ async function registerDesktopPreview() {
2814
2817
  import { existsSync as existsSync8 } from "fs";
2815
2818
  import { appendFile as appendFile4, copyFile, mkdir as mkdir17, readFile as readFile17, rename as rename3, rm as rm2 } from "fs/promises";
2816
2819
  import { join as join29 } from "path";
2817
- import { randomUUID as randomUUID8 } from "crypto";
2820
+ import { randomUUID as randomUUID9 } from "crypto";
2821
+
2822
+ // ../shared/src/workspace-chat.ts
2823
+ var WORKSPACE_CHATS_QUERY_SCHEMA_VERSION = 1;
2824
+ var WORKSPACE_CHATS_QUERY_ROOT = `workspace-chats:v${WORKSPACE_CHATS_QUERY_SCHEMA_VERSION}`;
2825
+ function isAcceptedUserMessage(event) {
2826
+ return getUserMessage(event) !== null && event.payload.source === ACCEPTED_USER_MESSAGE_SOURCE;
2827
+ }
2828
+ function getComparableUserMessage(event) {
2829
+ const message = getUserMessage(event);
2830
+ if (message !== null) return message;
2831
+ const payloadMessage = event.payload.message;
2832
+ if (event.type !== "claude-user" || typeof payloadMessage !== "object" || payloadMessage === null || !("content" in payloadMessage)) {
2833
+ return null;
2834
+ }
2835
+ return extractToolResultText(payloadMessage.content) || null;
2836
+ }
2837
+ function areDuplicateUserMessages(a, b) {
2838
+ if (areSameUserMessageEvents(a, b)) return true;
2839
+ const aMessage = getComparableUserMessage(a);
2840
+ const bMessage = getComparableUserMessage(b);
2841
+ if (!aMessage || aMessage !== bMessage) return false;
2842
+ const aAccepted = isAcceptedUserMessage(a);
2843
+ const bAccepted = isAcceptedUserMessage(b);
2844
+ return aAccepted !== bAccepted ? areUserMessagesWithinMatchWindow(
2845
+ { content: aMessage, timestamp: a.timestamp },
2846
+ { content: bMessage, timestamp: b.timestamp }
2847
+ ) : false;
2848
+ }
2849
+ var eventJsonCache = /* @__PURE__ */ new WeakMap();
2850
+ function workspaceChatEventSignals(event) {
2851
+ let json = eventJsonCache.get(event);
2852
+ if (json === void 0) {
2853
+ json = JSON.stringify(event);
2854
+ eventJsonCache.set(event, json);
2855
+ }
2856
+ return {
2857
+ streamId: getClaudePartialMessageStreamId(event),
2858
+ json,
2859
+ userMessage: getComparableUserMessage(event),
2860
+ userMessageId: getUserMessageId(event)
2861
+ };
2862
+ }
2863
+ function indexMergedEvent(index, event, position) {
2864
+ const { streamId, json, userMessage, userMessageId } = workspaceChatEventSignals(event);
2865
+ if (streamId !== null && !index.byStreamId.has(streamId)) {
2866
+ index.byStreamId.set(streamId, position);
2867
+ }
2868
+ if (!index.byJson.has(json)) index.byJson.set(json, position);
2869
+ if (userMessageId !== null && !index.byUserMessageId.has(userMessageId)) {
2870
+ index.byUserMessageId.set(userMessageId, position);
2871
+ }
2872
+ if (userMessage !== null) {
2873
+ const bucket = index.byUserMessage.get(userMessage);
2874
+ if (!bucket) {
2875
+ index.byUserMessage.set(userMessage, [{ position, event }]);
2876
+ return;
2877
+ }
2878
+ const insertAt = bucket.findIndex((entry) => entry.position > position);
2879
+ bucket.splice(insertAt === -1 ? bucket.length : insertAt, 0, { position, event });
2880
+ }
2881
+ }
2882
+ function reindexReplacedEvent(index, previous, event, position) {
2883
+ const { streamId, json, userMessage, userMessageId } = workspaceChatEventSignals(previous);
2884
+ if (streamId !== null && index.byStreamId.get(streamId) === position) {
2885
+ index.byStreamId.delete(streamId);
2886
+ }
2887
+ if (index.byJson.get(json) === position) index.byJson.delete(json);
2888
+ if (userMessageId !== null && index.byUserMessageId.get(userMessageId) === position) {
2889
+ index.byUserMessageId.delete(userMessageId);
2890
+ }
2891
+ if (userMessage !== null) {
2892
+ const bucket = index.byUserMessage.get(userMessage) ?? [];
2893
+ const entryIndex = bucket.findIndex((entry) => entry.position === position);
2894
+ if (entryIndex !== -1) bucket.splice(entryIndex, 1);
2895
+ if (bucket.length === 0) {
2896
+ index.byUserMessage.delete(userMessage);
2897
+ }
2898
+ }
2899
+ indexMergedEvent(index, event, position);
2900
+ }
2901
+ function findDuplicatePosition(index, event) {
2902
+ const { streamId, json, userMessage, userMessageId } = workspaceChatEventSignals(event);
2903
+ let match = -1;
2904
+ const consider = (position) => {
2905
+ if (position !== void 0 && (match === -1 || position < match)) match = position;
2906
+ };
2907
+ if (streamId !== null) consider(index.byStreamId.get(streamId));
2908
+ consider(index.byJson.get(json));
2909
+ if (userMessageId !== null) consider(index.byUserMessageId.get(userMessageId));
2910
+ if (userMessage !== null) {
2911
+ for (const entry of index.byUserMessage.get(userMessage) ?? []) {
2912
+ if (areDuplicateUserMessages(entry.event, event)) {
2913
+ consider(entry.position);
2914
+ break;
2915
+ }
2916
+ }
2917
+ }
2918
+ return match;
2919
+ }
2920
+ function mergeWorkspaceChatHistoryEvents(primary, supplemental) {
2921
+ if (supplemental.length === 0) return [...primary];
2922
+ const merged = [...primary];
2923
+ const index = {
2924
+ byStreamId: /* @__PURE__ */ new Map(),
2925
+ byJson: /* @__PURE__ */ new Map(),
2926
+ byUserMessageId: /* @__PURE__ */ new Map(),
2927
+ byUserMessage: /* @__PURE__ */ new Map()
2928
+ };
2929
+ for (let position = 0; position < merged.length; position++) {
2930
+ indexMergedEvent(index, merged[position], position);
2931
+ }
2932
+ for (const event of supplemental) {
2933
+ const position = findDuplicatePosition(index, event);
2934
+ const messageId = getUserMessageId(event);
2935
+ if (position === -1) {
2936
+ indexMergedEvent(index, event, merged.length);
2937
+ merged.push(event);
2938
+ } else if (getClaudePartialMessageStreamId(event) !== null || isAcceptedUserMessage(merged[position]) && !isAcceptedUserMessage(event) || messageId !== null && messageId === getUserMessageId(merged[position])) {
2939
+ const previous = merged[position];
2940
+ merged[position] = event;
2941
+ reindexReplacedEvent(index, previous, event, position);
2942
+ }
2943
+ }
2944
+ return merged;
2945
+ }
2818
2946
 
2819
2947
  // src/managers/claude-manager.ts
2820
2948
  import {
2821
2949
  query
2822
2950
  } from "@anthropic-ai/claude-agent-sdk";
2823
- import { randomUUID as randomUUID4 } from "crypto";
2951
+ import { randomUUID as randomUUID5 } from "crypto";
2824
2952
  import { dirname as dirname4, join as join17 } from "path";
2825
2953
  import { mkdir as mkdir10 } from "fs/promises";
2826
2954
  import { homedir as homedir12 } from "os";
@@ -3206,7 +3334,7 @@ function extractPlanFromCodexAspNotification(notification) {
3206
3334
  }
3207
3335
 
3208
3336
  // src/utils/image-utils.ts
3209
- import { randomUUID as randomUUID3 } from "crypto";
3337
+ import { randomUUID as randomUUID4 } from "crypto";
3210
3338
  import { mkdir as mkdir9, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
3211
3339
  import { homedir as homedir11 } from "os";
3212
3340
  import { join as join14 } from "path";
@@ -3315,7 +3443,7 @@ async function saveNormalizedImagesToTempFiles(images, tempImageDir = join14(hom
3315
3443
  try {
3316
3444
  for (const image of images) {
3317
3445
  const ext = image.source.media_type.split("/")[1] || "png";
3318
- const filename = `img_${randomUUID3()}.${ext}`;
3446
+ const filename = `img_${randomUUID4()}.${ext}`;
3319
3447
  const filepath = join14(tempImageDir, filename);
3320
3448
  await writeFile5(filepath, Buffer.from(image.source.data, "base64"));
3321
3449
  tempPaths.push(filepath);
@@ -3385,6 +3513,12 @@ var MessageQueueService = class {
3385
3513
  onProcessingChanged;
3386
3514
  onMessageStarted;
3387
3515
  acceptanceByMessage = /* @__PURE__ */ new WeakMap();
3516
+ // A withdrawal mark implies the entry was present when its acceptance
3517
+ // rejected; intentional removals (remove, clear, drain, steer) can never
3518
+ // coincide with it, so recovery only needs this mark plus the cancellation
3519
+ // version below.
3520
+ acceptanceWithdrawals = /* @__PURE__ */ new WeakSet();
3521
+ queueCancellationVersion = 0;
3388
3522
  constructor(processMessage, onProcessingChanged = () => {
3389
3523
  }, onMessageStarted = async () => {
3390
3524
  }) {
@@ -3403,13 +3537,32 @@ var MessageQueueService = class {
3403
3537
  }) {
3404
3538
  if (this.processing && this.canMergeIntoTail(request)) {
3405
3539
  const tail = this.queue[this.queue.length - 1];
3406
- this.mergeInto(tail, request);
3407
3540
  const response2 = {
3408
3541
  queued: true,
3409
3542
  messageId: tail.id,
3410
3543
  position: this.queue.length
3411
3544
  };
3412
- const acceptance = (this.acceptanceByMessage.get(tail) ?? Promise.resolve()).then(() => onAccepted(response2));
3545
+ const cancellationVersion = this.queueCancellationVersion;
3546
+ const prior = this.acceptanceByMessage.get(tail) ?? Promise.resolve();
3547
+ const acceptance = prior.catch(() => {
3548
+ }).then(async () => {
3549
+ let acceptedMessage;
3550
+ while (true) {
3551
+ const baseMessage = tail.message;
3552
+ acceptedMessage = {
3553
+ ...tail,
3554
+ message: `${baseMessage}${MERGED_MESSAGE_SEPARATOR}${request.message}`,
3555
+ ...tail.images || request.images?.length ? { images: [...tail.images ?? [], ...request.images ?? []] } : {}
3556
+ };
3557
+ await onAccepted(response2, acceptedMessage);
3558
+ if (tail.message === baseMessage) break;
3559
+ }
3560
+ tail.message = acceptedMessage.message;
3561
+ if (acceptedMessage.images) tail.images = [...acceptedMessage.images];
3562
+ if (!this.queue.includes(tail) && this.queueCancellationVersion === cancellationVersion && this.acceptanceWithdrawals.delete(tail)) {
3563
+ this.queue.push(tail);
3564
+ }
3565
+ });
3413
3566
  this.acceptanceByMessage.set(tail, acceptance);
3414
3567
  await acceptance;
3415
3568
  return response2;
@@ -3430,7 +3583,14 @@ var MessageQueueService = class {
3430
3583
  messageId,
3431
3584
  position: this.queue.length
3432
3585
  };
3433
- const acceptance = onAccepted(response2);
3586
+ const acceptance = onAccepted(response2, queuedMessage).catch((error) => {
3587
+ const index = this.queue.indexOf(queuedMessage);
3588
+ if (index !== -1) {
3589
+ this.queue.splice(index, 1);
3590
+ this.acceptanceWithdrawals.add(queuedMessage);
3591
+ }
3592
+ throw error;
3593
+ });
3434
3594
  this.acceptanceByMessage.set(queuedMessage, acceptance);
3435
3595
  await acceptance;
3436
3596
  return response2;
@@ -3440,7 +3600,7 @@ var MessageQueueService = class {
3440
3600
  messageId,
3441
3601
  position: 0
3442
3602
  };
3443
- await onAccepted(response);
3603
+ await onAccepted(response, queuedMessage);
3444
3604
  this.startProcessing(queuedMessage).catch((error) => {
3445
3605
  console.error("[MessageQueue] Unhandled error in startProcessing:", error);
3446
3606
  });
@@ -3452,12 +3612,6 @@ var MessageQueueService = class {
3452
3612
  if (!tail) return false;
3453
3613
  return tail.merge === true && tail.type === request.type;
3454
3614
  }
3455
- mergeInto(tail, request) {
3456
- tail.message = `${tail.message}${MERGED_MESSAGE_SEPARATOR}${request.message}`;
3457
- if (request.images && request.images.length > 0) {
3458
- tail.images = [...tail.images ?? [], ...request.images];
3459
- }
3460
- }
3461
3615
  async startProcessing(queuedMessage) {
3462
3616
  this.processing = true;
3463
3617
  this.onProcessingChanged(true);
@@ -3506,6 +3660,7 @@ var MessageQueueService = class {
3506
3660
  drained.push(text);
3507
3661
  totalChars += text.length;
3508
3662
  }
3663
+ this.queueCancellationVersion++;
3509
3664
  this.queue = [];
3510
3665
  return drained;
3511
3666
  }
@@ -3526,9 +3681,10 @@ var MessageQueueService = class {
3526
3681
  return true;
3527
3682
  }
3528
3683
  clearQueue() {
3529
- if (this.queue.length === 0) return false;
3684
+ const hadMessages = this.queue.length > 0;
3685
+ this.queueCancellationVersion++;
3530
3686
  this.queue = [];
3531
- return true;
3687
+ return hadMessages;
3532
3688
  }
3533
3689
  updateQueuedMessage(messageId, message) {
3534
3690
  const queuedMessage = this.queue.find((item) => item.id === messageId);
@@ -3569,6 +3725,7 @@ var MessageQueueService = class {
3569
3725
  * Reset everything including clearing processing state
3570
3726
  */
3571
3727
  reset() {
3728
+ this.queueCancellationVersion++;
3572
3729
  this.queue = [];
3573
3730
  this.processing = false;
3574
3731
  }
@@ -3647,6 +3804,21 @@ var CodingAgentManager = class {
3647
3804
  this.onEvent(event);
3648
3805
  historyFile.append(event);
3649
3806
  }
3807
+ recordUserMessageHistoryEvent(request, historyFile, extraPayload = {}) {
3808
+ this.recordHistoryEvent(
3809
+ "event_msg",
3810
+ this.createUserMessageHistoryPayload(request, extraPayload),
3811
+ historyFile
3812
+ );
3813
+ }
3814
+ createUserMessageHistoryPayload(request, extraPayload = {}) {
3815
+ return {
3816
+ type: "user_message",
3817
+ message: request.message,
3818
+ ...extraPayload,
3819
+ ...request.messageId ? { [USER_MESSAGE_ID_PAYLOAD_KEY]: request.messageId } : {}
3820
+ };
3821
+ }
3650
3822
  initializeManager(processMessage) {
3651
3823
  this.messageQueue = new MessageQueueService(
3652
3824
  processMessage,
@@ -3655,6 +3827,11 @@ var CodingAgentManager = class {
3655
3827
  );
3656
3828
  this.initialized = this.initialize();
3657
3829
  }
3830
+ async persistAcceptedMessage(event) {
3831
+ const history = this.getHistorySink();
3832
+ if (!history?.appendDurably) throw new Error("Chat history is unavailable");
3833
+ await history.appendDurably(event);
3834
+ }
3658
3835
  async interrupt() {
3659
3836
  const queue = this.messageQueue.drainQueue({
3660
3837
  maxItems: MAX_INTERRUPT_QUEUE_ITEMS,
@@ -4262,12 +4439,13 @@ var CodexHistoryFile = class {
4262
4439
  writeLock = new AsyncLock();
4263
4440
  /** Best-effort ordered append; failures must not disrupt the turn. */
4264
4441
  append(event) {
4265
- void this.writeLock.run(
4266
- () => appendFile2(this.filePath, `${JSON.stringify(event)}
4267
- `).catch((error) => {
4268
- console.error("[CodexHistoryFile] Failed to append event:", error);
4269
- })
4270
- );
4442
+ void this.appendDurably(event).catch((error) => {
4443
+ console.error("[CodexHistoryFile] Failed to append event:", error);
4444
+ });
4445
+ }
4446
+ appendDurably(event) {
4447
+ return this.writeLock.run(() => appendFile2(this.filePath, `${JSON.stringify(event)}
4448
+ `));
4271
4449
  }
4272
4450
  async flush() {
4273
4451
  await this.writeLock.drain();
@@ -4791,7 +4969,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
4791
4969
  if (!handler) {
4792
4970
  return { behavior: "allow" };
4793
4971
  }
4794
- const requestId = randomUUID4();
4972
+ const requestId = randomUUID5();
4795
4973
  const toolUseId = options.toolUseID;
4796
4974
  const { options: requestOptions, questions: requestQuestions } = handler.getRequest(input);
4797
4975
  const result = await new Promise((resolve5) => {
@@ -6462,6 +6640,13 @@ var DuplicateDefaultChatError = class extends Error {
6462
6640
  this.name = "DuplicateDefaultChatError";
6463
6641
  }
6464
6642
  };
6643
+ var ChatMessagePersistenceError = class extends Error {
6644
+ constructor(cause) {
6645
+ super(`Failed to persist user message: ${cause instanceof Error ? cause.message : String(cause)}`);
6646
+ this.name = "ChatMessagePersistenceError";
6647
+ this.cause = cause;
6648
+ }
6649
+ };
6465
6650
 
6466
6651
  // src/managers/codex-asp/codex-asp-manager.ts
6467
6652
  var CodexQuotaError = class extends Error {
@@ -6571,7 +6756,13 @@ var CodexAspManager = class extends CodingAgentManager {
6571
6756
  this.currentThreadId = this.initialSessionId ?? replayed?.transcript?.threadId ?? null;
6572
6757
  }
6573
6758
  getHistorySink() {
6574
- return { append: (event) => this.trackHistoryEvent(event) };
6759
+ return {
6760
+ append: (event) => this.trackHistoryEvent(event),
6761
+ appendDurably: (event) => {
6762
+ if (!this.historyFile) throw new Error("Chat history is unavailable");
6763
+ return this.historyFile.appendDurably(event);
6764
+ }
6765
+ };
6575
6766
  }
6576
6767
  async interruptActiveTurn() {
6577
6768
  if (!this.currentThreadId || !this.activeTurnId) {
@@ -6769,13 +6960,10 @@ var CodexAspManager = class extends CodingAgentManager {
6769
6960
  }
6770
6961
  recordUserMessageEvent(request, extraPayload = {}) {
6771
6962
  const images = imageContentToUserMessageImages(request.images);
6772
- this.recordCodexHistoryEvent("event_msg", {
6773
- type: "user_message",
6774
- message: request.message,
6775
- ...request.messageId ? { [USER_MESSAGE_ID_PAYLOAD_KEY]: request.messageId } : {},
6963
+ this.recordCodexHistoryEvent("event_msg", this.createUserMessageHistoryPayload(request, {
6776
6964
  ...images ? { images } : {},
6777
6965
  ...extraPayload
6778
- });
6966
+ }));
6779
6967
  }
6780
6968
  async processMessageInternal(request) {
6781
6969
  let userMessageRecorded = false;
@@ -8088,10 +8276,7 @@ var CursorManager = class extends CodingAgentManager {
8088
8276
  }
8089
8277
  }, CURSOR_RECONNECT_NOTICE_DELAY_MS);
8090
8278
  agent = await this.ensureAgent(request);
8091
- this.recordHistoryEvent("event_msg", {
8092
- type: "user_message",
8093
- message: request.message
8094
- }, this.historyFile);
8279
+ this.recordUserMessageHistoryEvent(request, this.historyFile);
8095
8280
  run = await agent.send(message, {
8096
8281
  model: { id: model },
8097
8282
  mode: request.planMode ? "plan" : "agent",
@@ -8337,7 +8522,7 @@ ${instructions}
8337
8522
 
8338
8523
  // src/managers/deepseek-manager.ts
8339
8524
  import { spawn as spawn3 } from "child_process";
8340
- import { randomUUID as randomUUID5 } from "crypto";
8525
+ import { randomUUID as randomUUID6 } from "crypto";
8341
8526
  import { createRequire } from "module";
8342
8527
  import { dirname as dirname6, join as join21 } from "path";
8343
8528
  import { fileURLToPath } from "url";
@@ -8379,7 +8564,7 @@ var DeepseekApiClient = class extends AbstractApiClient {
8379
8564
  return this.readWebSocket("/api/events.host", signal, hostFrameSchema, onOpen);
8380
8565
  }
8381
8566
  async executeCommand(sessionId, line) {
8382
- const rpcId = randomUUID5();
8567
+ const rpcId = randomUUID6();
8383
8568
  const response = await this.doFetch(new URL("/api/commands/execute", this.baseUrl), {
8384
8569
  method: "POST",
8385
8570
  headers: { "content-type": "application/json" },
@@ -8492,7 +8677,7 @@ var DeepseekManager = class extends CodingAgentManager {
8492
8677
  mode: "steer",
8493
8678
  content: [{ type: "text", text: request.message }]
8494
8679
  }));
8495
- this.recordHistoryEvent("event_msg", { type: "user_message", message: request.message }, this.historyFile);
8680
+ this.recordUserMessageHistoryEvent(request, this.historyFile);
8496
8681
  return true;
8497
8682
  }
8498
8683
  async getHistory(page = {}) {
@@ -8706,7 +8891,7 @@ var DeepseekManager = class extends CodingAgentManager {
8706
8891
  ...effort ? { reasoningEffort: effort } : {}
8707
8892
  }));
8708
8893
  }
8709
- this.recordHistoryEvent("event_msg", { type: "user_message", message: request.message }, this.historyFile);
8894
+ this.recordUserMessageHistoryEvent(request, this.historyFile);
8710
8895
  this.turnCompletion = new Promise((resolve5) => {
8711
8896
  this.resolveTurnCompletion = resolve5;
8712
8897
  });
@@ -8747,7 +8932,7 @@ import { spawn as spawn5 } from "child_process";
8747
8932
 
8748
8933
  // src/managers/acp-manager.ts
8749
8934
  import { spawn as spawn4 } from "child_process";
8750
- import { randomUUID as randomUUID6 } from "crypto";
8935
+ import { randomUUID as randomUUID7 } from "crypto";
8751
8936
  import { mkdir as mkdir13, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
8752
8937
  import { dirname as dirname7 } from "path";
8753
8938
  import { Writable } from "stream";
@@ -8920,7 +9105,7 @@ var AcpManager = class extends CodingAgentManager {
8920
9105
  createTerminal(params) {
8921
9106
  this.assertSession(params.sessionId);
8922
9107
  if (this.planMode) throw new Error("ACP terminals are disabled in plan mode.");
8923
- const terminalId = randomUUID6();
9108
+ const terminalId = randomUUID7();
8924
9109
  const outputByteLimit = Math.max(1, Math.min(params.outputByteLimit ?? 1048576, 10485760));
8925
9110
  const child = spawn4(params.command, params.args ?? [], {
8926
9111
  cwd: params.cwd ?? this.workingDirectory,
@@ -9051,7 +9236,7 @@ var AcpManager = class extends CodingAgentManager {
9051
9236
  }
9052
9237
  async requestUserInput(params) {
9053
9238
  if (!this.interactiveTools) return { outcome: { outcome: "cancelled" } };
9054
- const requestId = randomUUID6();
9239
+ const requestId = randomUUID7();
9055
9240
  const toolCallId = params.toolCall.toolCallId;
9056
9241
  return new Promise((resolve5) => {
9057
9242
  this.pendingPermissions.set(requestId, { toolCallId, options: params.options, resolve: resolve5 });
@@ -9112,7 +9297,7 @@ var AcpManager = class extends CodingAgentManager {
9112
9297
  const context = this.context;
9113
9298
  const sessionId = this.sessionId;
9114
9299
  if (!context || !sessionId) throw new Error(`${this.acp.command} ACP failed to initialize.`);
9115
- this.recordHistoryEvent("event_msg", { type: "user_message", message: request.message }, this.historyFile);
9300
+ this.recordUserMessageHistoryEvent(request, this.historyFile);
9116
9301
  this.planMode = request.planMode ?? false;
9117
9302
  this.interactiveTools = request.enableInteractiveTools ?? false;
9118
9303
  await this.setConfig("model", model);
@@ -9824,10 +10009,7 @@ var OpencodeManager = class extends CodingAgentManager {
9824
10009
  const variant = await this.getThinkingVariant(client2, providerModel, request.thinkingLevel);
9825
10010
  const sessionId = await this.ensureSession(client2, providerModel, agent, variant);
9826
10011
  const system = this.buildCombinedInstructions(request.customInstructions);
9827
- this.recordHistoryEvent("event_msg", {
9828
- type: "user_message",
9829
- message: request.message
9830
- }, this.historyFile);
10012
+ this.recordUserMessageHistoryEvent(request, this.historyFile);
9831
10013
  const result = await client2.session.prompt({
9832
10014
  sessionID: sessionId,
9833
10015
  directory: this.workingDirectory,
@@ -10261,7 +10443,7 @@ var PiManager = class extends CodingAgentManager {
10261
10443
  const session = this.session;
10262
10444
  if (!session?.isStreaming) return false;
10263
10445
  await session.steer(request.message);
10264
- this.recordUserMessage(session, request.message);
10446
+ this.recordUserMessage(session, request);
10265
10447
  return true;
10266
10448
  }
10267
10449
  async getHistory(page = {}) {
@@ -10386,18 +10568,18 @@ var PiManager = class extends CodingAgentManager {
10386
10568
  const payload = eventPayload(event);
10387
10569
  this.recordHistoryEvent(`pi-${event.type}`, payload, this.historyFile);
10388
10570
  }
10389
- recordUserMessage(session, message) {
10390
- const skillName = explicitSkillName(session, message);
10391
- this.recordHistoryEvent("event_msg", {
10392
- type: "user_message",
10393
- message,
10394
- ...skillName ? { skillName } : {}
10395
- }, this.historyFile);
10571
+ recordUserMessage(session, request) {
10572
+ const skillName = explicitSkillName(session, request.message);
10573
+ this.recordUserMessageHistoryEvent(
10574
+ request,
10575
+ this.historyFile,
10576
+ skillName ? { skillName } : {}
10577
+ );
10396
10578
  }
10397
10579
  async processMessageInternal(request) {
10398
10580
  try {
10399
10581
  const session = await this.ensureSession(request);
10400
- this.recordUserMessage(session, request.message);
10582
+ this.recordUserMessage(session, request);
10401
10583
  const images = request.images && request.images.length > 0 ? (await normalizeImages(request.images)).map((image) => ({
10402
10584
  type: "image",
10403
10585
  data: image.source.data,
@@ -11024,6 +11206,9 @@ var RelayManager = class {
11024
11206
  async enqueueMessage(request, onAccepted) {
11025
11207
  return this.inner.enqueueMessage(request, onAccepted);
11026
11208
  }
11209
+ persistAcceptedMessage(event) {
11210
+ return this.inner.persistAcceptedMessage(event);
11211
+ }
11027
11212
  async interrupt() {
11028
11213
  return this.inner.interrupt();
11029
11214
  }
@@ -11072,7 +11257,7 @@ import {
11072
11257
  unlink as unlink3
11073
11258
  } from "fs/promises";
11074
11259
  import { join as join24 } from "path";
11075
- import { randomUUID as randomUUID7 } from "crypto";
11260
+ import { randomUUID as randomUUID8 } from "crypto";
11076
11261
 
11077
11262
  // src/analytics/agent/activity/skill-mcp-call-extractor.ts
11078
11263
  var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "deepseek", "fx", "kimi", "opencode", "pi", "custom", "dynamic"]);
@@ -11293,7 +11478,7 @@ var AgentChatTurnActivityTracker = class {
11293
11478
  const credential = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[provider];
11294
11479
  this.pendingMessages.delete(messageId);
11295
11480
  this.activeTurns.set(chatId, {
11296
- turnId: randomUUID7(),
11481
+ turnId: randomUUID8(),
11297
11482
  startedAtMs: Date.now(),
11298
11483
  provider,
11299
11484
  model: attributes.model ?? getDefaultAgentModel(provider),
@@ -12125,7 +12310,7 @@ var ChatService = class {
12125
12310
  workingDirectory;
12126
12311
  agentChatActivityTrackerService;
12127
12312
  chats = /* @__PURE__ */ new Map();
12128
- persistInFlight = false;
12313
+ persistPromise = null;
12129
12314
  persistQueued = false;
12130
12315
  async initialize() {
12131
12316
  await mkdir17(ENGINE_DIR2, { recursive: true });
@@ -12191,7 +12376,7 @@ var ChatService = class {
12191
12376
  }
12192
12377
  const existingDefault = isDefaultChat({ provider: request.provider, title }) ? Array.from(this.chats.values()).find((chat) => !chat.persisted.deletedAt && chat.persisted.provider === request.provider && isDefaultChat(chat.persisted)) : void 0;
12193
12378
  if (existingDefault) {
12194
- if (!request.id || hasChatStarted(this.toSummary(existingDefault))) {
12379
+ if (!request.id || existingDefault.acceptingMessages > 0 || hasChatStarted(this.toSummary(existingDefault))) {
12195
12380
  throw new DuplicateDefaultChatError(request.provider);
12196
12381
  }
12197
12382
  this.chats.delete(existingDefault.persisted.id);
@@ -12201,7 +12386,7 @@ var ChatService = class {
12201
12386
  throw new ChatNotFoundError(parentChatId);
12202
12387
  }
12203
12388
  const persisted = {
12204
- id: request.id ?? randomUUID8(),
12389
+ id: request.id ?? randomUUID9(),
12205
12390
  provider: request.provider,
12206
12391
  title,
12207
12392
  createdAt: now,
@@ -12223,42 +12408,94 @@ var ChatService = class {
12223
12408
  }
12224
12409
  async sendMessage(chatId, request) {
12225
12410
  const chat = this.requireChat(chatId);
12226
- if (request.idempotencyKey) {
12227
- const accepted = chat.acceptedSendResponses.get(request.idempotencyKey);
12411
+ const idempotencyKey = request.idempotencyKey ?? request.messageId;
12412
+ if (idempotencyKey) {
12413
+ const accepted = chat.acceptedSendResponses.get(idempotencyKey);
12228
12414
  if (accepted) {
12229
12415
  return accepted;
12230
12416
  }
12417
+ const accepting2 = chat.acceptingSendResponses.get(idempotencyKey);
12418
+ if (accepting2) return accepting2;
12231
12419
  }
12232
- return chat.provider.enqueueMessage(
12233
- request,
12234
- (result) => this.handleMessageAccepted(chatId, chat, request, result)
12235
- );
12236
- }
12237
- async handleMessageAccepted(chatId, chat, request, result) {
12238
- if (request.idempotencyKey) {
12239
- chat.acceptedSendResponses.set(request.idempotencyKey, result);
12240
- for (const key of chat.acceptedSendResponses.keys()) {
12241
- if (chat.acceptedSendResponses.size <= MAX_ACCEPTED_SEND_RESPONSES) break;
12242
- chat.acceptedSendResponses.delete(key);
12420
+ const accepting = (async () => {
12421
+ chat.acceptingMessages += 1;
12422
+ try {
12423
+ return await chat.provider.enqueueMessage(
12424
+ request,
12425
+ (result, acceptedMessage) => this.handleMessageAccepted(
12426
+ chatId,
12427
+ chat,
12428
+ request,
12429
+ acceptedMessage,
12430
+ result
12431
+ )
12432
+ );
12433
+ } finally {
12434
+ chat.acceptingMessages -= 1;
12435
+ }
12436
+ })();
12437
+ if (!idempotencyKey) return accepting;
12438
+ chat.acceptingSendResponses.set(idempotencyKey, accepting);
12439
+ try {
12440
+ return await accepting;
12441
+ } finally {
12442
+ if (chat.acceptingSendResponses.get(idempotencyKey) === accepting) {
12443
+ chat.acceptingSendResponses.delete(idempotencyKey);
12243
12444
  }
12244
- chat.persisted.acceptedSendResponses = Object.fromEntries(chat.acceptedSendResponses);
12245
12445
  }
12446
+ }
12447
+ async handleMessageAccepted(chatId, chat, request, acceptedMessage, result) {
12246
12448
  const submittedAt = request.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12247
12449
  const { event: acceptedEvent, sender: recordedSender } = createTurnMetadata(
12248
- request,
12450
+ acceptedMessage,
12249
12451
  result.messageId,
12250
12452
  submittedAt
12251
12453
  );
12454
+ try {
12455
+ await chat.provider.persistAcceptedMessage(acceptedEvent);
12456
+ } catch (error) {
12457
+ throw new ChatMessagePersistenceError(error);
12458
+ }
12459
+ const previousAcceptedSendResponses = new Map(chat.acceptedSendResponses);
12460
+ const previousPendingMessageIds = [...chat.pendingMessageIds];
12461
+ const previousAcceptedEvent = chat.acceptedUserEvents.get(result.messageId);
12462
+ const previousPersistedResponses = chat.persisted.acceptedSendResponses;
12463
+ const previousLastMessageText = chat.persisted.lastMessageText;
12464
+ const previousUpdatedAt = chat.persisted.updatedAt;
12465
+ const idempotencyKey = request.idempotencyKey ?? request.messageId;
12466
+ if (idempotencyKey) {
12467
+ chat.acceptedSendResponses.set(idempotencyKey, result);
12468
+ for (const key of chat.acceptedSendResponses.keys()) {
12469
+ if (chat.acceptedSendResponses.size <= MAX_ACCEPTED_SEND_RESPONSES) break;
12470
+ chat.acceptedSendResponses.delete(key);
12471
+ }
12472
+ chat.persisted.acceptedSendResponses = Object.fromEntries(chat.acceptedSendResponses);
12473
+ }
12252
12474
  if (!chat.pendingMessageIds.includes(result.messageId)) {
12253
12475
  chat.pendingMessageIds.push(result.messageId);
12254
12476
  }
12477
+ chat.acceptedUserEvents.set(result.messageId, acceptedEvent);
12478
+ chat.persisted.lastMessageText = request.message.trim().slice(0, LAST_MESSAGE_PREVIEW_MAX) || null;
12479
+ chat.persisted.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
12480
+ try {
12481
+ await this.persistAllChats();
12482
+ } catch (error) {
12483
+ chat.acceptedSendResponses.clear();
12484
+ for (const [key, response] of previousAcceptedSendResponses) {
12485
+ chat.acceptedSendResponses.set(key, response);
12486
+ }
12487
+ chat.pendingMessageIds = previousPendingMessageIds;
12488
+ if (previousAcceptedEvent) chat.acceptedUserEvents.set(result.messageId, previousAcceptedEvent);
12489
+ else chat.acceptedUserEvents.delete(result.messageId);
12490
+ chat.persisted.acceptedSendResponses = previousPersistedResponses;
12491
+ chat.persisted.lastMessageText = previousLastMessageText;
12492
+ chat.persisted.updatedAt = previousUpdatedAt;
12493
+ throw new ChatMessagePersistenceError(error);
12494
+ }
12255
12495
  this.agentChatActivityTrackerService.noteMessageAccepted(result.messageId, request);
12256
12496
  if (request.errorNotificationTarget) {
12257
12497
  chat.errorNotificationTargets.set(result.messageId, request.errorNotificationTarget);
12258
12498
  }
12259
- chat.acceptedUserEvents.set(result.messageId, acceptedEvent);
12260
- chat.persisted.lastMessageText = request.message.trim().slice(0, LAST_MESSAGE_PREVIEW_MAX) || null;
12261
- this.touch(chat);
12262
12499
  if (recordedSender) {
12263
12500
  await this.appendSender(chatId, recordedSender);
12264
12501
  }
@@ -12535,15 +12772,16 @@ var ChatService = class {
12535
12772
  chat.provider.getHistory(page),
12536
12773
  this.readSenders(chatId)
12537
12774
  ]);
12775
+ const historyEvents = mergeWorkspaceChatHistoryEvents([], history.events);
12538
12776
  for (const [messageId, acceptedEvent] of chat.acceptedUserEvents) {
12539
- if (acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) || history.events.some((event) => isSameAcceptedUserEvent(event, acceptedEvent))) {
12777
+ if (acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) || historyEvents.some((event) => isSameAcceptedUserEvent(event, acceptedEvent))) {
12540
12778
  chat.acceptedUserEvents.delete(messageId);
12541
12779
  }
12542
12780
  }
12543
12781
  const isLatestPage = isLatestChatHistoryPage(page);
12544
12782
  const queuedMessageIds = new Set(chat.provider.getQueue().map((message) => message.id));
12545
- const acceptedEvents = (isLatestPage ? [...chat.acceptedUserEvents.entries()] : []).filter(([messageId]) => messageId === chat.activeMessageId || !chat.hasActiveTurn && chat.pendingMessageIds[0] === messageId && !queuedMessageIds.has(messageId)).map(([, acceptedEvent]) => acceptedEvent).filter((acceptedEvent) => !acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) && !history.events.some((event) => isSameAcceptedUserEvent(event, acceptedEvent)));
12546
- const events = [...history.events, ...acceptedEvents].sort((a, b) => getEventTimestampMs(a) - getEventTimestampMs(b));
12783
+ const acceptedEvents = (isLatestPage ? [...chat.acceptedUserEvents.entries()] : []).filter(([messageId]) => messageId === chat.activeMessageId || !chat.hasActiveTurn && chat.pendingMessageIds[0] === messageId && !queuedMessageIds.has(messageId)).map(([, acceptedEvent]) => acceptedEvent).filter((acceptedEvent) => !acceptedEventInCodexTranscript(acceptedEvent, history.codexAspTranscript) && !historyEvents.some((event) => isSameAcceptedUserEvent(event, acceptedEvent)));
12784
+ const events = [...historyEvents, ...acceptedEvents].sort((a, b) => getEventTimestampMs(a) - getEventTimestampMs(b));
12547
12785
  if (page.limit !== void 0 && "beforeCursor" in history) {
12548
12786
  const cursor = parseChatHistoryCursor(history.beforeCursor ?? void 0);
12549
12787
  return {
@@ -12561,7 +12799,7 @@ var ChatService = class {
12561
12799
  return {
12562
12800
  ...history,
12563
12801
  events,
12564
- totalEvents: (history.totalEvents ?? history.eventsStartIndex + history.events.length) + acceptedEvents.length,
12802
+ totalEvents: (history.totalEvents ?? history.eventsStartIndex + history.events.length) - (history.events.length - historyEvents.length) + acceptedEvents.length,
12565
12803
  goal: history.goal ?? chat.provider.getGoal?.() ?? null,
12566
12804
  senders
12567
12805
  };
@@ -12760,6 +12998,8 @@ var ChatService = class {
12760
12998
  pendingMessageIds: [],
12761
12999
  acceptedUserEvents: /* @__PURE__ */ new Map(),
12762
13000
  acceptedSendResponses: new Map(Object.entries(persisted.acceptedSendResponses ?? {})),
13001
+ acceptingSendResponses: /* @__PURE__ */ new Map(),
13002
+ acceptingMessages: 0,
12763
13003
  activeMessageId: null,
12764
13004
  hasActiveTurn: false,
12765
13005
  observedBranchesByRepo: /* @__PURE__ */ new Map(),
@@ -12965,35 +13205,33 @@ var ChatService = class {
12965
13205
  }
12966
13206
  }
12967
13207
  }
12968
- async persistAllChats() {
12969
- if (this.persistInFlight) {
13208
+ persistAllChats() {
13209
+ if (this.persistPromise) {
12970
13210
  this.persistQueued = true;
12971
- return;
13211
+ return this.persistPromise;
12972
13212
  }
12973
- this.persistInFlight = true;
12974
- try {
12975
- const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
12976
- try {
12977
- await copyFile(CHATS_FILE, CHATS_BACKUP_FILE);
12978
- } catch (error) {
12979
- if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
12980
- console.error("[ChatService] Failed to update chats backup:", error);
13213
+ this.persistPromise = (async () => {
13214
+ do {
13215
+ this.persistQueued = false;
13216
+ const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
13217
+ try {
13218
+ await copyFile(CHATS_FILE, CHATS_BACKUP_FILE);
13219
+ } catch (error) {
13220
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
13221
+ console.error("[ChatService] Failed to update chats backup:", error);
13222
+ }
12981
13223
  }
12982
- }
12983
- await atomicWriteFile(CHATS_FILE, `${JSON.stringify(payload, null, 2)}
13224
+ await atomicWriteFile(CHATS_FILE, `${JSON.stringify(payload, null, 2)}
12984
13225
  `);
12985
- } catch {
12986
- } finally {
12987
- this.persistInFlight = false;
12988
- if (this.persistQueued) {
12989
- this.persistQueued = false;
12990
- void this.persistAllChats();
12991
- }
12992
- }
13226
+ } while (this.persistQueued);
13227
+ })().finally(() => {
13228
+ this.persistPromise = null;
13229
+ });
13230
+ return this.persistPromise;
12993
13231
  }
12994
13232
  async publish(input) {
12995
13233
  const event = {
12996
- id: randomUUID8(),
13234
+ id: randomUUID9(),
12997
13235
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12998
13236
  ...input
12999
13237
  };
@@ -13735,7 +13973,7 @@ ${combinedScript}` : combinedScript;
13735
13973
  }
13736
13974
 
13737
13975
  // src/services/terminal-service.ts
13738
- import { randomUUID as randomUUID9 } from "crypto";
13976
+ import { randomUUID as randomUUID10 } from "crypto";
13739
13977
  import { existsSync as existsSync10 } from "fs";
13740
13978
  import { spawn as spawn7 } from "node-pty";
13741
13979
  var MAX_REPLAY_CHARS = 1024 * 1024;
@@ -13754,7 +13992,7 @@ var TerminalService = class {
13754
13992
  code: "limit"
13755
13993
  });
13756
13994
  }
13757
- const id = randomUUID9();
13995
+ const id = randomUUID10();
13758
13996
  const shell = process.env.SHELL && existsSync10(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
13759
13997
  const pty = spawn7(shell, ["-l"], {
13760
13998
  name: "xterm-256color",
@@ -14109,6 +14347,9 @@ function createV1Routes(deps) {
14109
14347
  if (details.includes("Chat not found")) {
14110
14348
  return c.json(jsonError("Failed to send message", details), 404);
14111
14349
  }
14350
+ if (error instanceof ChatMessagePersistenceError) {
14351
+ return c.json(jsonError("Failed to save user message", details), 500);
14352
+ }
14112
14353
  return c.json(jsonError("Failed to send message", details), 400);
14113
14354
  }
14114
14355
  });
@@ -15040,7 +15281,7 @@ function startStatusBroadcaster() {
15040
15281
  if (serialized !== previousRepoStatus) {
15041
15282
  previousRepoStatus = serialized;
15042
15283
  eventService.publish({
15043
- id: randomUUID10(),
15284
+ id: randomUUID11(),
15044
15285
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15045
15286
  type: "repo.status.changed",
15046
15287
  payload: { repos }
@@ -15061,7 +15302,7 @@ function startStatusBroadcaster() {
15061
15302
  if (engineStatusJson !== previousEngineStatus) {
15062
15303
  previousEngineStatus = engineStatusJson;
15063
15304
  eventService.publish({
15064
- id: randomUUID10(),
15305
+ id: randomUUID11(),
15065
15306
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15066
15307
  type: "engine.status.changed",
15067
15308
  payload: { status: engineStatus }
@@ -15080,7 +15321,7 @@ function startStatusBroadcaster() {
15080
15321
  previousHookStatus = hookSnapshot;
15081
15322
  if (!lastHooksRunning && hooksRunning) {
15082
15323
  eventService.publish({
15083
- id: randomUUID10(),
15324
+ id: randomUUID11(),
15084
15325
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15085
15326
  type: "hooks.started",
15086
15327
  payload: { running: true, completed: false }
@@ -15089,7 +15330,7 @@ function startStatusBroadcaster() {
15089
15330
  }
15090
15331
  if (hooksRunning) {
15091
15332
  eventService.publish({
15092
- id: randomUUID10(),
15333
+ id: randomUUID11(),
15093
15334
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15094
15335
  type: "hooks.progress",
15095
15336
  payload: { running: true, completed: false }
@@ -15098,7 +15339,7 @@ function startStatusBroadcaster() {
15098
15339
  }
15099
15340
  if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
15100
15341
  eventService.publish({
15101
- id: randomUUID10(),
15342
+ id: randomUUID11(),
15102
15343
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15103
15344
  type: "hooks.completed",
15104
15345
  payload: { running: false, completed: true }
@@ -15107,7 +15348,7 @@ function startStatusBroadcaster() {
15107
15348
  }
15108
15349
  if (lastHooksRunning && !hooksRunning && hooksFailed) {
15109
15350
  eventService.publish({
15110
- id: randomUUID10(),
15351
+ id: randomUUID11(),
15111
15352
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15112
15353
  type: "hooks.failed",
15113
15354
  payload: { running: false, completed: hooksCompleted }
@@ -15115,7 +15356,7 @@ function startStatusBroadcaster() {
15115
15356
  });
15116
15357
  }
15117
15358
  eventService.publish({
15118
- id: randomUUID10(),
15359
+ id: randomUUID11(),
15119
15360
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15120
15361
  type: "hooks.status",
15121
15362
  payload: {
@@ -15170,20 +15411,20 @@ serve(
15170
15411
  }
15171
15412
  const repos = await gitService.listRepos();
15172
15413
  await eventService.publish({
15173
- id: randomUUID10(),
15414
+ id: randomUUID11(),
15174
15415
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15175
15416
  type: "repo.discovered",
15176
15417
  payload: { repos }
15177
15418
  });
15178
15419
  const repoStatuses = await gitService.listRepos();
15179
15420
  await eventService.publish({
15180
- id: randomUUID10(),
15421
+ id: randomUUID11(),
15181
15422
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15182
15423
  type: "repo.status.changed",
15183
15424
  payload: { repos: repoStatuses }
15184
15425
  });
15185
15426
  await eventService.publish({
15186
- id: randomUUID10(),
15427
+ id: randomUUID11(),
15187
15428
  ts: (/* @__PURE__ */ new Date()).toISOString(),
15188
15429
  type: "engine.ready",
15189
15430
  payload: { version: "v1" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.697",
3
+ "version": "0.1.698",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",