openmates 0.27.0-alpha.80 → 0.27.0-alpha.81

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.
@@ -778,6 +778,45 @@ function assertCodexClaimCaller(threadId, currentThread = process.env.CODEX_THRE
778
778
  }
779
779
  }
780
780
 
781
+ // src/recoveryBatch.ts
782
+ async function runRecoveryBatch(items, work, dependsOn = () => false) {
783
+ const results = new Array(items.length);
784
+ const pending = new Set(items.map((_, index) => index));
785
+ const complete = /* @__PURE__ */ new Set();
786
+ const active = /* @__PURE__ */ new Map();
787
+ let failed = false;
788
+ let failure2;
789
+ while (pending.size || active.size) {
790
+ if (!failed) for (const index of pending) {
791
+ if (active.size >= 4) break;
792
+ if (items.some((dependency, other) => other !== index && !complete.has(other) && dependsOn(items[index], dependency, index, other))) continue;
793
+ pending.delete(index);
794
+ const task = Promise.resolve().then(() => work(items[index], index)).then(
795
+ (result) => {
796
+ results[index] = result;
797
+ complete.add(index);
798
+ },
799
+ (error) => {
800
+ if (!failed) {
801
+ failed = true;
802
+ failure2 = error;
803
+ }
804
+ }
805
+ ).finally(() => {
806
+ active.delete(index);
807
+ });
808
+ active.set(index, task);
809
+ }
810
+ if (!active.size) {
811
+ if (failed) throw failure2;
812
+ throw new Error("Recovery embed dependency is cyclic or unresolved; outputs remain pending.");
813
+ }
814
+ await Promise.race(active.values());
815
+ if (failed && !active.size) throw failure2;
816
+ }
817
+ return results;
818
+ }
819
+
781
820
  // src/preparedPrivacy.ts
782
821
  var PREPARED_PRIVACY_IDLE_MS = 2 * 60 * 6e4;
783
822
  var PreparedPrivacyCache = class {
@@ -13605,6 +13644,39 @@ var OpenMatesClient = class _OpenMatesClient {
13605
13644
  chatActivitySocket = null;
13606
13645
  preparedSendSocket = null;
13607
13646
  preparedSendSocketAttemptOwner = null;
13647
+ foregroundSendCount = 0;
13648
+ pendingSendSocketRearm = null;
13649
+ cancelSendSocketRearm() {
13650
+ const pending = this.pendingSendSocketRearm;
13651
+ this.pendingSendSocketRearm = null;
13652
+ pending?.removeClose();
13653
+ }
13654
+ maybeRearmSendSocket() {
13655
+ const pending = this.pendingSendSocketRearm;
13656
+ if (!pending || this.foregroundSendCount > 0 || pending.ws.isOpen()) return;
13657
+ this.cancelSendSocketRearm();
13658
+ if (!this.interactiveViewerAllowed || !this.preparedChatOwnerCurrent(pending.owner, pending.teamId) || pending.ownerKey !== this.sendSocketOwnerKey(this.requireSession(), pending.teamId) || authCookieFingerprint(this.http.getCookieMap()) !== authCookieFingerprint(this.requireSession().cookies)) return;
13659
+ this.preparedSendSocketAttemptOwner = null;
13660
+ this.warmSendSocket();
13661
+ }
13662
+ rearmSendSocketWhenClosed(ws, teamId) {
13663
+ if (!this.interactiveViewerAllowed || !this.hasSession()) return;
13664
+ this.cancelSendSocketRearm();
13665
+ const session = this.requireSession();
13666
+ const pending = {
13667
+ ws,
13668
+ teamId,
13669
+ owner: this.preparedChatOwner(session, teamId),
13670
+ ownerKey: this.sendSocketOwnerKey(session, teamId),
13671
+ removeClose: () => {
13672
+ }
13673
+ };
13674
+ this.pendingSendSocketRearm = pending;
13675
+ pending.removeClose = ws.onClose(() => {
13676
+ this.maybeRearmSendSocket();
13677
+ });
13678
+ this.maybeRearmSendSocket();
13679
+ }
13608
13680
  sendSocketOwnerKey(session, teamId) {
13609
13681
  return JSON.stringify([
13610
13682
  this.preparedChatOwner(session, teamId),
@@ -13631,7 +13703,7 @@ var OpenMatesClient = class _OpenMatesClient {
13631
13703
  }
13632
13704
  /** Prepare one authenticated send socket for the interactive TUI's next turn. */
13633
13705
  warmSendSocket() {
13634
- if (!this.interactiveViewerAllowed || this.explicitSession || !this.hasSession()) return;
13706
+ if (!this.interactiveViewerAllowed || this.foregroundSendCount > 0 || this.explicitSession || !this.hasSession()) return;
13635
13707
  const session = this.requireSession();
13636
13708
  const teamId = this.getActiveTeamId();
13637
13709
  const owner2 = this.preparedChatOwner(session, teamId);
@@ -15562,14 +15634,32 @@ var OpenMatesClient = class _OpenMatesClient {
15562
15634
  }
15563
15635
  return { bySubject, acknowledged: /* @__PURE__ */ new Map(), visiting: /* @__PURE__ */ new Set() };
15564
15636
  }
15637
+ captureRecoveryOwner() {
15638
+ const account = this.captureTeamAccountOwner();
15639
+ const team = this.getActiveTeamId();
15640
+ const master = Buffer.from(this.getMasterKeyBytes());
15641
+ return () => {
15642
+ if (!this.hasSession() || !account.isCurrent() || this.getActiveTeamId() !== team || !master.equals(Buffer.from(this.getMasterKeyBytes()))) {
15643
+ throw new Error("Recovery account, Team or key changed; outputs remain pending.");
15644
+ }
15645
+ };
15646
+ }
15565
15647
  async persistAvailableRecoveryOutputs(ws, ownerId2, cache, outputs, teamId, dependencies = this.recoveryReplayDependencies([outputs])) {
15566
15648
  if (outputs.length > 100) throw new Error("Recovery output discovery exceeded the bounded batch size.");
15567
15649
  const order = { message: 0, embed: 1, diff: 2, summary: 3, checkpoint: 4 };
15568
15650
  const ordered = [...outputs].sort((a, b) => order[a.output_kind] - order[b.output_kind] || (a.output_kind === "message" && b.output_kind === "message" ? (a.message_role === "user" ? 0 : 1) - (b.message_role === "user" ? 0 : 1) : 0) || a.output_version - b.output_version);
15651
+ const assertOwner = this.captureRecoveryOwner();
15569
15652
  const masterKey = this.getMasterKeyBytes();
15570
15653
  const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
15654
+ const http = { get: async (path2, headers) => {
15655
+ assertOwner();
15656
+ const result = await this.http.get(path2, headers);
15657
+ assertOwner();
15658
+ return result;
15659
+ } };
15571
15660
  let acknowledged = 0;
15572
15661
  const request = async (type, responseType, recordId, payload) => {
15662
+ assertOwner();
15573
15663
  const requestId = randomUUID9();
15574
15664
  const response = ws.waitForMessage(responseType, (value) => {
15575
15665
  const frame = value;
@@ -15577,10 +15667,14 @@ var OpenMatesClient = class _OpenMatesClient {
15577
15667
  }, 2e4);
15578
15668
  void response.catch(() => {
15579
15669
  });
15670
+ assertOwner();
15580
15671
  await ws.sendAsync(type, { protocol_version: 1, record_id: recordId, request_id: requestId, ...payload });
15581
- return (await response).payload;
15672
+ const received = await response;
15673
+ assertOwner();
15674
+ return received.payload;
15582
15675
  };
15583
15676
  const storeWithReceipt = async (type, responseType, embedId, payload, recoveryRecordId, version2) => {
15677
+ assertOwner();
15584
15678
  const requestId = randomUUID9();
15585
15679
  const receipt = ws.waitForMessage(
15586
15680
  responseType,
@@ -15589,8 +15683,11 @@ var OpenMatesClient = class _OpenMatesClient {
15589
15683
  );
15590
15684
  void receipt.catch(() => {
15591
15685
  });
15686
+ assertOwner();
15592
15687
  await ws.sendAsync(type, { ...payload, request_id: requestId, recovery_record_id: recoveryRecordId });
15593
- return (await receipt).payload;
15688
+ const received = await receipt;
15689
+ assertOwner();
15690
+ return received.payload;
15594
15691
  };
15595
15692
  const reconcileAcknowledgedEmbed = async (output, chatKey, sealedContent) => {
15596
15693
  if (output.output_kind !== "embed" && output.output_kind !== "diff") {
@@ -15613,7 +15710,7 @@ var OpenMatesClient = class _OpenMatesClient {
15613
15710
  throw new Error(`Recovery output ${output.record_id} lacks verified canonical acknowledgement.`);
15614
15711
  }
15615
15712
  const embedPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(output.subject_id)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
15616
- const canonical = await this.http.get(embedPath, this.getCliRequestHeaders());
15713
+ const canonical = await http.get(embedPath, this.getCliRequestHeaders());
15617
15714
  const row3 = canonical.data?.embed;
15618
15715
  if (!canonical.ok || !row3 || row3.hashed_chat_id !== computeSHA256(output.target_chat_id) || typeof row3.encrypted_content !== "string" || typeof row3.encrypted_type !== "string") {
15619
15716
  throw new Error(`Recovery output ${output.record_id} canonical embed is unavailable.`);
@@ -15638,7 +15735,7 @@ var OpenMatesClient = class _OpenMatesClient {
15638
15735
  } else {
15639
15736
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15640
15737
  if (teamId) query.set("team_id", teamId);
15641
- const history = await this.http.get(
15738
+ const history = await http.get(
15642
15739
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15643
15740
  this.getCliRequestHeaders()
15644
15741
  );
@@ -15652,12 +15749,14 @@ var OpenMatesClient = class _OpenMatesClient {
15652
15749
  if (typeof encryptedPatch === "string") await decryptWithAesGcmCombined(encryptedPatch, embedKey);
15653
15750
  }
15654
15751
  };
15655
- for (const output of ordered) {
15752
+ const rootKeys = /* @__PURE__ */ new Map();
15753
+ const prepareOne = async (output) => {
15754
+ assertOwner();
15656
15755
  const identity3 = recoveryOutputIdentity(output);
15657
15756
  const committed = dependencies.acknowledged.get(output.record_id);
15658
15757
  if (committed !== void 0) {
15659
15758
  if (committed !== identity3) throw new Error("Acknowledged recovery dependency changed identity.");
15660
- continue;
15759
+ return null;
15661
15760
  }
15662
15761
  if (dependencies.visiting.has(output.record_id) || dependencies.visiting.size >= 32) {
15663
15762
  throw new Error("Recovery embed dependency is cyclic or exceeds the bounded depth.");
@@ -15665,26 +15764,35 @@ var OpenMatesClient = class _OpenMatesClient {
15665
15764
  if (output.root_hashed_team_id !== void 0 && output.root_hashed_team_id !== (teamId ? computeSHA256(teamId) : null)) {
15666
15765
  throw new Error(`Recovery output ${output.record_id} Team key context is unavailable; output remains pending.`);
15667
15766
  }
15668
- let root = cache.chats.find((chat) => String(chat.details.id ?? "") === output.root_chat_id);
15669
- let rootKey = root ? await this.resolveChatKey(cache, root, wrappingKey, teamId) : null;
15670
- if (!rootKey) {
15671
- const response = await this.http.get(
15672
- `/v1/chats/${encodeURIComponent(output.root_chat_id)}/wrappers/window` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : ""),
15673
- this.getCliRequestHeaders()
15674
- );
15675
- if (!response.ok) throw new Error(`Recovery root key lookup failed (HTTP ${response.status}); output remains pending.`);
15676
- const keyType = teamId ? "team" : "master";
15677
- const wrapper = (response.data.wrappers ?? []).find((item) => item.key_type === keyType && typeof item.encrypted_chat_key === "string" && item.encrypted_chat_key);
15678
- if (!wrapper) throw new Error(`Recovery root chat ${output.root_chat_id} key wrapper is unavailable.`);
15679
- cache.chatKeyWrappers ??= [];
15680
- cache.chatKeyWrappers.push(wrapper);
15681
- if (!root) {
15682
- root = { details: { id: output.root_chat_id, encrypted_chat_key: wrapper.encrypted_chat_key }, messages: [] };
15683
- cache.chats.push(root);
15684
- }
15685
- rootKey = await this.resolveChatKey(cache, root, wrappingKey, teamId);
15686
- }
15687
- if (!rootKey) throw new Error(`Recovery root chat ${output.root_chat_id} key is unavailable.`);
15767
+ let rootKeyPromise = rootKeys.get(output.root_chat_id);
15768
+ if (!rootKeyPromise) {
15769
+ rootKeyPromise = (async () => {
15770
+ let root = cache.chats.find((chat) => String(chat.details.id ?? "") === output.root_chat_id);
15771
+ let rootKey2 = root ? await this.resolveChatKey(cache, root, wrappingKey, teamId) : null;
15772
+ if (!rootKey2) {
15773
+ const response = await http.get(
15774
+ `/v1/chats/${encodeURIComponent(output.root_chat_id)}/wrappers/window` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : ""),
15775
+ this.getCliRequestHeaders()
15776
+ );
15777
+ if (!response.ok) throw new Error(`Recovery root key lookup failed (HTTP ${response.status}); output remains pending.`);
15778
+ const keyType = teamId ? "team" : "master";
15779
+ const wrapper = (response.data.wrappers ?? []).find((item) => item.key_type === keyType && typeof item.encrypted_chat_key === "string" && item.encrypted_chat_key);
15780
+ if (!wrapper) throw new Error(`Recovery root chat ${output.root_chat_id} key wrapper is unavailable.`);
15781
+ cache.chatKeyWrappers ??= [];
15782
+ cache.chatKeyWrappers.push(wrapper);
15783
+ if (!root) {
15784
+ root = { details: { id: output.root_chat_id, encrypted_chat_key: wrapper.encrypted_chat_key }, messages: [] };
15785
+ cache.chats.push(root);
15786
+ }
15787
+ rootKey2 = await this.resolveChatKey(cache, root, wrappingKey, teamId);
15788
+ }
15789
+ if (!rootKey2) throw new Error(`Recovery root chat ${output.root_chat_id} key is unavailable.`);
15790
+ return rootKey2;
15791
+ })();
15792
+ rootKeys.set(output.root_chat_id, rootKeyPromise);
15793
+ }
15794
+ const rootKey = await rootKeyPromise;
15795
+ assertOwner();
15688
15796
  let ready2;
15689
15797
  try {
15690
15798
  ready2 = await request("recovery_output_get", "recovery_output_ready", output.record_id, {});
@@ -15693,7 +15801,7 @@ var OpenMatesClient = class _OpenMatesClient {
15693
15801
  await reconcileAcknowledgedEmbed(output, rootKey);
15694
15802
  dependencies.acknowledged.set(output.record_id, identity3);
15695
15803
  acknowledged += 1;
15696
- continue;
15804
+ return null;
15697
15805
  }
15698
15806
  for (const field3 of ["record_id", "root_chat_id", "target_chat_id", "turn_id", "subject_id", "output_kind", "output_version", "chat_key_version"]) {
15699
15807
  if (ready2[field3] !== output[field3]) throw new Error(`Recovery output ${output.record_id} identity mismatch: ${field3}.`);
@@ -15729,6 +15837,27 @@ var OpenMatesClient = class _OpenMatesClient {
15729
15837
  throw new Error("Recovery output plaintext identity mismatch.");
15730
15838
  }
15731
15839
  const content = recovered.content;
15840
+ assertOwner();
15841
+ if (typeof content.parent_embed_id === "string") {
15842
+ const candidates = (dependencies.bySubject.get(content.parent_embed_id) ?? []).filter((candidate) => candidate.output_kind === "embed");
15843
+ if (candidates.length && !candidates.some((candidate) => candidate.output_kind === "embed" && candidate.root_chat_id === output.root_chat_id && candidate.target_chat_id === output.target_chat_id && candidate.turn_id === output.turn_id && candidate.chat_key_version === output.chat_key_version && (candidate.root_hashed_team_id ?? null) === (output.root_hashed_team_id ?? null))) {
15844
+ const parentPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(content.parent_embed_id)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
15845
+ const parent = await http.get(parentPath, this.getCliRequestHeaders());
15846
+ if (!parent.ok) throw new Error("Canonical recovery parent embed key scope was unavailable.");
15847
+ }
15848
+ }
15849
+ return { output, ready: ready2, content, rootKey, identity: identity3 };
15850
+ };
15851
+ const prepared = [];
15852
+ for (const output of ordered) prepared.push(await prepareOne(output));
15853
+ const persistOne = async ({ output, ready: ready2, content, rootKey, identity: identity3 }) => {
15854
+ assertOwner();
15855
+ const committed = dependencies.acknowledged.get(output.record_id);
15856
+ if (committed !== void 0) {
15857
+ if (committed !== identity3) throw new Error("Acknowledged recovery dependency changed identity.");
15858
+ return;
15859
+ }
15860
+ const messageRole = output.message_role ?? "assistant";
15732
15861
  const childKey = rootKey;
15733
15862
  const now = Math.floor(Date.now() / 1e3);
15734
15863
  const encryptedChatKey = typeof ready2.encrypted_chat_key === "string" && ready2.encrypted_chat_key ? ready2.encrypted_chat_key : await encryptBytesWithAesGcm(childKey, wrappingKey);
@@ -15803,6 +15932,7 @@ var OpenMatesClient = class _OpenMatesClient {
15803
15932
  }, 2e4);
15804
15933
  void stored.catch(() => {
15805
15934
  });
15935
+ assertOwner();
15806
15936
  await ws.sendAsync("store_chat_compression_checkpoint", {
15807
15937
  chat_id: output.target_chat_id,
15808
15938
  checkpoint_id: output.subject_id,
@@ -15830,7 +15960,7 @@ var OpenMatesClient = class _OpenMatesClient {
15830
15960
  } else {
15831
15961
  if (content.embed_id !== output.subject_id) throw new Error("Recovery embed identity mismatch.");
15832
15962
  const embedPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(output.subject_id)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
15833
- let canonicalBefore = await this.http.get(
15963
+ let canonicalBefore = await http.get(
15834
15964
  embedPath,
15835
15965
  this.getCliRequestHeaders()
15836
15966
  );
@@ -15929,7 +16059,7 @@ var OpenMatesClient = class _OpenMatesClient {
15929
16059
  }
15930
16060
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15931
16061
  if (teamId) query.set("team_id", teamId);
15932
- const existing = await this.http.get(
16062
+ const existing = await http.get(
15933
16063
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15934
16064
  this.getCliRequestHeaders()
15935
16065
  );
@@ -15985,7 +16115,7 @@ var OpenMatesClient = class _OpenMatesClient {
15985
16115
  } else {
15986
16116
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15987
16117
  if (teamId) query.set("team_id", teamId);
15988
- const history = await this.http.get(
16118
+ const history = await http.get(
15989
16119
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15990
16120
  this.getCliRequestHeaders()
15991
16121
  );
@@ -16106,7 +16236,7 @@ var OpenMatesClient = class _OpenMatesClient {
16106
16236
  }
16107
16237
  const keyPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(keySubject)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
16108
16238
  const bootstrapRootKeys = output.output_kind === "embed" && !parentId && !canonicalBefore.ok && canonicalBefore.status === 404;
16109
- const keyPage = bootstrapRootKeys ? null : keySubject === output.subject_id && canonicalBefore.ok ? canonicalBefore : parentPage ?? await this.http.get(
16239
+ const keyPage = bootstrapRootKeys ? null : keySubject === output.subject_id && canonicalBefore.ok ? canonicalBefore : parentPage ?? await http.get(
16110
16240
  keyPath,
16111
16241
  this.getCliRequestHeaders()
16112
16242
  );
@@ -16133,20 +16263,24 @@ var OpenMatesClient = class _OpenMatesClient {
16133
16263
  }
16134
16264
  ].filter((key) => !existingTypes.has(key.key_type));
16135
16265
  if (keys2.length > 0) {
16266
+ assertOwner();
16136
16267
  const requestId = randomUUID9();
16137
16268
  const receipt = ws.waitForMessage("store_embed_keys_confirmed", (value) => value.request_id === requestId, 3e4);
16138
16269
  void receipt.catch(() => {
16139
16270
  });
16271
+ assertOwner();
16140
16272
  await ws.sendAsync("store_embed_keys", {
16141
16273
  request_id: requestId,
16142
16274
  recovery_record_id: output.record_id,
16143
16275
  keys: keys2
16144
16276
  });
16145
- const result = (await receipt).payload;
16277
+ const received = await receipt;
16278
+ assertOwner();
16279
+ const result = received.payload;
16146
16280
  if (result.failed_count !== 0 || result.created_count !== keys2.length || result.requested_count !== keys2.length) {
16147
16281
  throw new Error("Recovery embed key wrappers were not exactly confirmed.");
16148
16282
  }
16149
- const durable = await this.http.get(
16283
+ const durable = await http.get(
16150
16284
  keyPath,
16151
16285
  this.getCliRequestHeaders()
16152
16286
  );
@@ -16156,7 +16290,7 @@ var OpenMatesClient = class _OpenMatesClient {
16156
16290
  throw new Error("Recovery embed key wrappers are not both durable.");
16157
16291
  }
16158
16292
  }
16159
- const canonicalAfter = await this.http.get(
16293
+ const canonicalAfter = await http.get(
16160
16294
  embedPath,
16161
16295
  this.getCliRequestHeaders()
16162
16296
  );
@@ -16171,7 +16305,7 @@ var OpenMatesClient = class _OpenMatesClient {
16171
16305
  } else {
16172
16306
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
16173
16307
  if (teamId) query.set("team_id", teamId);
16174
- const history = await this.http.get(
16308
+ const history = await http.get(
16175
16309
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
16176
16310
  this.getCliRequestHeaders()
16177
16311
  );
@@ -16195,7 +16329,14 @@ var OpenMatesClient = class _OpenMatesClient {
16195
16329
  }
16196
16330
  }
16197
16331
  dependencies.acknowledged.set(output.record_id, identity3);
16332
+ assertOwner();
16198
16333
  acknowledged += 1;
16334
+ };
16335
+ for (const kind of ["message", "embed", "diff", "summary", "checkpoint"]) {
16336
+ const group = prepared.filter((item) => item !== null && item.output.output_kind === kind);
16337
+ if (kind === "embed" || kind === "diff") {
16338
+ await runRecoveryBatch(group, persistOne, (item, dependency, index, other) => item.output.subject_id === dependency.output.subject_id && other < index || item.content.parent_embed_id === dependency.output.subject_id && item.output.root_chat_id === dependency.output.root_chat_id && item.output.target_chat_id === dependency.output.target_chat_id && item.output.turn_id === dependency.output.turn_id && item.output.chat_key_version === dependency.output.chat_key_version && (item.output.root_hashed_team_id ?? null) === (dependency.output.root_hashed_team_id ?? null) || typeof item.content.parent_embed_id === "string" && item.content.parent_embed_id === dependency.content.parent_embed_id && other < index && !group.some((candidate) => candidate.output.subject_id === item.content.parent_embed_id));
16339
+ } else for (const item of group) await persistOne(item);
16199
16340
  }
16200
16341
  return acknowledged;
16201
16342
  }
@@ -16321,1651 +16462,1555 @@ var OpenMatesClient = class _OpenMatesClient {
16321
16462
  }
16322
16463
  }
16323
16464
  async sendMessage(params) {
16324
- const preWsStartedAt = performance.now();
16325
- const traceSendTiming = (phase) => {
16326
- if (process.env.OPENMATES_TRACE_SEND_TIMING === "1") {
16327
- console.error(`[chat send timing] phase=${phase} elapsed_ms=${Math.round(performance.now() - preWsStartedAt)}`);
16328
- }
16329
- };
16330
- traceSendTiming("send_begin");
16331
- if (this.preparedSendSocket && !this.preparedSendSocket.opened) this.closePreparedSendSocket();
16332
- const teamId = this.resolveTeamContext({ teamId: params.teamId, personal: params.personal });
16333
- const teamSenderName = teamId ? await this.ownTeamSenderName(teamId) : "User";
16334
- if (params.chatId && params.newChatId) {
16335
- throw new Error("chatId and newChatId cannot be used together.");
16336
- }
16337
- if (params.newChatId && !CANONICAL_UUID_PATTERN.test(params.newChatId)) {
16338
- throw new Error("newChatId must be a canonical UUID.");
16339
- }
16340
- if (params.incognito && params.projectId) {
16341
- throw new Error("Project file execution requires a saved chat.");
16342
- }
16343
- const ownsPreparedId = Boolean(params.newChatId && this.preparedChatKey?.chatId === params.newChatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId));
16344
- const sdkMintedChatId = params.chatId === void 0 && params.projectId === void 0 && (params.newChatId === void 0 || ownsPreparedId);
16345
- let chatId;
16346
- if (params.newChatId) {
16347
- chatId = params.newChatId;
16348
- } else if (!params.chatId) {
16349
- chatId = randomUUID9();
16350
- } else if (!CANONICAL_UUID_PATTERN.test(params.chatId)) {
16351
- const resolved = await this.resolveFullChatId(params.chatId, { teamId });
16352
- if (!resolved) {
16353
- throw new Error(
16354
- `Chat not found for '${params.chatId}'. Use a slug, full UUID, or the first 8 characters of an existing chat ID.`
16355
- );
16356
- }
16357
- chatId = resolved;
16358
- } else {
16359
- chatId = params.chatId;
16360
- }
16361
- const historyAbort = new AbortController();
16362
- let savedPrivacyHistoryRead = null;
16363
- let historySnapshotOwner = null;
16364
- const readSavedPrivacyHistory = () => {
16365
- if (!savedPrivacyHistoryRead) {
16366
- savedPrivacyHistoryRead = withHttpAbortSignal(historyAbort.signal, () => this.getChatMessages(
16465
+ this.cancelSendSocketRearm();
16466
+ this.foregroundSendCount++;
16467
+ try {
16468
+ const preWsStartedAt = performance.now();
16469
+ const traceSendTiming = (phase) => {
16470
+ if (process.env.OPENMATES_TRACE_SEND_TIMING === "1") {
16471
+ console.error(`[chat send timing] phase=${phase} elapsed_ms=${Math.round(performance.now() - preWsStartedAt)}`);
16472
+ }
16473
+ };
16474
+ traceSendTiming("send_begin");
16475
+ if (this.preparedSendSocket && !this.preparedSendSocket.opened) this.closePreparedSendSocket();
16476
+ const teamId = this.resolveTeamContext({ teamId: params.teamId, personal: params.personal });
16477
+ const teamSenderName = teamId ? await this.ownTeamSenderName(teamId) : "User";
16478
+ if (params.chatId && params.newChatId) {
16479
+ throw new Error("chatId and newChatId cannot be used together.");
16480
+ }
16481
+ if (params.newChatId && !CANONICAL_UUID_PATTERN.test(params.newChatId)) {
16482
+ throw new Error("newChatId must be a canonical UUID.");
16483
+ }
16484
+ if (params.incognito && params.projectId) {
16485
+ throw new Error("Project file execution requires a saved chat.");
16486
+ }
16487
+ const ownsPreparedId = Boolean(params.newChatId && this.preparedChatKey?.chatId === params.newChatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId));
16488
+ const sdkMintedChatId = params.chatId === void 0 && params.projectId === void 0 && (params.newChatId === void 0 || ownsPreparedId);
16489
+ let chatId;
16490
+ if (params.newChatId) {
16491
+ chatId = params.newChatId;
16492
+ } else if (!params.chatId) {
16493
+ chatId = randomUUID9();
16494
+ } else if (!CANONICAL_UUID_PATTERN.test(params.chatId)) {
16495
+ const resolved = await this.resolveFullChatId(params.chatId, { teamId });
16496
+ if (!resolved) {
16497
+ throw new Error(
16498
+ `Chat not found for '${params.chatId}'. Use a slug, full UUID, or the first 8 characters of an existing chat ID.`
16499
+ );
16500
+ }
16501
+ chatId = resolved;
16502
+ } else {
16503
+ chatId = params.chatId;
16504
+ }
16505
+ const historyAbort = new AbortController();
16506
+ let savedPrivacyHistoryRead = null;
16507
+ let historySnapshotOwner = null;
16508
+ const readSavedPrivacyHistory = () => {
16509
+ if (!savedPrivacyHistoryRead) {
16510
+ savedPrivacyHistoryRead = withHttpAbortSignal(historyAbort.signal, () => this.getChatMessages(
16511
+ chatId,
16512
+ { personal: !teamId, teamId, freshChatOnly: true, signal: historyAbort.signal }
16513
+ )).then((history) => {
16514
+ historySnapshotOwner = this.preparedChatOwner(this.requireSession(), teamId);
16515
+ return history;
16516
+ });
16517
+ void savedPrivacyHistoryRead.catch(() => {
16518
+ });
16519
+ }
16520
+ return savedPrivacyHistoryRead;
16521
+ };
16522
+ const joinSavedPrivacyHistory = async () => {
16523
+ const history = await readSavedPrivacyHistory();
16524
+ if (!historySnapshotOwner || !this.preparedChatOwnerCurrent(historySnapshotOwner, teamId)) {
16525
+ throw new Error("Chat workspace changed during history preparation.");
16526
+ }
16527
+ return history;
16528
+ };
16529
+ try {
16530
+ let finalMessage = params.message;
16531
+ if (hasRememberMessageReference(finalMessage)) {
16532
+ const rememberedHistory = (params.messageHistory ?? []).map((message) => ({ id: message.message_id, content: message.content })).filter((message) => Boolean(message.id) && typeof message.content === "string");
16533
+ finalMessage = rewriteRememberMessageReferences(finalMessage, rememberedHistory);
16534
+ }
16535
+ if (params.chatId && !params.incognito && hasRememberMessageReference(finalMessage)) {
16536
+ const { messages } = await joinSavedPrivacyHistory();
16537
+ finalMessage = rewriteRememberMessageReferences(finalMessage, messages);
16538
+ }
16539
+ const shouldWaitForAi = shouldWaitForTeamAi(finalMessage, teamId);
16540
+ const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
16541
+ const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
16542
+ const sendSocketOptions = {
16543
+ taskUpdateJobs: taskUpdateJobsEnabled,
16544
+ projectFileJobs: !params.incognito,
16545
+ remoteCommandJobs: !params.incognito,
16546
+ interactiveHuman: params.interactiveHuman === true && this.interactiveViewerAllowed && process.stdin.isTTY === true && process.stdout.isTTY === true
16547
+ };
16548
+ const independentKeyOwner = this.preparedChatOwner(this.requireSession(), teamId);
16549
+ const independentNewChatKey = !params.incognito && !params.chatId ? (async () => {
16550
+ if (ownsPreparedId && this.preparedChatKey?.chatId === chatId) {
16551
+ const prepared = await this.preparedChatKey.value;
16552
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(prepared.wrappingKey)).digest("hex");
16553
+ return prepared;
16554
+ }
16555
+ const wrappingKey = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
16556
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(wrappingKey)).digest("hex");
16557
+ const key = new Uint8Array(randomBytes4(32));
16558
+ return { key, wrappingKey, encryptedKey: await encryptBytesWithAesGcm(key, wrappingKey) };
16559
+ })() : null;
16560
+ void independentNewChatKey?.catch(() => {
16561
+ });
16562
+ const independentRecoveryKeypair = independentNewChatKey?.then((prepared) => deriveChatCompletionRecoveryKeypair(
16563
+ Buffer.from(prepared.key).toString("base64url"),
16367
16564
  chatId,
16368
- { personal: !teamId, teamId, freshChatOnly: true, signal: historyAbort.signal }
16369
- )).then((history) => {
16370
- historySnapshotOwner = this.preparedChatOwner(this.requireSession(), teamId);
16371
- return history;
16565
+ 1
16566
+ ));
16567
+ void independentRecoveryKeypair?.catch(() => {
16372
16568
  });
16373
- void savedPrivacyHistoryRead.catch(() => {
16569
+ const privacyTurnId = params.incognito ? null : randomUUID9();
16570
+ let conditionalPrivacyReceipt = null;
16571
+ let compiledPrivacy;
16572
+ let memorySocket = null;
16573
+ let socketSnapshotAttempted = false;
16574
+ let socketSnapshotResultRows = null;
16575
+ let socketProjectCandidateRows = null;
16576
+ let verifiedProjectCandidates = null;
16577
+ let deferredProjectDirectory = false;
16578
+ let usePersonalRoutingDirectory = false;
16579
+ let projectCandidatesReadyFromSendStartMs = null;
16580
+ let socketMemoryReceipt = null;
16581
+ const socketMemoryOptionsKey = () => JSON.stringify([
16582
+ this.sendSocketOptionsKey(sendSocketOptions),
16583
+ params.projectId ?? null,
16584
+ params.teamId ?? null,
16585
+ params.personal === true,
16586
+ params.incognito === true,
16587
+ params.autoApproveMemories === true,
16588
+ params.piiDetection !== false,
16589
+ params.learningMode?.enabled ?? null,
16590
+ params.learningMode?.ageGroup ?? null
16591
+ ]);
16592
+ if (!params.incognito && params.chatId) readSavedPrivacyHistory();
16593
+ const followupDiscoveryOwner = !params.incognito && params.chatId && !teamId && !params.projectId ? this.preparedChatOwner(this.requireSession(), teamId) : null;
16594
+ const followupDiscoveryRead = followupDiscoveryOwner ? withHttpAbortSignal(historyAbort.signal, async () => prefetchCliJevDiscovery(this, chatId, { personal: true, teamId })) : null;
16595
+ void followupDiscoveryRead?.catch(() => {
16374
16596
  });
16375
- }
16376
- return savedPrivacyHistoryRead;
16377
- };
16378
- const joinSavedPrivacyHistory = async () => {
16379
- const history = await readSavedPrivacyHistory();
16380
- if (!historySnapshotOwner || !this.preparedChatOwnerCurrent(historySnapshotOwner, teamId)) {
16381
- throw new Error("Chat workspace changed during history preparation.");
16382
- }
16383
- return history;
16384
- };
16385
- try {
16386
- let finalMessage = params.message;
16387
- if (hasRememberMessageReference(finalMessage)) {
16388
- const rememberedHistory = (params.messageHistory ?? []).map((message) => ({ id: message.message_id, content: message.content })).filter((message) => Boolean(message.id) && typeof message.content === "string");
16389
- finalMessage = rewriteRememberMessageReferences(finalMessage, rememberedHistory);
16390
- }
16391
- if (params.chatId && !params.incognito && hasRememberMessageReference(finalMessage)) {
16392
- const { messages } = await joinSavedPrivacyHistory();
16393
- finalMessage = rewriteRememberMessageReferences(finalMessage, messages);
16394
- }
16395
- const shouldWaitForAi = shouldWaitForTeamAi(finalMessage, teamId);
16396
- const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
16397
- const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
16398
- const sendSocketOptions = {
16399
- taskUpdateJobs: taskUpdateJobsEnabled,
16400
- projectFileJobs: !params.incognito,
16401
- remoteCommandJobs: !params.incognito,
16402
- interactiveHuman: params.interactiveHuman === true && this.interactiveViewerAllowed && process.stdin.isTTY === true && process.stdout.isTTY === true
16403
- };
16404
- const independentKeyOwner = this.preparedChatOwner(this.requireSession(), teamId);
16405
- const independentNewChatKey = !params.incognito && !params.chatId ? (async () => {
16406
- if (ownsPreparedId && this.preparedChatKey?.chatId === chatId) {
16407
- const prepared = await this.preparedChatKey.value;
16408
- if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(prepared.wrappingKey)).digest("hex");
16409
- return prepared;
16410
- }
16411
- const wrappingKey = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
16412
- if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(wrappingKey)).digest("hex");
16413
- const key = new Uint8Array(randomBytes4(32));
16414
- return { key, wrappingKey, encryptedKey: await encryptBytesWithAesGcm(key, wrappingKey) };
16415
- })() : null;
16416
- void independentNewChatKey?.catch(() => {
16417
- });
16418
- const independentRecoveryKeypair = independentNewChatKey?.then((prepared) => deriveChatCompletionRecoveryKeypair(
16419
- Buffer.from(prepared.key).toString("base64url"),
16420
- chatId,
16421
- 1
16422
- ));
16423
- void independentRecoveryKeypair?.catch(() => {
16424
- });
16425
- const privacyTurnId = params.incognito ? null : randomUUID9();
16426
- let conditionalPrivacyReceipt = null;
16427
- let compiledPrivacy;
16428
- let memorySocket = null;
16429
- let socketSnapshotAttempted = false;
16430
- let socketSnapshotResultRows = null;
16431
- let socketProjectCandidateRows = null;
16432
- let verifiedProjectCandidates = null;
16433
- let deferredProjectDirectory = false;
16434
- let usePersonalRoutingDirectory = false;
16435
- let projectCandidatesReadyFromSendStartMs = null;
16436
- let socketMemoryReceipt = null;
16437
- const socketMemoryOptionsKey = () => JSON.stringify([
16438
- this.sendSocketOptionsKey(sendSocketOptions),
16439
- params.projectId ?? null,
16440
- params.teamId ?? null,
16441
- params.personal === true,
16442
- params.incognito === true,
16443
- params.autoApproveMemories === true,
16444
- params.piiDetection !== false,
16445
- params.learningMode?.enabled ?? null,
16446
- params.learningMode?.ageGroup ?? null
16447
- ]);
16448
- if (!params.incognito && params.chatId) readSavedPrivacyHistory();
16449
- const followupDiscoveryOwner = !params.incognito && params.chatId && !teamId && !params.projectId ? this.preparedChatOwner(this.requireSession(), teamId) : null;
16450
- const followupDiscoveryRead = followupDiscoveryOwner ? withHttpAbortSignal(historyAbort.signal, async () => prefetchCliJevDiscovery(this, chatId, { personal: true, teamId })) : null;
16451
- void followupDiscoveryRead?.catch(() => {
16452
- });
16453
- const memoryStartedAt = performance.now();
16454
- let availableMemories = params.memorySnapshot ?? [];
16455
- let memoryCountsLoaded = false;
16456
- let memoryMetadataKeys = [];
16457
- if (!params.incognito) {
16458
- try {
16459
- if (!teamId) {
16460
- try {
16461
- socketSnapshotAttempted = true;
16462
- memorySocket = (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient(sendSocketOptions);
16463
- const ownerKey = JSON.stringify([
16464
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16465
- authCookieFingerprint(this.http.getCookieMap())
16466
- ]);
16467
- const conditionalSupported = await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v2");
16468
- const combinedSupported = conditionalSupported || await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v1");
16469
- const supported = combinedSupported || await memorySocket.ws.waitForServerCapability("personal_memory_snapshot_v1");
16470
- if (supported) {
16471
- const deferProjectDirectory = sdkMintedChatId && !teamId && !params.incognito && !params.projectId && shouldWaitForAi && memorySocket.ws.supportsServerCapability("personal_memory_snapshot_v1") && memorySocket.ws.supportsServerCapability("chat_turn_admit_v1") && memorySocket.ws.supportsServerCapability("chat_turn_context_v1");
16472
- deferredProjectDirectory = deferProjectDirectory;
16473
- usePersonalRoutingDirectory = deferProjectDirectory && memorySocket.ws.supportsServerCapability("personal_project_routing_directory_v1");
16474
- traceSendTiming("snapshot_request");
16475
- const privacyOwnerKey = this.preparedPrivacyOwnerKey(null);
16476
- const preferencesKey = JSON.stringify(await readPrivacyPreferences());
16477
- const preparedBundle = conditionalSupported ? this.preparedPrivacy.get(privacyOwnerKey, preferencesKey) : null;
16478
- const conditional = conditionalSupported ? await memorySocket.ws.requestPersonalSendContextSnapshotV2({
16479
- request_id: randomUUID9(),
16480
- purpose: "send",
16481
- turn_id: privacyTurnId,
16482
- ...preparedBundle ? { known_generation: preparedBundle.generation } : {}
16483
- }) : null;
16484
- const combined = !conditional && combinedSupported && !deferProjectDirectory ? await memorySocket.ws.requestPersonalSendContextSnapshot(randomUUID9()) : null;
16485
- const snapshot2 = conditional ? conditional.personal_memories ?? null : combined?.personal_memories ?? await memorySocket.ws.requestPersonalMemorySnapshot(randomUUID9());
16486
- traceSendTiming("snapshot_ready");
16487
- if (conditional || combined) {
16488
- socketProjectCandidateRows = (conditional ?? combined).project_candidates;
16489
- verifiedProjectCandidates = await decodePersonalProjectCandidates(this, (conditional ?? combined).project_candidates);
16490
- projectCandidatesReadyFromSendStartMs = Math.round(performance.now() - preWsStartedAt);
16491
- }
16492
- if (conditional?.unchanged) {
16493
- if (!preparedBundle || conditional.privacy_generation !== preparedBundle.generation) throw new Error("Unchanged privacy snapshot has no matching prepared bundle.");
16494
- availableMemories = preparedBundle.memories;
16495
- compiledPrivacy = preparedBundle.privacy;
16496
- socketSnapshotResultRows = [];
16497
- } else {
16498
- if (!snapshot2) throw new Error("Full Personal privacy snapshot is required.");
16499
- socketSnapshotResultRows = snapshot2.memories;
16500
- availableMemories = await this.listMemoriesUnshared({ personal: true }, snapshot2);
16597
+ const memoryStartedAt = performance.now();
16598
+ let availableMemories = params.memorySnapshot ?? [];
16599
+ let memoryCountsLoaded = false;
16600
+ let memoryMetadataKeys = [];
16601
+ if (!params.incognito) {
16602
+ try {
16603
+ if (!teamId) {
16604
+ try {
16605
+ socketSnapshotAttempted = true;
16606
+ memorySocket = (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient(sendSocketOptions);
16607
+ const ownerKey = JSON.stringify([
16608
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16609
+ authCookieFingerprint(this.http.getCookieMap())
16610
+ ]);
16611
+ const conditionalSupported = await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v2");
16612
+ const combinedSupported = conditionalSupported || await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v1");
16613
+ const supported = combinedSupported || await memorySocket.ws.waitForServerCapability("personal_memory_snapshot_v1");
16614
+ if (supported) {
16615
+ const deferProjectDirectory = sdkMintedChatId && !teamId && !params.incognito && !params.projectId && shouldWaitForAi && memorySocket.ws.supportsServerCapability("personal_memory_snapshot_v1") && memorySocket.ws.supportsServerCapability("chat_turn_admit_v1") && memorySocket.ws.supportsServerCapability("chat_turn_context_v1");
16616
+ deferredProjectDirectory = deferProjectDirectory;
16617
+ usePersonalRoutingDirectory = deferProjectDirectory && memorySocket.ws.supportsServerCapability("personal_project_routing_directory_v1");
16618
+ traceSendTiming("snapshot_request");
16619
+ const privacyOwnerKey = this.preparedPrivacyOwnerKey(null);
16620
+ const preferencesKey = JSON.stringify(await readPrivacyPreferences());
16621
+ const preparedBundle = conditionalSupported ? this.preparedPrivacy.get(privacyOwnerKey, preferencesKey) : null;
16622
+ const conditional = conditionalSupported ? await memorySocket.ws.requestPersonalSendContextSnapshotV2({
16623
+ request_id: randomUUID9(),
16624
+ purpose: "send",
16625
+ turn_id: privacyTurnId,
16626
+ ...preparedBundle ? { known_generation: preparedBundle.generation } : {}
16627
+ }) : null;
16628
+ const combined = !conditional && combinedSupported && !deferProjectDirectory ? await memorySocket.ws.requestPersonalSendContextSnapshot(randomUUID9()) : null;
16629
+ const snapshot2 = conditional ? conditional.personal_memories ?? null : combined?.personal_memories ?? await memorySocket.ws.requestPersonalMemorySnapshot(randomUUID9());
16630
+ traceSendTiming("snapshot_ready");
16631
+ if (conditional || combined) {
16632
+ socketProjectCandidateRows = (conditional ?? combined).project_candidates;
16633
+ verifiedProjectCandidates = await decodePersonalProjectCandidates(this, (conditional ?? combined).project_candidates);
16634
+ projectCandidatesReadyFromSendStartMs = Math.round(performance.now() - preWsStartedAt);
16635
+ }
16636
+ if (conditional?.unchanged) {
16637
+ if (!preparedBundle || conditional.privacy_generation !== preparedBundle.generation) throw new Error("Unchanged privacy snapshot has no matching prepared bundle.");
16638
+ availableMemories = preparedBundle.memories;
16639
+ compiledPrivacy = preparedBundle.privacy;
16640
+ socketSnapshotResultRows = [];
16641
+ } else {
16642
+ if (!snapshot2) throw new Error("Full Personal privacy snapshot is required.");
16643
+ socketSnapshotResultRows = snapshot2.memories;
16644
+ availableMemories = await this.listMemoriesUnshared({ personal: true }, snapshot2);
16645
+ if (conditional) {
16646
+ this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16647
+ compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16648
+ }
16649
+ }
16501
16650
  if (conditional) {
16502
- this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16503
- compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16651
+ if (!conditional.receipt_id || conditional.turn_id !== privacyTurnId) throw new Error("Conditional privacy snapshot has no valid send receipt.");
16652
+ conditionalPrivacyReceipt = {
16653
+ receiptId: conditional.receipt_id,
16654
+ generation: conditional.privacy_generation,
16655
+ preferencesKey,
16656
+ epoch: this.preparedPrivacyEpoch
16657
+ };
16504
16658
  }
16505
- }
16506
- if (conditional) {
16507
- if (!conditional.receipt_id || conditional.turn_id !== privacyTurnId) throw new Error("Conditional privacy snapshot has no valid send receipt.");
16508
- conditionalPrivacyReceipt = {
16509
- receiptId: conditional.receipt_id,
16510
- generation: conditional.privacy_generation,
16511
- preferencesKey,
16512
- epoch: this.preparedPrivacyEpoch
16659
+ traceSendTiming("memory_decrypt_ready");
16660
+ if (!memorySocket.ws.isOpen() || JSON.stringify([
16661
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16662
+ authCookieFingerprint(this.http.getCookieMap())
16663
+ ]) !== ownerKey) {
16664
+ throw new Error("Personal Memory snapshot owner or socket changed during preparation.");
16665
+ }
16666
+ socketMemoryReceipt = {
16667
+ ws: memorySocket.ws,
16668
+ snapshot: availableMemories,
16669
+ resultRows: socketSnapshotResultRows,
16670
+ projectRows: socketProjectCandidateRows,
16671
+ candidates: verifiedProjectCandidates,
16672
+ ownerKey,
16673
+ optionsKey: socketMemoryOptionsKey(),
16674
+ used: false
16513
16675
  };
16676
+ } else {
16677
+ memorySocket.ws.close();
16678
+ memorySocket = null;
16679
+ socketSnapshotAttempted = false;
16514
16680
  }
16515
- traceSendTiming("memory_decrypt_ready");
16516
- if (!memorySocket.ws.isOpen() || JSON.stringify([
16517
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16518
- authCookieFingerprint(this.http.getCookieMap())
16519
- ]) !== ownerKey) {
16520
- throw new Error("Personal Memory snapshot owner or socket changed during preparation.");
16521
- }
16522
- socketMemoryReceipt = {
16523
- ws: memorySocket.ws,
16524
- snapshot: availableMemories,
16525
- resultRows: socketSnapshotResultRows,
16526
- projectRows: socketProjectCandidateRows,
16527
- candidates: verifiedProjectCandidates,
16528
- ownerKey,
16529
- optionsKey: socketMemoryOptionsKey(),
16530
- used: false
16531
- };
16532
- } else {
16533
- memorySocket.ws.close();
16681
+ } catch (error) {
16682
+ memorySocket?.ws.close();
16534
16683
  memorySocket = null;
16535
- socketSnapshotAttempted = false;
16684
+ throw error;
16536
16685
  }
16537
- } catch (error) {
16538
- memorySocket?.ws.close();
16539
- memorySocket = null;
16540
- throw error;
16541
16686
  }
16542
- }
16543
- if (!socketMemoryReceipt) availableMemories = teamId && params.memorySnapshot ? params.memorySnapshot : await this.listMemoriesUnshared({ teamId }, void 0, !teamId);
16544
- memoryCountsLoaded = true;
16545
- memoryMetadataKeys = [
16546
- ...new Set(
16547
- availableMemories.filter((memory) => memory.app_id && memory.item_type).map((memory) => `${memory.app_id}-${memory.item_type}`)
16548
- )
16549
- ];
16550
- } catch (error) {
16551
- if (socketSnapshotAttempted || !teamId) throw error;
16552
- if (params.autoApproveMemories) {
16553
- const message = error instanceof Error ? error.message : String(error);
16554
- throw new Error(`Failed to load memories for auto-approval: ${message}`);
16687
+ if (!socketMemoryReceipt) availableMemories = teamId && params.memorySnapshot ? params.memorySnapshot : await this.listMemoriesUnshared({ teamId }, void 0, !teamId);
16688
+ memoryCountsLoaded = true;
16689
+ memoryMetadataKeys = [
16690
+ ...new Set(
16691
+ availableMemories.filter((memory) => memory.app_id && memory.item_type).map((memory) => `${memory.app_id}-${memory.item_type}`)
16692
+ )
16693
+ ];
16694
+ } catch (error) {
16695
+ if (socketSnapshotAttempted || !teamId) throw error;
16696
+ if (params.autoApproveMemories) {
16697
+ const message = error instanceof Error ? error.message : String(error);
16698
+ throw new Error(`Failed to load memories for auto-approval: ${message}`);
16699
+ }
16555
16700
  }
16556
16701
  }
16557
- }
16558
- try {
16559
- const snapshotAuthHandoff = this.personalMemoryAuthHandoff?.snapshot.deref() === availableMemories ? this.personalMemoryAuthHandoff.auth : null;
16560
- this.personalMemoryAuthHandoff = null;
16561
- this.pendingWhoAmIAuthHandoff = null;
16562
- const personalSendAuthHandoff = !teamId && !params.incognito ? snapshotAuthHandoff : null;
16563
- const memoryPrepMs = Math.round(performance.now() - memoryStartedAt);
16564
- const privacyStartedAt = performance.now();
16565
- traceSendTiming("privacy_begin");
16566
- const savedPrivacyHistory = !params.incognito && params.chatId ? await joinSavedPrivacyHistory() : null;
16567
- let piiMappings = [...savedPrivacyHistory?.messages.flatMap((message) => (message.piiMappings ?? []).map((mapping) => ({ ...mapping, type: mapping.type ?? "OTHER" }))) ?? [], ...params.piiMappings ?? []];
16568
16702
  try {
16569
- if (params.piiDetection !== false) {
16570
- const prepared = await prepareCliMessagePrivacy(finalMessage, availableMemories, piiMappings, { kind: "message" }, params.onPrivacyProgress, compiledPrivacy);
16571
- finalMessage = prepared.message;
16572
- piiMappings = prepared.mappings;
16573
- for (const embed of params.preparedEmbeds ?? []) {
16574
- if (!["code-code", "docs-doc", "sheets-sheet"].includes(embed.type)) continue;
16575
- const decoded = toonDecode(embed.content);
16576
- for (const field3 of ["code", "html", "table", "text"]) if (typeof decoded[field3] === "string") {
16577
- const value = await prepareCliMessagePrivacy(decoded[field3], availableMemories, piiMappings, { kind: "document" }, params.onPrivacyProgress, compiledPrivacy);
16578
- decoded[field3] = value.message;
16579
- piiMappings = value.mappings;
16580
- }
16581
- embed.content = toonEncodeContent(decoded);
16582
- const content = decoded.code ?? decoded.table ?? decoded.html ?? decoded.text;
16583
- if (typeof content === "string") {
16584
- embed.contentHash = createHash14("sha256").update(content).digest("hex");
16585
- embed.textLengthChars = content.length;
16703
+ const snapshotAuthHandoff = this.personalMemoryAuthHandoff?.snapshot.deref() === availableMemories ? this.personalMemoryAuthHandoff.auth : null;
16704
+ this.personalMemoryAuthHandoff = null;
16705
+ this.pendingWhoAmIAuthHandoff = null;
16706
+ const personalSendAuthHandoff = !teamId && !params.incognito ? snapshotAuthHandoff : null;
16707
+ const memoryPrepMs = Math.round(performance.now() - memoryStartedAt);
16708
+ const privacyStartedAt = performance.now();
16709
+ traceSendTiming("privacy_begin");
16710
+ const savedPrivacyHistory = !params.incognito && params.chatId ? await joinSavedPrivacyHistory() : null;
16711
+ let piiMappings = [...savedPrivacyHistory?.messages.flatMap((message) => (message.piiMappings ?? []).map((mapping) => ({ ...mapping, type: mapping.type ?? "OTHER" }))) ?? [], ...params.piiMappings ?? []];
16712
+ try {
16713
+ if (params.piiDetection !== false) {
16714
+ const prepared = await prepareCliMessagePrivacy(finalMessage, availableMemories, piiMappings, { kind: "message" }, params.onPrivacyProgress, compiledPrivacy);
16715
+ finalMessage = prepared.message;
16716
+ piiMappings = prepared.mappings;
16717
+ for (const embed of params.preparedEmbeds ?? []) {
16718
+ if (!["code-code", "docs-doc", "sheets-sheet"].includes(embed.type)) continue;
16719
+ const decoded = toonDecode(embed.content);
16720
+ for (const field3 of ["code", "html", "table", "text"]) if (typeof decoded[field3] === "string") {
16721
+ const value = await prepareCliMessagePrivacy(decoded[field3], availableMemories, piiMappings, { kind: "document" }, params.onPrivacyProgress, compiledPrivacy);
16722
+ decoded[field3] = value.message;
16723
+ piiMappings = value.mappings;
16724
+ }
16725
+ embed.content = toonEncodeContent(decoded);
16726
+ const content = decoded.code ?? decoded.table ?? decoded.html ?? decoded.text;
16727
+ if (typeof content === "string") {
16728
+ embed.contentHash = createHash14("sha256").update(content).digest("hex");
16729
+ embed.textLengthChars = content.length;
16730
+ }
16586
16731
  }
16587
16732
  }
16733
+ params.onPrivacyPrepared?.(finalMessage);
16734
+ } catch (error) {
16735
+ memorySocket?.ws.close();
16736
+ throw error;
16588
16737
  }
16589
- params.onPrivacyPrepared?.(finalMessage);
16590
- } catch (error) {
16591
- memorySocket?.ws.close();
16592
- throw error;
16593
- }
16594
- const privacyPrepMs = Math.round(performance.now() - privacyStartedAt);
16595
- traceSendTiming("privacy_ready");
16596
- const preWsMs = Math.round(performance.now() - preWsStartedAt);
16597
- const contextSession = this.requireSession();
16598
- const {
16599
- apiUrl: contextApiUrl,
16600
- hashedEmail: contextHashedEmail,
16601
- sessionId: contextSessionId,
16602
- createdAt: contextSessionCreatedAt,
16603
- masterKeyExportedB64: contextMasterKey,
16604
- activeTeamId: contextActiveTeamId
16605
- } = contextSession;
16606
- const contextScope = { teamId, personal: !teamId };
16607
- const suppliedContext = deferredProjectDirectory ? { ...params.jevContext, project_focus_candidates: void 0 } : params.jevContext;
16608
- const contextStartedAt = Date.now();
16609
- let publishRouting;
16610
- const routingContext = new Promise((resolve20) => {
16611
- publishRouting = resolve20;
16612
- });
16613
- let publishProjectRouting;
16614
- let rejectProjectRouting;
16615
- const projectRoutingContext = new Promise((resolve20, reject) => {
16616
- publishProjectRouting = resolve20;
16617
- rejectProjectRouting = reject;
16618
- });
16619
- void projectRoutingContext.catch(() => {
16620
- });
16621
- const publishProjectRoutingTimed = (context) => {
16622
- projectCandidatesReadyFromSendStartMs ??= Math.round(performance.now() - preWsStartedAt);
16623
- publishProjectRouting(deferredProjectDirectory ? { ...context, project_focus_candidates: context.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : context);
16624
- };
16625
- let recoveryMs = 0;
16626
- const contextOwnerIsCurrent = () => {
16627
- const current2 = this.session;
16628
- return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
16629
- };
16630
- const earlyContextAbort = new AbortController();
16631
- const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16632
- sdkMintedChatId,
16633
- onRouting: publishRouting,
16634
- verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16635
- usePersonalRoutingDirectory,
16636
- onProjectRouting: publishProjectRoutingTimed,
16637
- onProjectRoutingError: rejectProjectRouting
16638
- })).then(
16639
- (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
16640
- (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
16641
- ) : null;
16642
- const authSocketStartedAt = Date.now();
16643
- let opened;
16644
- try {
16645
- opened = memorySocket ?? (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient({ ...sendSocketOptions, personalSendAuthHandoff });
16646
- } catch (error) {
16647
- earlyContextAbort.abort();
16648
- if (earlyContext) await earlyContext;
16649
- throw error;
16650
- }
16651
- const { ws, ownerId: ownerId2 } = opened;
16652
- const consumeSocketMemoryReceipt = async () => {
16653
- if (!socketMemoryReceipt) return;
16654
- const receipt = socketMemoryReceipt;
16655
- if (receipt.used || receipt.ws !== ws || receipt.snapshot !== availableMemories || receipt.resultRows !== socketSnapshotResultRows || receipt.projectRows !== socketProjectCandidateRows || receipt.candidates !== verifiedProjectCandidates || !ws.isOpen() || receipt.optionsKey !== socketMemoryOptionsKey()) {
16656
- throw new Error("Personal Memory snapshot socket receipt is invalid.");
16657
- }
16658
- await this.withFreshStoredSession(async () => {
16659
- if (!ws.isOpen() || JSON.stringify([
16660
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16661
- authCookieFingerprint(this.http.getCookieMap())
16662
- ]) !== receipt.ownerKey) {
16663
- throw new Error("Personal Memory snapshot owner changed before send.");
16664
- }
16738
+ const privacyPrepMs = Math.round(performance.now() - privacyStartedAt);
16739
+ traceSendTiming("privacy_ready");
16740
+ const preWsMs = Math.round(performance.now() - preWsStartedAt);
16741
+ const contextSession = this.requireSession();
16742
+ const {
16743
+ apiUrl: contextApiUrl,
16744
+ hashedEmail: contextHashedEmail,
16745
+ sessionId: contextSessionId,
16746
+ createdAt: contextSessionCreatedAt,
16747
+ masterKeyExportedB64: contextMasterKey,
16748
+ activeTeamId: contextActiveTeamId
16749
+ } = contextSession;
16750
+ const contextScope = { teamId, personal: !teamId };
16751
+ const suppliedContext = deferredProjectDirectory ? { ...params.jevContext, project_focus_candidates: void 0 } : params.jevContext;
16752
+ const contextStartedAt = Date.now();
16753
+ let publishRouting;
16754
+ const routingContext = new Promise((resolve20) => {
16755
+ publishRouting = resolve20;
16665
16756
  });
16666
- if (conditionalPrivacyReceipt && (conditionalPrivacyReceipt.epoch !== this.preparedPrivacyEpoch || conditionalPrivacyReceipt.preferencesKey !== JSON.stringify(await readPrivacyPreferences()))) {
16667
- throw new Error("Privacy preferences changed before send; prepare the turn again.");
16757
+ let publishProjectRouting;
16758
+ let rejectProjectRouting;
16759
+ const projectRoutingContext = new Promise((resolve20, reject) => {
16760
+ publishProjectRouting = resolve20;
16761
+ rejectProjectRouting = reject;
16762
+ });
16763
+ void projectRoutingContext.catch(() => {
16764
+ });
16765
+ const publishProjectRoutingTimed = (context) => {
16766
+ projectCandidatesReadyFromSendStartMs ??= Math.round(performance.now() - preWsStartedAt);
16767
+ publishProjectRouting(deferredProjectDirectory ? { ...context, project_focus_candidates: context.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : context);
16768
+ };
16769
+ let recoveryMs = 0;
16770
+ const contextOwnerIsCurrent = () => {
16771
+ const current2 = this.session;
16772
+ return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
16773
+ };
16774
+ const earlyContextAbort = new AbortController();
16775
+ const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16776
+ sdkMintedChatId,
16777
+ onRouting: publishRouting,
16778
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16779
+ usePersonalRoutingDirectory,
16780
+ onProjectRouting: publishProjectRoutingTimed,
16781
+ onProjectRoutingError: rejectProjectRouting
16782
+ })).then(
16783
+ (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
16784
+ (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
16785
+ ) : null;
16786
+ const authSocketStartedAt = Date.now();
16787
+ let opened;
16788
+ try {
16789
+ opened = memorySocket ?? (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient({ ...sendSocketOptions, personalSendAuthHandoff });
16790
+ } catch (error) {
16791
+ earlyContextAbort.abort();
16792
+ if (earlyContext) await earlyContext;
16793
+ throw error;
16668
16794
  }
16669
- receipt.used = true;
16670
- };
16671
- let metadataTransportOwned = false;
16672
- let metadataHandoffReady = false;
16673
- let metadataHandoffSuggestion = null;
16674
- let initialHeaderWork = null;
16675
- let initialHeaderNotice = null;
16676
- let initialAdmissionDurable = false;
16677
- let initialHeaderStopped = false;
16678
- let initialTitleScheduled;
16679
- let initialIconScheduled;
16680
- let persistOptionalHeader;
16681
- let optionalMetadata = null;
16682
- const socketReadyForSendAt = performance.now();
16683
- let capabilitiesWaitMs = 0;
16684
- let keyWaitMs = 0;
16685
- let recoveryKeypairMs = 0;
16686
- let encryptPayloadMs = 0;
16687
- if (!params.incognito && !ownerId2) {
16688
- earlyContextAbort.abort();
16689
- ws.close();
16690
- if (earlyContext) await earlyContext;
16691
- throw new Error("Authenticated user identity is required for saved chat recovery.");
16692
- }
16693
- const authSocketMs = Date.now() - authSocketStartedAt;
16694
- const mayDeferRecovery = !params.incognito && sdkMintedChatId && !teamId && !params.projectId && shouldWaitForAi;
16695
- let recoveryDiscovery = null;
16696
- let recoveryDiscoveryError = null;
16697
- let contextAckRejection = null;
16698
- let removeContextAckWatcher = null;
16699
- try {
16700
- if (!params.incognito) {
16701
- try {
16702
- ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
16703
- } catch {
16795
+ const { ws, ownerId: ownerId2 } = opened;
16796
+ const consumeSocketMemoryReceipt = async () => {
16797
+ if (!socketMemoryReceipt) return;
16798
+ const receipt = socketMemoryReceipt;
16799
+ if (receipt.used || receipt.ws !== ws || receipt.snapshot !== availableMemories || receipt.resultRows !== socketSnapshotResultRows || receipt.projectRows !== socketProjectCandidateRows || receipt.candidates !== verifiedProjectCandidates || !ws.isOpen() || receipt.optionsKey !== socketMemoryOptionsKey()) {
16800
+ throw new Error("Personal Memory snapshot socket receipt is invalid.");
16704
16801
  }
16705
- const recoveryStartedAt = Date.now();
16706
- recoveryDiscovery = ws.waitForRecoveryOutputDiscovery().then(
16707
- () => {
16708
- recoveryMs = Date.now() - recoveryStartedAt;
16709
- },
16710
- (error) => {
16711
- recoveryDiscoveryError = error;
16712
- throw error;
16802
+ await this.withFreshStoredSession(async () => {
16803
+ if (!ws.isOpen() || JSON.stringify([
16804
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16805
+ authCookieFingerprint(this.http.getCookieMap())
16806
+ ]) !== receipt.ownerKey) {
16807
+ throw new Error("Personal Memory snapshot owner changed before send.");
16713
16808
  }
16714
- );
16715
- void recoveryDiscovery.catch(() => {
16716
16809
  });
16717
- if (mayDeferRecovery) {
16718
- const capabilitiesWaitStartedAt = performance.now();
16719
- await Promise.race([ws.waitForServerCapabilities(), recoveryDiscovery]);
16720
- capabilitiesWaitMs = Math.round(performance.now() - capabilitiesWaitStartedAt);
16721
- } else await recoveryDiscovery;
16722
- }
16723
- const messageId2 = randomUUID9();
16724
- const wallCreatedAt = Math.floor(Date.now() / 1e3);
16725
- const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
16726
- const isNewChat = !params.chatId;
16727
- let messageHistoryForRequest = params.messageHistory;
16728
- if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
16729
- const { messages } = savedPrivacyHistory;
16730
- messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
16810
+ if (conditionalPrivacyReceipt && (conditionalPrivacyReceipt.epoch !== this.preparedPrivacyEpoch || conditionalPrivacyReceipt.preferencesKey !== JSON.stringify(await readPrivacyPreferences()))) {
16811
+ throw new Error("Privacy preferences changed before send; prepare the turn again.");
16812
+ }
16813
+ receipt.used = true;
16814
+ };
16815
+ let metadataTransportOwned = false;
16816
+ let metadataHandoffReady = false;
16817
+ let metadataHandoffSuggestion = null;
16818
+ let initialHeaderWork = null;
16819
+ let initialHeaderNotice = null;
16820
+ let initialAdmissionDurable = false;
16821
+ let initialHeaderStopped = false;
16822
+ let initialTitleScheduled;
16823
+ let initialIconScheduled;
16824
+ let persistOptionalHeader;
16825
+ let optionalMetadata = null;
16826
+ const socketReadyForSendAt = performance.now();
16827
+ let capabilitiesWaitMs = 0;
16828
+ let keyWaitMs = 0;
16829
+ let recoveryKeypairMs = 0;
16830
+ let encryptPayloadMs = 0;
16831
+ if (!params.incognito && !ownerId2) {
16832
+ earlyContextAbort.abort();
16833
+ ws.close();
16834
+ if (earlyContext) await earlyContext;
16835
+ throw new Error("Authenticated user identity is required for saved chat recovery.");
16731
16836
  }
16732
- ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
16733
- const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
16734
- params.connectedAccountDirectory
16735
- );
16736
- const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
16737
- chatId,
16738
- messageId: messageId2,
16739
- refs: params.connectedAccountTokenRefInputs,
16740
- teamId
16741
- }) : [];
16742
- assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
16743
- let chatKeyBytes = null;
16744
- let activeFocusId = null;
16745
- let encryptedChatKey = null;
16746
- let chatSlugLookupKey = null;
16747
- let focusPhaseState = {};
16748
- let baselineMessagesV = 0;
16749
- let notificationTitle = null;
16750
- let terminalExpectedMessagesV = 1;
16751
- let savedTurnId = null;
16752
- if (!params.incognito) {
16753
- traceSendTiming("key_begin");
16754
- const masterKey = this.getMasterKeyBytes();
16755
- const preparedKey = isNewChat && ownsPreparedId && this.preparedChatKey?.chatId === chatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId) ? this.preparedChatKey : null;
16756
- if (preparedKey) this.preparedChatKey = null;
16757
- const keyWaitStartedAt = performance.now();
16758
- const prepared = independentNewChatKey ? await independentNewChatKey : preparedKey ? await preparedKey.value : null;
16759
- if (prepared && !this.preparedChatOwnerCurrent(independentKeyOwner, teamId)) throw new Error("Chat key preparation owner changed before send.");
16760
- const wrappingKey = prepared?.wrappingKey ?? await this.getChatWrappingKey(teamId, masterKey);
16761
- keyWaitMs = Math.round(performance.now() - keyWaitStartedAt);
16762
- chatSlugLookupKey = wrappingKey;
16763
- if (isNewChat && prepared) {
16764
- chatKeyBytes = prepared.key;
16765
- encryptedChatKey = prepared.encryptedKey;
16766
- } else if (isNewChat) {
16767
- chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
16768
- (await import("crypto")).webcrypto.getRandomValues(
16769
- new Uint8Array(32)
16770
- )
16771
- );
16772
- encryptedChatKey = await encryptBytesWithAesGcm(
16773
- chatKeyBytes,
16774
- wrappingKey
16775
- );
16776
- } else {
16777
- const cache = (savedPrivacyHistory && this.scopedChatHistory.get(savedPrivacyHistory)) ?? loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
16778
- const chat = cache.chats.find(
16779
- (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
16837
+ const authSocketMs = Date.now() - authSocketStartedAt;
16838
+ const mayDeferRecovery = !params.incognito && sdkMintedChatId && !teamId && !params.projectId && shouldWaitForAi;
16839
+ let recoveryDiscovery = null;
16840
+ let recoveryDiscoveryError = null;
16841
+ let contextAckRejection = null;
16842
+ let removeContextAckWatcher = null;
16843
+ try {
16844
+ if (!params.incognito) {
16845
+ try {
16846
+ ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
16847
+ } catch {
16848
+ }
16849
+ const recoveryStartedAt = Date.now();
16850
+ recoveryDiscovery = ws.waitForRecoveryOutputDiscovery().then(
16851
+ () => {
16852
+ recoveryMs = Date.now() - recoveryStartedAt;
16853
+ },
16854
+ (error) => {
16855
+ recoveryDiscoveryError = error;
16856
+ throw error;
16857
+ }
16780
16858
  );
16781
- if (chat) {
16782
- baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
16783
- const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
16784
- if (encKey) {
16785
- chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
16786
- encryptedChatKey = encKey;
16787
- if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
16788
- if (typeof chat.details.encrypted_title === "string") {
16789
- try {
16790
- notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
16791
- } catch {
16859
+ void recoveryDiscovery.catch(() => {
16860
+ });
16861
+ if (mayDeferRecovery) {
16862
+ const capabilitiesWaitStartedAt = performance.now();
16863
+ await Promise.race([ws.waitForServerCapabilities(), recoveryDiscovery]);
16864
+ capabilitiesWaitMs = Math.round(performance.now() - capabilitiesWaitStartedAt);
16865
+ } else await recoveryDiscovery;
16866
+ }
16867
+ const messageId2 = randomUUID9();
16868
+ const wallCreatedAt = Math.floor(Date.now() / 1e3);
16869
+ const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
16870
+ const isNewChat = !params.chatId;
16871
+ let messageHistoryForRequest = params.messageHistory;
16872
+ if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
16873
+ const { messages } = savedPrivacyHistory;
16874
+ messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
16875
+ }
16876
+ ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
16877
+ const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
16878
+ params.connectedAccountDirectory
16879
+ );
16880
+ const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
16881
+ chatId,
16882
+ messageId: messageId2,
16883
+ refs: params.connectedAccountTokenRefInputs,
16884
+ teamId
16885
+ }) : [];
16886
+ assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
16887
+ let chatKeyBytes = null;
16888
+ let activeFocusId = null;
16889
+ let encryptedChatKey = null;
16890
+ let chatSlugLookupKey = null;
16891
+ let focusPhaseState = {};
16892
+ let baselineMessagesV = 0;
16893
+ let notificationTitle = null;
16894
+ let terminalExpectedMessagesV = 1;
16895
+ let savedTurnId = null;
16896
+ if (!params.incognito) {
16897
+ traceSendTiming("key_begin");
16898
+ const masterKey = this.getMasterKeyBytes();
16899
+ const preparedKey = isNewChat && ownsPreparedId && this.preparedChatKey?.chatId === chatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId) ? this.preparedChatKey : null;
16900
+ if (preparedKey) this.preparedChatKey = null;
16901
+ const keyWaitStartedAt = performance.now();
16902
+ const prepared = independentNewChatKey ? await independentNewChatKey : preparedKey ? await preparedKey.value : null;
16903
+ if (prepared && !this.preparedChatOwnerCurrent(independentKeyOwner, teamId)) throw new Error("Chat key preparation owner changed before send.");
16904
+ const wrappingKey = prepared?.wrappingKey ?? await this.getChatWrappingKey(teamId, masterKey);
16905
+ keyWaitMs = Math.round(performance.now() - keyWaitStartedAt);
16906
+ chatSlugLookupKey = wrappingKey;
16907
+ if (isNewChat && prepared) {
16908
+ chatKeyBytes = prepared.key;
16909
+ encryptedChatKey = prepared.encryptedKey;
16910
+ } else if (isNewChat) {
16911
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
16912
+ (await import("crypto")).webcrypto.getRandomValues(
16913
+ new Uint8Array(32)
16914
+ )
16915
+ );
16916
+ encryptedChatKey = await encryptBytesWithAesGcm(
16917
+ chatKeyBytes,
16918
+ wrappingKey
16919
+ );
16920
+ } else {
16921
+ const cache = (savedPrivacyHistory && this.scopedChatHistory.get(savedPrivacyHistory)) ?? loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
16922
+ const chat = cache.chats.find(
16923
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
16924
+ );
16925
+ if (chat) {
16926
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
16927
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
16928
+ if (encKey) {
16929
+ chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
16930
+ encryptedChatKey = encKey;
16931
+ if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
16932
+ if (typeof chat.details.encrypted_title === "string") {
16933
+ try {
16934
+ notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
16935
+ } catch {
16936
+ }
16792
16937
  }
16793
- }
16794
- const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
16795
- if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
16796
- const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
16797
- if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
16798
- focusPhaseState = JSON.parse(phaseText);
16799
- }
16800
- const encryptedFocusId = chat.details.encrypted_active_focus_id;
16801
- if (typeof encryptedFocusId === "string" && encryptedFocusId) {
16802
- activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
16803
- if (!activeFocusId) {
16804
- throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
16938
+ const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
16939
+ if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
16940
+ const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
16941
+ if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
16942
+ focusPhaseState = JSON.parse(phaseText);
16943
+ }
16944
+ const encryptedFocusId = chat.details.encrypted_active_focus_id;
16945
+ if (typeof encryptedFocusId === "string" && encryptedFocusId) {
16946
+ activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
16947
+ if (!activeFocusId) {
16948
+ throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
16949
+ }
16805
16950
  }
16806
16951
  }
16807
16952
  }
16953
+ if (!chatKeyBytes || !encryptedChatKey) {
16954
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
16955
+ }
16808
16956
  }
16809
- if (!chatKeyBytes || !encryptedChatKey) {
16810
- throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
16811
- }
16812
- }
16813
- traceSendTiming("key_ready");
16814
- }
16815
- let focusPersistence = Promise.resolve();
16816
- let focusPersistenceError = null;
16817
- const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
16818
- ws,
16819
- chatId,
16820
- chatKey: chatKeyBytes,
16821
- existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
16822
- previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
16823
- onApplied: params.onChatContextApplied
16824
- }) : null;
16825
- let projectAuthoringAvailable = null;
16826
- if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
16827
- if (!value || typeof value !== "object") return;
16828
- const frame = value;
16829
- if (frame.chat_id === chatId && frame.user_message_id === messageId2 && typeof frame.project_id === "string")
16830
- projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId2 };
16831
- });
16832
- const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
16833
- client: this,
16834
- ws,
16835
- chatId,
16836
- teamId,
16837
- onPending: params.onProjectFocusPending,
16838
- onError: (error) => {
16839
- focusPersistenceError = error;
16957
+ traceSendTiming("key_ready");
16840
16958
  }
16841
- }) : null;
16842
- ws.onMessageType("focus_mode_activated", (event) => {
16843
- if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
16844
- const focusId = event.focus_id;
16845
- if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
16846
- const key = chatKeyBytes;
16847
- focusPersistence = focusPersistence.then(async () => {
16848
- const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
16849
- await ws.sendAsync("update_encrypted_active_focus_id", {
16850
- chat_id: chatId,
16851
- encrypted_active_focus_id: encryptedFocusId
16852
- });
16853
- }).catch((error) => {
16854
- focusPersistenceError = error;
16959
+ let focusPersistence = Promise.resolve();
16960
+ let focusPersistenceError = null;
16961
+ const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
16962
+ ws,
16963
+ chatId,
16964
+ chatKey: chatKeyBytes,
16965
+ existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
16966
+ previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
16967
+ onApplied: params.onChatContextApplied
16968
+ }) : null;
16969
+ let projectAuthoringAvailable = null;
16970
+ if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
16971
+ if (!value || typeof value !== "object") return;
16972
+ const frame = value;
16973
+ if (frame.chat_id === chatId && frame.user_message_id === messageId2 && typeof frame.project_id === "string")
16974
+ projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId2 };
16855
16975
  });
16856
- });
16857
- ws.onMessageType("focus_phases_updated", (event) => {
16858
- if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
16859
- const key = chatKeyBytes;
16860
- const states2 = event.states;
16861
- focusPersistence = focusPersistence.then(async () => {
16862
- for (const [focusId, phase] of Object.entries(states2)) {
16863
- if (phase.schema_version !== 1 || phase.chat_id !== chatId || phase.focus_id !== focusId || !Number.isSafeInteger(phase.version) || phase.version < 0 || !Array.isArray(phase.transitions) || phase.transitions.length > 32) {
16864
- throw new Error("Invalid focus phase state received");
16865
- }
16866
- if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
16976
+ const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
16977
+ client: this,
16978
+ ws,
16979
+ chatId,
16980
+ teamId,
16981
+ onPending: params.onProjectFocusPending,
16982
+ onError: (error) => {
16983
+ focusPersistenceError = error;
16867
16984
  }
16868
- focusPhaseState = states2;
16869
- const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
16870
- const persisted = ws.waitForMessage(
16871
- "encrypted_metadata_stored",
16872
- (payload) => payload.chat_id === chatId,
16873
- 2e4
16874
- );
16875
- await ws.sendAsync("encrypted_chat_metadata", {
16876
- chat_id: chatId,
16877
- ...teamId ? { team_id: teamId } : {},
16878
- encrypted_focus_phase_state: encryptedState
16879
- });
16880
- await persisted;
16881
- const eventIds = /* @__PURE__ */ new Set();
16882
- for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
16883
- if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
16884
- eventIds.add(transition.event_id);
16885
- const content = JSON.stringify(transition);
16886
- await ws.sendAsync("chat_system_message_added", {
16985
+ }) : null;
16986
+ ws.onMessageType("focus_mode_activated", (event) => {
16987
+ if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
16988
+ const focusId = event.focus_id;
16989
+ if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
16990
+ const key = chatKeyBytes;
16991
+ focusPersistence = focusPersistence.then(async () => {
16992
+ const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
16993
+ await ws.sendAsync("update_encrypted_active_focus_id", {
16887
16994
  chat_id: chatId,
16888
- message: {
16889
- message_id: transition.event_id,
16890
- role: "system",
16891
- status: "synced",
16892
- encrypted_content: await encryptWithAesGcmCombined(content, key),
16893
- created_at: transition.created_at
16894
- }
16995
+ encrypted_active_focus_id: encryptedFocusId
16895
16996
  });
16896
- }
16897
- clearSyncCache(teamId);
16898
- }).catch((error) => {
16899
- focusPersistenceError = error;
16997
+ }).catch((error) => {
16998
+ focusPersistenceError = error;
16999
+ });
16900
17000
  });
16901
- });
16902
- const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
16903
- if (!params.incognito && chatKeyBytes) {
16904
- registerCliProjectFileExecutor({
16905
- client: this,
16906
- ws,
16907
- chatId,
16908
- chatKey: chatKeyBytes,
16909
- piiMappings,
16910
- memories: availableMemories,
16911
- requestApproval: params.onProjectWriteApproval,
16912
- requestReadApproval: params.onProjectReadApproval,
16913
- onHostedVersionCommitted: params.onHostedVersionCommitted
17001
+ ws.onMessageType("focus_phases_updated", (event) => {
17002
+ if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
17003
+ const key = chatKeyBytes;
17004
+ const states2 = event.states;
17005
+ focusPersistence = focusPersistence.then(async () => {
17006
+ for (const [focusId, phase] of Object.entries(states2)) {
17007
+ if (phase.schema_version !== 1 || phase.chat_id !== chatId || phase.focus_id !== focusId || !Number.isSafeInteger(phase.version) || phase.version < 0 || !Array.isArray(phase.transitions) || phase.transitions.length > 32) {
17008
+ throw new Error("Invalid focus phase state received");
17009
+ }
17010
+ if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
17011
+ }
17012
+ focusPhaseState = states2;
17013
+ const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
17014
+ const persisted = ws.waitForMessage(
17015
+ "encrypted_metadata_stored",
17016
+ (payload) => payload.chat_id === chatId,
17017
+ 2e4
17018
+ );
17019
+ await ws.sendAsync("encrypted_chat_metadata", {
17020
+ chat_id: chatId,
17021
+ ...teamId ? { team_id: teamId } : {},
17022
+ encrypted_focus_phase_state: encryptedState
17023
+ });
17024
+ await persisted;
17025
+ const eventIds = /* @__PURE__ */ new Set();
17026
+ for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
17027
+ if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
17028
+ eventIds.add(transition.event_id);
17029
+ const content = JSON.stringify(transition);
17030
+ await ws.sendAsync("chat_system_message_added", {
17031
+ chat_id: chatId,
17032
+ message: {
17033
+ message_id: transition.event_id,
17034
+ role: "system",
17035
+ status: "synced",
17036
+ encrypted_content: await encryptWithAesGcmCombined(content, key),
17037
+ created_at: transition.created_at
17038
+ }
17039
+ });
17040
+ }
17041
+ clearSyncCache(teamId);
17042
+ }).catch((error) => {
17043
+ focusPersistenceError = error;
17044
+ });
16914
17045
  });
16915
- clientCapabilities.push("project_file_jobs");
16916
- registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
16917
- clientCapabilities.push("remote_command_jobs");
16918
- }
16919
- const messagePayload = {
16920
- chat_id: chatId,
16921
- ...teamId ? { team_id: teamId } : {},
16922
- client_capabilities: clientCapabilities,
16923
- // Only decrypted current metadata restores focus; history is never authority.
16924
- active_focus_id: activeFocusId,
16925
- focus_phase_state: focusPhaseState,
16926
- is_incognito: Boolean(params.incognito),
16927
- message: {
16928
- message_id: messageId2,
16929
- chat_id: chatId,
16930
- role: "user",
16931
- sender_name: "User",
16932
- status: "sent",
16933
- content: finalMessage,
16934
- created_at: createdAt,
16935
- chat_has_title: Boolean(params.chatId)
16936
- }
16937
- };
16938
- const useCombinedAdmission = ws.supportsServerCapability?.("chat_turn_admit_v1") === true;
16939
- const useDeferredContext = useCombinedAdmission && !params.chatId && !params.projectId && !teamId && ws.supportsServerCapability?.("chat_turn_context_v1") === true;
16940
- const useSelectionDetailsV2 = useDeferredContext && sdkMintedChatId && ws.supportsServerCapability?.("chat_turn_context_selection_details_v2") === true;
16941
- const contextRequestId = !params.incognito && shouldWaitForAi && useDeferredContext ? randomUUID9() : null;
16942
- let pendingContext = null;
16943
- if (!params.incognito && shouldWaitForAi) {
16944
- if (contextRequestId) {
16945
- messagePayload.client_context_pending = true;
16946
- messagePayload.client_context_request_id = contextRequestId;
17046
+ const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
17047
+ if (!params.incognito && chatKeyBytes) {
17048
+ registerCliProjectFileExecutor({
17049
+ client: this,
17050
+ ws,
17051
+ chatId,
17052
+ chatKey: chatKeyBytes,
17053
+ piiMappings,
17054
+ memories: availableMemories,
17055
+ requestApproval: params.onProjectWriteApproval,
17056
+ requestReadApproval: params.onProjectReadApproval,
17057
+ onHostedVersionCommitted: params.onHostedVersionCommitted
17058
+ });
17059
+ clientCapabilities.push("project_file_jobs");
17060
+ registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
17061
+ clientCapabilities.push("remote_command_jobs");
16947
17062
  }
16948
- pendingContext = (async () => {
16949
- const prefetchedDiscovery2 = followupDiscoveryRead ? await followupDiscoveryRead : void 0;
16950
- if (followupDiscoveryOwner && !this.preparedChatOwnerCurrent(followupDiscoveryOwner, teamId)) {
16951
- throw new Error("Chat workspace changed during context discovery.");
16952
- }
16953
- const prepared = earlyContext ? await earlyContext : { ok: true, value: await withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16954
- prefetchedDiscovery: prefetchedDiscovery2,
16955
- sdkMintedChatId,
16956
- onRouting: publishRouting,
16957
- verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16958
- usePersonalRoutingDirectory,
16959
- onProjectRouting: publishProjectRoutingTimed,
16960
- onProjectRoutingError: rejectProjectRouting
16961
- })), elapsedMs: Date.now() - contextStartedAt };
16962
- if (!prepared.ok) throw prepared.error;
16963
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
16964
- if (process.env.OPENMATES_DEBUG === "1") {
16965
- console.error(`[chat preparation] pre_ws_ms=${preWsMs} memory_ms=${memoryPrepMs} privacy_ms=${privacyPrepMs} auth_ws_ms=${authSocketMs} recovery_ms=${recoveryMs} context_ms=${prepared.elapsedMs} project_candidates_ready_from_send_start_ms=${projectCandidatesReadyFromSendStartMs ?? "pending"}`);
17063
+ const messagePayload = {
17064
+ chat_id: chatId,
17065
+ ...teamId ? { team_id: teamId } : {},
17066
+ client_capabilities: clientCapabilities,
17067
+ // Only decrypted current metadata restores focus; history is never authority.
17068
+ active_focus_id: activeFocusId,
17069
+ focus_phase_state: focusPhaseState,
17070
+ is_incognito: Boolean(params.incognito),
17071
+ message: {
17072
+ message_id: messageId2,
17073
+ chat_id: chatId,
17074
+ role: "user",
17075
+ sender_name: "User",
17076
+ status: "sent",
17077
+ content: finalMessage,
17078
+ created_at: createdAt,
17079
+ chat_has_title: Boolean(params.chatId)
16966
17080
  }
16967
- return deferredProjectDirectory ? { ...prepared.value, project_focus_candidates: prepared.value.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : prepared.value;
16968
- })();
16969
- void pendingContext.catch(() => {
16970
- });
16971
- }
16972
- if (params.testMockMarker !== void 0) {
16973
- const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
16974
- messagePayload.test_mock_marker = capacityReplayMarker(
16975
- params.testMockMarker,
16976
- this.apiUrl,
16977
- process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
16978
- );
16979
- }
16980
- if (isNewChat && encryptedChatKey) {
16981
- messagePayload.encrypted_chat_key = encryptedChatKey;
16982
- }
16983
- if (memoryMetadataKeys.length > 0) {
16984
- messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
16985
- }
16986
- if (connectedAccountDirectory) {
16987
- messagePayload.connected_account_directory = connectedAccountDirectory;
16988
- }
16989
- if (connectedAccountTokenRefs.length > 0) {
16990
- messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
16991
- }
16992
- if (params.benchmarkMetadata) {
16993
- messagePayload.benchmark_metadata = params.benchmarkMetadata;
16994
- }
16995
- if (params.learningMode) {
16996
- messagePayload.learning_mode = {
16997
- enabled: params.learningMode.enabled,
16998
- age_group: params.learningMode.ageGroup ?? null
16999
17081
  };
17000
- }
17001
- if (params.incognito) {
17002
- const providedHistory = (params.messageHistory ?? []).map(
17003
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
17004
- );
17005
- messagePayload.message_history = [...providedHistory, {
17006
- message_id: messageId2,
17007
- chat_id: chatId,
17008
- role: "user",
17009
- sender_name: "User",
17010
- content: finalMessage,
17011
- created_at: createdAt
17012
- }];
17013
- } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
17014
- messagePayload.message_history = messageHistoryForRequest.map(
17015
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
17016
- );
17017
- }
17018
- if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17019
- messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
17020
- embed_id: embed.embedId,
17021
- type: embed.type,
17022
- content: embed.content,
17023
- status: embed.status,
17024
- text_preview: embed.textPreview
17025
- }));
17026
- }
17027
- const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
17028
- if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17029
- if (!ownerId2) {
17030
- throw new Error("Authenticated user identity is required to encrypt embeds.");
17082
+ const useCombinedAdmission = ws.supportsServerCapability?.("chat_turn_admit_v1") === true;
17083
+ const useDeferredContext = useCombinedAdmission && !params.chatId && !params.projectId && !teamId && ws.supportsServerCapability?.("chat_turn_context_v1") === true;
17084
+ const useSelectionDetailsV2 = useDeferredContext && sdkMintedChatId && ws.supportsServerCapability?.("chat_turn_context_selection_details_v2") === true;
17085
+ const contextRequestId = !params.incognito && shouldWaitForAi && useDeferredContext ? randomUUID9() : null;
17086
+ let pendingContext = null;
17087
+ if (!params.incognito && shouldWaitForAi) {
17088
+ if (contextRequestId) {
17089
+ messagePayload.client_context_pending = true;
17090
+ messagePayload.client_context_request_id = contextRequestId;
17091
+ }
17092
+ pendingContext = (async () => {
17093
+ const prefetchedDiscovery2 = followupDiscoveryRead ? await followupDiscoveryRead : void 0;
17094
+ if (followupDiscoveryOwner && !this.preparedChatOwnerCurrent(followupDiscoveryOwner, teamId)) {
17095
+ throw new Error("Chat workspace changed during context discovery.");
17096
+ }
17097
+ const prepared = earlyContext ? await earlyContext : { ok: true, value: await withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
17098
+ prefetchedDiscovery: prefetchedDiscovery2,
17099
+ sdkMintedChatId,
17100
+ onRouting: publishRouting,
17101
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
17102
+ usePersonalRoutingDirectory,
17103
+ onProjectRouting: publishProjectRoutingTimed,
17104
+ onProjectRoutingError: rejectProjectRouting
17105
+ })), elapsedMs: Date.now() - contextStartedAt };
17106
+ if (!prepared.ok) throw prepared.error;
17107
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17108
+ if (process.env.OPENMATES_DEBUG === "1") {
17109
+ console.error(`[chat preparation] pre_ws_ms=${preWsMs} memory_ms=${memoryPrepMs} privacy_ms=${privacyPrepMs} auth_ws_ms=${authSocketMs} recovery_ms=${recoveryMs} context_ms=${prepared.elapsedMs} project_candidates_ready_from_send_start_ms=${projectCandidatesReadyFromSendStartMs ?? "pending"}`);
17110
+ }
17111
+ return deferredProjectDirectory ? { ...prepared.value, project_focus_candidates: prepared.value.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : prepared.value;
17112
+ })();
17113
+ void pendingContext.catch(() => {
17114
+ });
17031
17115
  }
17032
- if (!chatKeyBytes) {
17033
- throw new Error("Saved chat key is required to encrypt every prepared embed.");
17116
+ if (params.testMockMarker !== void 0) {
17117
+ const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
17118
+ messagePayload.test_mock_marker = capacityReplayMarker(
17119
+ params.testMockMarker,
17120
+ this.apiUrl,
17121
+ process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
17122
+ );
17034
17123
  }
17035
- if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
17036
- throw new Error("Prepared message embed IDs must be unique.");
17124
+ if (isNewChat && encryptedChatKey) {
17125
+ messagePayload.encrypted_chat_key = encryptedChatKey;
17037
17126
  }
17038
- const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
17039
- if (preparedIds.some((id) => !id || id.length > 128)) {
17040
- throw new Error("Prepared message embed ID is invalid.");
17127
+ if (memoryMetadataKeys.length > 0) {
17128
+ messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
17041
17129
  }
17042
- const canonicalReady = /* @__PURE__ */ new Set();
17043
- for (let offset = 0; offset < preparedIds.length; offset += 20) {
17044
- const selectedIds = preparedIds.slice(offset, offset + 20);
17045
- if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
17046
- throw new Error("Message embed availability request exceeds its byte limit.");
17047
- }
17048
- const path2 = this.appendTeamQuery(
17049
- `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
17050
- { teamId }
17051
- );
17052
- const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
17053
- if (!availability.ok) {
17054
- throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
17055
- }
17056
- const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
17057
- for (const id of page.ready) canonicalReady.add(id);
17130
+ if (connectedAccountDirectory) {
17131
+ messagePayload.connected_account_directory = connectedAccountDirectory;
17058
17132
  }
17059
- const masterKey = this.getMasterKeyBytes();
17060
- for (const embed of params.preparedEmbeds) {
17061
- if (canonicalReady.has(embed.embedId)) continue;
17062
- const encrypted = await encryptEmbed(
17063
- embed,
17064
- masterKey,
17065
- chatKeyBytes,
17066
- chatId,
17067
- messageId2,
17068
- ownerId2
17133
+ if (connectedAccountTokenRefs.length > 0) {
17134
+ messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
17135
+ }
17136
+ if (params.benchmarkMetadata) {
17137
+ messagePayload.benchmark_metadata = params.benchmarkMetadata;
17138
+ }
17139
+ if (params.learningMode) {
17140
+ messagePayload.learning_mode = {
17141
+ enabled: params.learningMode.enabled,
17142
+ age_group: params.learningMode.ageGroup ?? null
17143
+ };
17144
+ }
17145
+ if (params.incognito) {
17146
+ const providedHistory = (params.messageHistory ?? []).map(
17147
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
17148
+ );
17149
+ messagePayload.message_history = [...providedHistory, {
17150
+ message_id: messageId2,
17151
+ chat_id: chatId,
17152
+ role: "user",
17153
+ sender_name: "User",
17154
+ content: finalMessage,
17155
+ created_at: createdAt
17156
+ }];
17157
+ } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
17158
+ messagePayload.message_history = messageHistoryForRequest.map(
17159
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
17069
17160
  );
17070
- if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
17071
- throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
17072
- }
17073
- encryptedEmbeds.push(encrypted);
17074
17161
  }
17075
- }
17076
- if (!params.incognito && encryptedEmbeds.length > 0) {
17077
- if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
17078
- assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId: messageId2, ownerId: ownerId2 });
17079
- }
17080
- if (encryptedEmbeds.length > 0) {
17081
- messagePayload.encrypted_embeds = encryptedEmbeds;
17082
- }
17083
- let dispatchHandlersReady = () => {
17084
- };
17085
- const handlersReady = new Promise((resolve20) => {
17086
- dispatchHandlersReady = resolve20;
17087
- });
17088
- let admissionPayload = null;
17089
- let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId2, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
17090
- if (!params.incognito && chatKeyBytes && encryptedChatKey) {
17091
- traceSendTiming("preflight_begin");
17092
- const protocolVersion = 1;
17093
- const chatKeyVersion = 1;
17094
- const turnId = privacyTurnId;
17095
- savedTurnId = turnId;
17096
- terminalExpectedMessagesV = baselineMessagesV + 1;
17097
- const recoveryKeypairStartedAt = performance.now();
17098
- const recoveryKeypair = independentRecoveryKeypair ? await independentRecoveryKeypair : await deriveChatCompletionRecoveryKeypair(
17099
- Buffer.from(chatKeyBytes).toString("base64url"),
17100
- chatId,
17101
- chatKeyVersion
17102
- );
17103
- recoveryKeypairMs = Math.round(performance.now() - recoveryKeypairStartedAt);
17104
- const encryptPayloadStartedAt = performance.now();
17105
- const [encryptedContent, encryptedSenderName, encryptedPiiMappings] = await Promise.all([
17106
- encryptWithAesGcmCombined(finalMessage, chatKeyBytes),
17107
- encryptWithAesGcmCombined(teamSenderName, chatKeyBytes),
17108
- piiMappings.length ? encryptWithAesGcmCombined(JSON.stringify(piiMappings), chatKeyBytes) : null
17109
- ]);
17110
- const encryptedUserMessage = {
17111
- client_message_id: messageId2,
17112
- chat_id: chatId,
17113
- encrypted_content: encryptedContent,
17114
- role: "user",
17115
- created_at: createdAt,
17116
- updated_at: createdAt
17117
- };
17118
- encryptedUserMessage.encrypted_sender_name = encryptedSenderName;
17119
- if (encryptedPiiMappings) encryptedUserMessage.encrypted_pii_mappings = encryptedPiiMappings;
17120
- Object.assign(messagePayload, {
17121
- turn_id: turnId,
17122
- recovery_public_key: recoveryKeypair.publicKey,
17123
- chat_key_version: chatKeyVersion
17124
- });
17125
- let preflightInferenceRequest = messagePayload;
17126
- if (teamId) {
17127
- const teamInferenceHistory = [
17128
- ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
17129
- role: historyMessage.role,
17130
- content: historyMessage.content,
17131
- category: historyMessage.category ?? null,
17132
- sender_name: historyMessage.sender_name ?? historyMessage.role,
17133
- created_at: historyMessage.created_at
17134
- })),
17135
- {
17136
- role: "user",
17137
- content: finalMessage,
17138
- category: null,
17139
- sender_name: teamSenderName,
17140
- created_at: createdAt
17162
+ if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17163
+ messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
17164
+ embed_id: embed.embedId,
17165
+ type: embed.type,
17166
+ content: embed.content,
17167
+ status: embed.status,
17168
+ text_preview: embed.textPreview
17169
+ }));
17170
+ }
17171
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
17172
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17173
+ if (!ownerId2) {
17174
+ throw new Error("Authenticated user identity is required to encrypt embeds.");
17175
+ }
17176
+ if (!chatKeyBytes) {
17177
+ throw new Error("Saved chat key is required to encrypt every prepared embed.");
17178
+ }
17179
+ if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
17180
+ throw new Error("Prepared message embed IDs must be unique.");
17181
+ }
17182
+ const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
17183
+ if (preparedIds.some((id) => !id || id.length > 128)) {
17184
+ throw new Error("Prepared message embed ID is invalid.");
17185
+ }
17186
+ const canonicalReady = /* @__PURE__ */ new Set();
17187
+ for (let offset = 0; offset < preparedIds.length; offset += 20) {
17188
+ const selectedIds = preparedIds.slice(offset, offset + 20);
17189
+ if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
17190
+ throw new Error("Message embed availability request exceeds its byte limit.");
17141
17191
  }
17142
- ];
17143
- const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId2 };
17144
- delete messagePayload.message_history;
17145
- messagePayload.message = teamMessageEnvelope;
17146
- if (shouldWaitForAi) {
17147
- messagePayload.team_ai_invocation = { history: teamInferenceHistory };
17148
- preflightInferenceRequest = {
17149
- ...messagePayload,
17150
- message_history: teamInferenceHistory
17151
- };
17192
+ const path2 = this.appendTeamQuery(
17193
+ `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
17194
+ { teamId }
17195
+ );
17196
+ const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
17197
+ if (!availability.ok) {
17198
+ throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
17199
+ }
17200
+ const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
17201
+ for (const id of page.ready) canonicalReady.add(id);
17202
+ }
17203
+ const masterKey = this.getMasterKeyBytes();
17204
+ for (const embed of params.preparedEmbeds) {
17205
+ if (canonicalReady.has(embed.embedId)) continue;
17206
+ const encrypted = await encryptEmbed(
17207
+ embed,
17208
+ masterKey,
17209
+ chatKeyBytes,
17210
+ chatId,
17211
+ messageId2,
17212
+ ownerId2
17213
+ );
17214
+ if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
17215
+ throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
17216
+ }
17217
+ encryptedEmbeds.push(encrypted);
17152
17218
  }
17153
17219
  }
17154
- const preflightPayload = {
17155
- protocol_version: protocolVersion,
17156
- chat_id: chatId,
17157
- ...teamId ? { team_id: teamId } : {},
17158
- turn_id: turnId,
17159
- message_id: messageId2,
17160
- chat_key_version: chatKeyVersion,
17161
- encrypted_chat_key: encryptedChatKey,
17162
- recovery_public_key: recoveryKeypair.publicKey,
17163
- expected_messages_v: baselineMessagesV,
17164
- encrypted_user_message: encryptedUserMessage,
17165
- inference_request: preflightInferenceRequest,
17166
- ...conditionalPrivacyReceipt ? {
17167
- privacy_receipt_id: conditionalPrivacyReceipt.receiptId,
17168
- privacy_generation: conditionalPrivacyReceipt.generation
17169
- } : {}
17220
+ if (!params.incognito && encryptedEmbeds.length > 0) {
17221
+ if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
17222
+ assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId: messageId2, ownerId: ownerId2 });
17223
+ }
17224
+ if (encryptedEmbeds.length > 0) {
17225
+ messagePayload.encrypted_embeds = encryptedEmbeds;
17226
+ }
17227
+ let dispatchHandlersReady = () => {
17170
17228
  };
17171
- if (isNewChat) {
17172
- if (!chatSlugLookupKey) {
17173
- throw new Error("Encrypted chat slug lookup key was not initialized.");
17174
- }
17175
- const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
17176
- const slugMetadata = await buildEncryptedObjectSlugMetadata({
17177
- value: selectNewChatSlugValue({
17178
- message: finalMessage,
17179
- chatId,
17180
- explicitSlug: params.slug
17181
- }),
17182
- encryptionKey: chatKeyBytes,
17183
- lookupKey: chatSlugLookupKey
17184
- });
17185
- preflightPayload.encrypted_chat_metadata = {
17186
- encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
17187
- encrypted_slug: slugMetadata.encrypted_slug,
17188
- slug_lookup_hash: slugMetadata.slug_lookup_hash,
17189
- encrypted_chat_key: encryptedChatKey,
17229
+ const handlersReady = new Promise((resolve20) => {
17230
+ dispatchHandlersReady = resolve20;
17231
+ });
17232
+ let admissionPayload = null;
17233
+ let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId2, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
17234
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
17235
+ traceSendTiming("preflight_begin");
17236
+ const protocolVersion = 1;
17237
+ const chatKeyVersion = 1;
17238
+ const turnId = privacyTurnId;
17239
+ savedTurnId = turnId;
17240
+ terminalExpectedMessagesV = baselineMessagesV + 1;
17241
+ const recoveryKeypairStartedAt = performance.now();
17242
+ const recoveryKeypair = independentRecoveryKeypair ? await independentRecoveryKeypair : await deriveChatCompletionRecoveryKeypair(
17243
+ Buffer.from(chatKeyBytes).toString("base64url"),
17244
+ chatId,
17245
+ chatKeyVersion
17246
+ );
17247
+ recoveryKeypairMs = Math.round(performance.now() - recoveryKeypairStartedAt);
17248
+ const encryptPayloadStartedAt = performance.now();
17249
+ const [encryptedContent, encryptedSenderName, encryptedPiiMappings] = await Promise.all([
17250
+ encryptWithAesGcmCombined(finalMessage, chatKeyBytes),
17251
+ encryptWithAesGcmCombined(teamSenderName, chatKeyBytes),
17252
+ piiMappings.length ? encryptWithAesGcmCombined(JSON.stringify(piiMappings), chatKeyBytes) : null
17253
+ ]);
17254
+ const encryptedUserMessage = {
17255
+ client_message_id: messageId2,
17256
+ chat_id: chatId,
17257
+ encrypted_content: encryptedContent,
17258
+ role: "user",
17190
17259
  created_at: createdAt,
17191
17260
  updated_at: createdAt
17192
17261
  };
17193
- }
17194
- encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17195
- try {
17262
+ encryptedUserMessage.encrypted_sender_name = encryptedSenderName;
17263
+ if (encryptedPiiMappings) encryptedUserMessage.encrypted_pii_mappings = encryptedPiiMappings;
17264
+ Object.assign(messagePayload, {
17265
+ turn_id: turnId,
17266
+ recovery_public_key: recoveryKeypair.publicKey,
17267
+ chat_key_version: chatKeyVersion
17268
+ });
17269
+ let preflightInferenceRequest = messagePayload;
17196
17270
  if (teamId) {
17197
- await stageCliTeamNotificationPreview(ws, {
17198
- teamId,
17199
- chatId,
17200
- messageId: messageId2,
17201
- content: finalMessage,
17202
- title: notificationTitle
17271
+ const teamInferenceHistory = [
17272
+ ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
17273
+ role: historyMessage.role,
17274
+ content: historyMessage.content,
17275
+ category: historyMessage.category ?? null,
17276
+ sender_name: historyMessage.sender_name ?? historyMessage.role,
17277
+ created_at: historyMessage.created_at
17278
+ })),
17279
+ {
17280
+ role: "user",
17281
+ content: finalMessage,
17282
+ category: null,
17283
+ sender_name: teamSenderName,
17284
+ created_at: createdAt
17285
+ }
17286
+ ];
17287
+ const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId2 };
17288
+ delete messagePayload.message_history;
17289
+ messagePayload.message = teamMessageEnvelope;
17290
+ if (shouldWaitForAi) {
17291
+ messagePayload.team_ai_invocation = { history: teamInferenceHistory };
17292
+ preflightInferenceRequest = {
17293
+ ...messagePayload,
17294
+ message_history: teamInferenceHistory
17295
+ };
17296
+ }
17297
+ }
17298
+ const preflightPayload = {
17299
+ protocol_version: protocolVersion,
17300
+ chat_id: chatId,
17301
+ ...teamId ? { team_id: teamId } : {},
17302
+ turn_id: turnId,
17303
+ message_id: messageId2,
17304
+ chat_key_version: chatKeyVersion,
17305
+ encrypted_chat_key: encryptedChatKey,
17306
+ recovery_public_key: recoveryKeypair.publicKey,
17307
+ expected_messages_v: baselineMessagesV,
17308
+ encrypted_user_message: encryptedUserMessage,
17309
+ inference_request: preflightInferenceRequest,
17310
+ ...conditionalPrivacyReceipt ? {
17311
+ privacy_receipt_id: conditionalPrivacyReceipt.receiptId,
17312
+ privacy_generation: conditionalPrivacyReceipt.generation
17313
+ } : {}
17314
+ };
17315
+ if (isNewChat) {
17316
+ if (!chatSlugLookupKey) {
17317
+ throw new Error("Encrypted chat slug lookup key was not initialized.");
17318
+ }
17319
+ const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
17320
+ const slugMetadata = await buildEncryptedObjectSlugMetadata({
17321
+ value: selectNewChatSlugValue({
17322
+ message: finalMessage,
17323
+ chatId,
17324
+ explicitSlug: params.slug
17325
+ }),
17326
+ encryptionKey: chatKeyBytes,
17327
+ lookupKey: chatSlugLookupKey
17203
17328
  });
17329
+ preflightPayload.encrypted_chat_metadata = {
17330
+ encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
17331
+ encrypted_slug: slugMetadata.encrypted_slug,
17332
+ slug_lookup_hash: slugMetadata.slug_lookup_hash,
17333
+ encrypted_chat_key: encryptedChatKey,
17334
+ created_at: createdAt,
17335
+ updated_at: createdAt
17336
+ };
17204
17337
  }
17205
- if (params.projectId) {
17206
- await prepareCliProjectFocusForPreflight({
17338
+ encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17339
+ try {
17340
+ if (teamId) {
17341
+ await stageCliTeamNotificationPreview(ws, {
17342
+ teamId,
17343
+ chatId,
17344
+ messageId: messageId2,
17345
+ content: finalMessage,
17346
+ title: notificationTitle
17347
+ });
17348
+ }
17349
+ if (params.projectId) {
17350
+ await prepareCliProjectFocusForPreflight({
17351
+ ws,
17352
+ chatId,
17353
+ teamId,
17354
+ isNewChat,
17355
+ encryptedChatKey,
17356
+ createdAt,
17357
+ activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17358
+ });
17359
+ const focusedContext = await prepareCliJevContext(
17360
+ this,
17361
+ chatId,
17362
+ { teamId, personal: !teamId },
17363
+ params.jevContext,
17364
+ finalMessage
17365
+ );
17366
+ pendingContext = Promise.resolve(focusedContext);
17367
+ }
17368
+ admissionPayload = preflightPayload;
17369
+ traceSendTiming("preflight_ready");
17370
+ } catch (error) {
17371
+ ws.close();
17372
+ throw error;
17373
+ }
17374
+ }
17375
+ const initialHeaderOwner = this.preparedChatOwner(this.requireSession(), teamId);
17376
+ const startInitialHeader = () => {
17377
+ if (!initialAdmissionDurable || !initialHeaderNotice || !chatKeyBytes || !isNewChat || params.incognito || !savedTurnId) return;
17378
+ const notice = { ...initialHeaderNotice }, key = chatKeyBytes, turn = savedTurnId;
17379
+ const titleChanged = !!notice.chatTitle && notice.chatTitle !== initialTitleScheduled;
17380
+ const iconChanged = !!notice.chatIcon && notice.chatIcon !== initialIconScheduled;
17381
+ if (!titleChanged && !iconChanged) return;
17382
+ initialTitleScheduled = notice.chatTitle ?? initialTitleScheduled;
17383
+ initialIconScheduled = notice.chatIcon ?? initialIconScheduled;
17384
+ const current2 = () => !initialHeaderStopped && ws.isOpen() && this.preparedChatOwnerCurrent(initialHeaderOwner, teamId) && this.activeSubmittedChatTurns.get(chatId) === turn;
17385
+ initialHeaderWork = (initialHeaderWork ?? Promise.resolve()).then(async () => {
17386
+ if (!current2()) return;
17387
+ if (titleChanged) await this.persistPostProcessingMetadata({
17207
17388
  ws,
17208
17389
  chatId,
17209
17390
  teamId,
17210
- isNewChat,
17391
+ chatKeyBytes: key,
17211
17392
  encryptedChatKey,
17212
- createdAt,
17213
- activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17393
+ followUpSuggestions: [],
17394
+ suggestedFollowupMessage: null,
17395
+ newChatSuggestions: [],
17396
+ chatSummary: null,
17397
+ chatTags: [],
17398
+ updatedChatTitle: notice.chatTitle ?? null,
17399
+ sourceTitleVersion: notice.sourceTitleVersion ?? null,
17400
+ sourceMetadataVersion: notice.sourceMetadataVersion ?? null,
17401
+ unversionedGeneratedTitle: notice.sourceTitleVersion === void 0 && notice.sourceMetadataVersion === void 0,
17402
+ ownerCurrent: current2
17214
17403
  });
17215
- const focusedContext = await prepareCliJevContext(
17216
- this,
17217
- chatId,
17218
- { teamId, personal: !teamId },
17219
- params.jevContext,
17220
- finalMessage
17221
- );
17222
- pendingContext = Promise.resolve(focusedContext);
17404
+ if (!current2()) return;
17405
+ if (iconChanged || notice.category) {
17406
+ const [encryptedIcon, encryptedCategory] = await Promise.all([
17407
+ notice.chatIcon ? encryptWithAesGcmCombined(notice.chatIcon, key) : null,
17408
+ notice.category ? encryptWithAesGcmCombined(notice.category, key) : null
17409
+ ]);
17410
+ if (!current2()) return;
17411
+ const stored = ws.waitForMessage("encrypted_metadata_stored", (value) => value.chat_id === chatId, 2e4);
17412
+ void stored.catch(() => {
17413
+ });
17414
+ await ws.sendAsync("encrypted_chat_metadata", {
17415
+ chat_id: chatId,
17416
+ ...teamId ? { team_id: teamId } : {},
17417
+ encrypted_chat_key: encryptedChatKey,
17418
+ ...encryptedIcon ? { encrypted_icon: encryptedIcon } : {},
17419
+ ...encryptedCategory ? { encrypted_chat_category: encryptedCategory } : {}
17420
+ });
17421
+ await stored;
17422
+ }
17423
+ if (current2()) clearSyncCache(teamId);
17424
+ }).catch(() => {
17425
+ });
17426
+ };
17427
+ const onTurnStream = (event) => {
17428
+ if (isNewChat && event.kind === "typing" && (event.chatTitle || event.chatIcon) && event.chatId === chatId && event.userMessageId === messageId2 && event.turnId === savedTurnId && event.taskId && (!initialHeaderNotice?.taskId || event.taskId === initialHeaderNotice.taskId)) {
17429
+ initialHeaderNotice = { ...initialHeaderNotice, ...event };
17430
+ startInitialHeader();
17223
17431
  }
17224
- admissionPayload = preflightPayload;
17225
- traceSendTiming("preflight_ready");
17226
- } catch (error) {
17227
- ws.close();
17228
- throw error;
17229
- }
17230
- }
17231
- const initialHeaderOwner = this.preparedChatOwner(this.requireSession(), teamId);
17232
- const startInitialHeader = () => {
17233
- if (!initialAdmissionDurable || !initialHeaderNotice || !chatKeyBytes || !isNewChat || params.incognito || !savedTurnId) return;
17234
- const notice = { ...initialHeaderNotice }, key = chatKeyBytes, turn = savedTurnId;
17235
- const titleChanged = !!notice.chatTitle && notice.chatTitle !== initialTitleScheduled;
17236
- const iconChanged = !!notice.chatIcon && notice.chatIcon !== initialIconScheduled;
17237
- if (!titleChanged && !iconChanged) return;
17238
- initialTitleScheduled = notice.chatTitle ?? initialTitleScheduled;
17239
- initialIconScheduled = notice.chatIcon ?? initialIconScheduled;
17240
- const current2 = () => !initialHeaderStopped && ws.isOpen() && this.preparedChatOwnerCurrent(initialHeaderOwner, teamId) && this.activeSubmittedChatTurns.get(chatId) === turn;
17241
- initialHeaderWork = (initialHeaderWork ?? Promise.resolve()).then(async () => {
17242
- if (!current2()) return;
17243
- if (titleChanged) await this.persistPostProcessingMetadata({
17432
+ params.onStream?.(event);
17433
+ };
17434
+ if (!params.incognito && chatKeyBytes && savedTurnId) {
17435
+ const key = chatKeyBytes;
17436
+ const ownedTurnId = savedTurnId;
17437
+ this.activeSubmittedChatTurns.set(chatId, ownedTurnId);
17438
+ const owner2 = this.preparedChatOwner(this.requireSession(), teamId);
17439
+ const stopOwned = () => optionalMetadata?.stop();
17440
+ optionalMetadata = observeOwnedTurnMetadata({
17244
17441
  ws,
17245
17442
  chatId,
17246
- teamId,
17247
- chatKeyBytes: key,
17248
- encryptedChatKey,
17249
- followUpSuggestions: [],
17250
- suggestedFollowupMessage: null,
17251
- newChatSuggestions: [],
17252
- chatSummary: null,
17253
- chatTags: [],
17254
- updatedChatTitle: notice.chatTitle ?? null,
17255
- sourceTitleVersion: notice.sourceTitleVersion ?? null,
17256
- sourceMetadataVersion: notice.sourceMetadataVersion ?? null,
17257
- unversionedGeneratedTitle: notice.sourceTitleVersion === void 0 && notice.sourceMetadataVersion === void 0,
17258
- ownerCurrent: current2
17443
+ turnId: savedTurnId,
17444
+ userMessageId: messageId2,
17445
+ closeAfterMetadata: true,
17446
+ ownerCurrent: () => this.preparedChatOwnerCurrent(owner2, teamId),
17447
+ persist: async (metadata) => {
17448
+ await this.persistPostProcessingMetadata({ ws, teamId, chatKeyBytes: key, encryptedChatKey, ...metadata });
17449
+ clearSyncCache(teamId);
17450
+ },
17451
+ onMetadata: params.onMetadata,
17452
+ onReleased: () => {
17453
+ this.ownedMetadataSubscriptions.delete(stopOwned);
17454
+ if (this.activeSubmittedChatTurns.get(chatId) === ownedTurnId) this.activeSubmittedChatTurns.delete(chatId);
17455
+ if (metadataTransportOwned) ws.close();
17456
+ }
17259
17457
  });
17260
- if (!current2()) return;
17261
- if (iconChanged || notice.category) {
17262
- const [encryptedIcon, encryptedCategory] = await Promise.all([
17263
- notice.chatIcon ? encryptWithAesGcmCombined(notice.chatIcon, key) : null,
17264
- notice.category ? encryptWithAesGcmCombined(notice.category, key) : null
17265
- ]);
17266
- if (!current2()) return;
17267
- const stored = ws.waitForMessage("encrypted_metadata_stored", (value) => value.chat_id === chatId, 2e4);
17268
- void stored.catch(() => {
17269
- });
17270
- await ws.sendAsync("encrypted_chat_metadata", {
17271
- chat_id: chatId,
17272
- ...teamId ? { team_id: teamId } : {},
17273
- encrypted_chat_key: encryptedChatKey,
17274
- ...encryptedIcon ? { encrypted_icon: encryptedIcon } : {},
17275
- ...encryptedCategory ? { encrypted_chat_category: encryptedCategory } : {}
17276
- });
17277
- await stored;
17278
- }
17279
- if (current2()) clearSyncCache(teamId);
17280
- }).catch(() => {
17281
- });
17282
- };
17283
- const onTurnStream = (event) => {
17284
- if (isNewChat && event.kind === "typing" && (event.chatTitle || event.chatIcon) && event.chatId === chatId && event.userMessageId === messageId2 && event.turnId === savedTurnId && event.taskId && (!initialHeaderNotice?.taskId || event.taskId === initialHeaderNotice.taskId)) {
17285
- initialHeaderNotice = { ...initialHeaderNotice, ...event };
17286
- startInitialHeader();
17458
+ while (this.ownedMetadataSubscriptions.size >= 8) this.ownedMetadataSubscriptions.values().next().value?.();
17459
+ this.ownedMetadataSubscriptions.add(stopOwned);
17287
17460
  }
17288
- params.onStream?.(event);
17289
- };
17290
- if (!params.incognito && chatKeyBytes && savedTurnId) {
17291
- const key = chatKeyBytes;
17292
- const ownedTurnId = savedTurnId;
17293
- this.activeSubmittedChatTurns.set(chatId, ownedTurnId);
17294
- const owner2 = this.preparedChatOwner(this.requireSession(), teamId);
17295
- const stopOwned = () => optionalMetadata?.stop();
17296
- optionalMetadata = observeOwnedTurnMetadata({
17297
- ws,
17298
- chatId,
17299
- turnId: savedTurnId,
17300
- userMessageId: messageId2,
17301
- closeAfterMetadata: true,
17302
- ownerCurrent: () => this.preparedChatOwnerCurrent(owner2, teamId),
17303
- persist: async (metadata) => {
17304
- await this.persistPostProcessingMetadata({ ws, teamId, chatKeyBytes: key, encryptedChatKey, ...metadata });
17305
- clearSyncCache(teamId);
17306
- },
17307
- onMetadata: params.onMetadata,
17308
- onReleased: () => {
17309
- this.ownedMetadataSubscriptions.delete(stopOwned);
17310
- if (this.activeSubmittedChatTurns.get(chatId) === ownedTurnId) this.activeSubmittedChatTurns.delete(chatId);
17311
- if (metadataTransportOwned) ws.close();
17312
- }
17461
+ if (!params.incognito && shouldWaitForAi) {
17462
+ precollectedResponse = ws.collectAiResponse(messageId2, chatId, {
17463
+ waitForPostProcessing: false,
17464
+ onStream: onTurnStream,
17465
+ onSubChatEvent: async (event) => {
17466
+ await handlersReady;
17467
+ await handleSubChatEvent(event);
17468
+ },
17469
+ onAppSettingsMemoriesRequest: async (event) => {
17470
+ await handlersReady;
17471
+ await handleAppSettingsMemoriesRequest(event);
17472
+ },
17473
+ timeoutMs: params.responseTimeoutMs,
17474
+ recoveryTurnId: savedTurnId,
17475
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17476
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17477
+ });
17478
+ }
17479
+ const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
17480
+ const event = payload;
17481
+ return event.chat_id === chatId && event.user_message_id === messageId2;
17482
+ }, params.responseTimeoutMs ?? 9e4) : null;
17483
+ if (queuedAdmission) void queuedAdmission.catch(() => {
17313
17484
  });
17314
- while (this.ownedMetadataSubscriptions.size >= 8) this.ownedMetadataSubscriptions.values().next().value?.();
17315
- this.ownedMetadataSubscriptions.add(stopOwned);
17316
- }
17317
- if (!params.incognito && shouldWaitForAi) {
17318
- precollectedResponse = ws.collectAiResponse(messageId2, chatId, {
17319
- waitForPostProcessing: false,
17320
- onStream: onTurnStream,
17321
- onSubChatEvent: async (event) => {
17322
- await handlersReady;
17323
- await handleSubChatEvent(event);
17324
- },
17325
- onAppSettingsMemoriesRequest: async (event) => {
17326
- await handlersReady;
17327
- await handleAppSettingsMemoriesRequest(event);
17328
- },
17329
- timeoutMs: params.responseTimeoutMs,
17330
- recoveryTurnId: savedTurnId,
17331
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17332
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17485
+ const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
17486
+ const event = payload;
17487
+ return event.chat_id === chatId && (event.message_id === messageId2 || event.message_id === void 0);
17488
+ }, params.responseTimeoutMs ?? 9e4) : null;
17489
+ if (pausedHandoff) void pausedHandoff.catch(() => {
17333
17490
  });
17334
- }
17335
- const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
17336
- const event = payload;
17337
- return event.chat_id === chatId && event.user_message_id === messageId2;
17338
- }, params.responseTimeoutMs ?? 9e4) : null;
17339
- if (queuedAdmission) void queuedAdmission.catch(() => {
17340
- });
17341
- const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
17342
- const event = payload;
17343
- return event.chat_id === chatId && (event.message_id === messageId2 || event.message_id === void 0);
17344
- }, params.responseTimeoutMs ?? 9e4) : null;
17345
- if (pausedHandoff) void pausedHandoff.catch(() => {
17346
- });
17347
- const confirmed = ws.waitForMessage(
17348
- "chat_message_confirmed",
17349
- (payload) => {
17350
- const eventPayload = payload;
17351
- return eventPayload.chat_id === chatId && eventPayload.message_id === messageId2;
17352
- },
17353
- params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
17354
- );
17355
- let confirmedPayload;
17356
- let admissionContinuationPhase = "before_dispatch";
17357
- let admissionAckObserved = false;
17358
- let routingFrameSent = false;
17359
- let detailsFrameSent = false;
17360
- const admissionContinuationStartedAt = performance.now();
17361
- try {
17362
- if (admissionPayload) {
17363
- if (!useDeferredContext && recoveryDiscovery) await recoveryDiscovery;
17364
- if (recoveryDiscoveryError) throw recoveryDiscoveryError;
17365
- if (!contextRequestId && pendingContext) {
17366
- const context = await pendingContext;
17367
- Object.assign(messagePayload, context);
17368
- Object.assign(admissionPayload.inference_request, context);
17369
- }
17370
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17371
- const admissionAck = ws.waitForMessage("chat_turn_preflight_ack", (payload) => payload.turn_id === savedTurnId);
17372
- void admissionAck.catch(() => {
17373
- });
17374
- const frameDispatchAt = performance.now();
17375
- admissionContinuationPhase = "await_ack";
17376
- await consumeSocketMemoryReceipt();
17377
- await ws.sendAsync(useCombinedAdmission ? "chat_turn_admit" : "chat_turn_preflight", admissionPayload);
17378
- traceSendTiming("admission_frame");
17379
- if (process.env.OPENMATES_DEBUG === "1") {
17380
- console.error(`[chat admission preparation] sdk_minted=${sdkMintedChatId} defer_recovery=${mayDeferRecovery} combined_admission=${useCombinedAdmission} caps_wait_ms=${capabilitiesWaitMs} key_wait_ms=${keyWaitMs} recovery_keypair_ms=${recoveryKeypairMs} encrypt_payload_ms=${encryptPayloadMs} socket_ready_to_frame_dispatch_ms=${Math.round(frameDispatchAt - socketReadyForSendAt)} frame_dispatch_from_send_start_ms=${Math.round(frameDispatchAt - preWsStartedAt)} wire_send_ms=${Math.round(performance.now() - frameDispatchAt)}`);
17381
- }
17382
- const ackPayload = (await admissionAck).payload;
17383
- traceSendTiming("admission_ack");
17384
- admissionAckObserved = true;
17385
- if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
17386
- throw new Error("Encrypted chat admission acknowledgement omitted preflight_id.");
17387
- }
17388
- if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
17389
- terminalExpectedMessagesV = ackPayload.committed_messages_v;
17390
- }
17391
- initialAdmissionDurable = true;
17392
- startInitialHeader();
17393
- if (!useCombinedAdmission) {
17394
- Object.assign(messagePayload, { protocol_version: admissionPayload.protocol_version, preflight_id: ackPayload.preflight_id });
17395
- await ws.sendAsync("chat_message_added", messagePayload);
17396
- }
17397
- if (pendingContext && contextRequestId) {
17398
- admissionContinuationPhase = "await_routing";
17399
- const identity3 = {
17400
- chat_id: chatId,
17401
- turn_id: savedTurnId,
17402
- message_id: messageId2,
17403
- request_id: contextRequestId,
17404
- ...teamId ? { team_id: teamId } : {}
17405
- };
17406
- let failureNotice = null;
17407
- const notifyContextFailure = (reason) => {
17408
- if (failureNotice) return failureNotice;
17409
- if (!ws.supportsServerCapability("chat_turn_context_failure_v1") || !ws.isOpen() || !contextOwnerIsCurrent() || !savedTurnId) return Promise.resolve();
17410
- failureNotice = (async () => {
17411
- const failureAck = ws.waitForMessage("chat_turn_context_failed_ack", (payload) => {
17412
- const event = payload;
17413
- return event.chat_id === chatId && event.turn_id === savedTurnId && event.message_id === messageId2 && event.request_id === contextRequestId;
17414
- }, 5e3);
17415
- void failureAck.catch(() => {
17416
- });
17417
- await ws.sendAsync("chat_turn_context_failed", { ...identity3, reason });
17418
- const status = (await failureAck).payload;
17419
- if (status.status !== "accepted" && status.status !== "duplicate") {
17420
- throw new Error("Chat context failure receipt was rejected.");
17421
- }
17422
- })();
17423
- return failureNotice;
17424
- };
17425
- let routing;
17426
- try {
17427
- routing = useSelectionDetailsV2 ? await projectRoutingContext : deferredProjectDirectory ? (await Promise.all([pendingContext, projectRoutingContext]))[0] : params.projectId ? await pendingContext : await Promise.race([routingContext, pendingContext]);
17428
- } catch (error) {
17429
- try {
17430
- await notifyContextFailure(useSelectionDetailsV2 ? "project_discovery_failed" : "context_preparation_failed");
17431
- } catch {
17432
- }
17433
- throw error;
17491
+ const confirmed = ws.waitForMessage(
17492
+ "chat_message_confirmed",
17493
+ (payload) => {
17494
+ const eventPayload = payload;
17495
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId2;
17496
+ },
17497
+ params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
17498
+ );
17499
+ let confirmedPayload;
17500
+ let admissionContinuationPhase = "before_dispatch";
17501
+ let admissionAckObserved = false;
17502
+ let routingFrameSent = false;
17503
+ let detailsFrameSent = false;
17504
+ const admissionContinuationStartedAt = performance.now();
17505
+ try {
17506
+ if (admissionPayload) {
17507
+ if (!useDeferredContext && recoveryDiscovery) await recoveryDiscovery;
17508
+ if (recoveryDiscoveryError) throw recoveryDiscoveryError;
17509
+ if (!contextRequestId && pendingContext) {
17510
+ const context = await pendingContext;
17511
+ Object.assign(messagePayload, context);
17512
+ Object.assign(admissionPayload.inference_request, context);
17434
17513
  }
17435
17514
  if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17436
- let rejectContextAck;
17437
- contextAckRejection = new Promise((_resolve, reject) => {
17438
- rejectContextAck = reject;
17439
- });
17440
- void contextAckRejection.catch(() => {
17515
+ const admissionAck = ws.waitForMessage("chat_turn_preflight_ack", (payload) => payload.turn_id === savedTurnId);
17516
+ void admissionAck.catch(() => {
17441
17517
  });
17442
- let rejectedPhaseSeen = false;
17443
- const attemptedContextPhases = /* @__PURE__ */ new Set();
17444
- removeContextAckWatcher = ws.onMessageType("chat_turn_context_ack", (event) => {
17445
- if (rejectedPhaseSeen || event.chat_id !== chatId || event.turn_id !== savedTurnId || event.message_id !== messageId2 || event.request_id !== contextRequestId || event.phase !== "routing" && event.phase !== "details" || !attemptedContextPhases.has(event.phase)) return;
17446
- if (event.status === "accepted" || event.status === "duplicate") {
17447
- traceSendTiming(event.phase === "routing" ? "routing_ack" : "details_ack");
17448
- return;
17449
- }
17450
- if (event.status !== "rejected") return;
17451
- rejectedPhaseSeen = true;
17452
- if (!contextOwnerIsCurrent()) {
17453
- rejectContextAck(new Error("Chat workspace changed while preparing context."));
17454
- return;
17518
+ const frameDispatchAt = performance.now();
17519
+ admissionContinuationPhase = "await_ack";
17520
+ await consumeSocketMemoryReceipt();
17521
+ await ws.sendAsync(useCombinedAdmission ? "chat_turn_admit" : "chat_turn_preflight", admissionPayload);
17522
+ traceSendTiming("admission_frame");
17523
+ if (process.env.OPENMATES_DEBUG === "1") {
17524
+ console.error(`[chat admission preparation] sdk_minted=${sdkMintedChatId} defer_recovery=${mayDeferRecovery} combined_admission=${useCombinedAdmission} caps_wait_ms=${capabilitiesWaitMs} key_wait_ms=${keyWaitMs} recovery_keypair_ms=${recoveryKeypairMs} encrypt_payload_ms=${encryptPayloadMs} socket_ready_to_frame_dispatch_ms=${Math.round(frameDispatchAt - socketReadyForSendAt)} frame_dispatch_from_send_start_ms=${Math.round(frameDispatchAt - preWsStartedAt)} wire_send_ms=${Math.round(performance.now() - frameDispatchAt)}`);
17525
+ }
17526
+ const ackPayload = (await admissionAck).payload;
17527
+ traceSendTiming("admission_ack");
17528
+ admissionAckObserved = true;
17529
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
17530
+ throw new Error("Encrypted chat admission acknowledgement omitted preflight_id.");
17531
+ }
17532
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
17533
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
17534
+ }
17535
+ initialAdmissionDurable = true;
17536
+ startInitialHeader();
17537
+ if (!useCombinedAdmission) {
17538
+ Object.assign(messagePayload, { protocol_version: admissionPayload.protocol_version, preflight_id: ackPayload.preflight_id });
17539
+ await ws.sendAsync("chat_message_added", messagePayload);
17540
+ }
17541
+ if (pendingContext && contextRequestId) {
17542
+ admissionContinuationPhase = "await_routing";
17543
+ const identity3 = {
17544
+ chat_id: chatId,
17545
+ turn_id: savedTurnId,
17546
+ message_id: messageId2,
17547
+ request_id: contextRequestId,
17548
+ ...teamId ? { team_id: teamId } : {}
17549
+ };
17550
+ let failureNotice = null;
17551
+ const notifyContextFailure = (reason) => {
17552
+ if (failureNotice) return failureNotice;
17553
+ if (!ws.supportsServerCapability("chat_turn_context_failure_v1") || !ws.isOpen() || !contextOwnerIsCurrent() || !savedTurnId) return Promise.resolve();
17554
+ failureNotice = (async () => {
17555
+ const failureAck = ws.waitForMessage("chat_turn_context_failed_ack", (payload) => {
17556
+ const event = payload;
17557
+ return event.chat_id === chatId && event.turn_id === savedTurnId && event.message_id === messageId2 && event.request_id === contextRequestId;
17558
+ }, 5e3);
17559
+ void failureAck.catch(() => {
17560
+ });
17561
+ await ws.sendAsync("chat_turn_context_failed", { ...identity3, reason });
17562
+ const status = (await failureAck).payload;
17563
+ if (status.status !== "accepted" && status.status !== "duplicate") {
17564
+ throw new Error("Chat context failure receipt was rejected.");
17565
+ }
17566
+ })();
17567
+ return failureNotice;
17568
+ };
17569
+ let routing;
17570
+ try {
17571
+ routing = useSelectionDetailsV2 ? await projectRoutingContext : deferredProjectDirectory ? (await Promise.all([pendingContext, projectRoutingContext]))[0] : params.projectId ? await pendingContext : await Promise.race([routingContext, pendingContext]);
17572
+ } catch (error) {
17573
+ try {
17574
+ await notifyContextFailure(useSelectionDetailsV2 ? "project_discovery_failed" : "context_preparation_failed");
17575
+ } catch {
17576
+ }
17577
+ throw error;
17455
17578
  }
17456
- void notifyContextFailure("context_preparation_failed").catch(() => {
17457
- }).then(() => {
17458
- rejectContextAck(new Error("Chat context delivery was rejected."));
17579
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17580
+ let rejectContextAck;
17581
+ contextAckRejection = new Promise((_resolve, reject) => {
17582
+ rejectContextAck = reject;
17459
17583
  });
17460
- });
17461
- const { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } = routing;
17462
- traceSendTiming("routing_prepared");
17463
- attemptedContextPhases.add("routing");
17464
- await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "routing", context: useSelectionDetailsV2 ? { project_focus_candidates } : { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } });
17465
- routingFrameSent = true;
17466
- traceSendTiming("routing_frame");
17467
- admissionContinuationPhase = "await_details";
17468
- let context;
17469
- try {
17470
- context = await Promise.race([pendingContext, contextAckRejection]);
17471
- } catch (error) {
17584
+ void contextAckRejection.catch(() => {
17585
+ });
17586
+ let rejectedPhaseSeen = false;
17587
+ const attemptedContextPhases = /* @__PURE__ */ new Set();
17588
+ removeContextAckWatcher = ws.onMessageType("chat_turn_context_ack", (event) => {
17589
+ if (rejectedPhaseSeen || event.chat_id !== chatId || event.turn_id !== savedTurnId || event.message_id !== messageId2 || event.request_id !== contextRequestId || event.phase !== "routing" && event.phase !== "details" || !attemptedContextPhases.has(event.phase)) return;
17590
+ if (event.status === "accepted" || event.status === "duplicate") {
17591
+ traceSendTiming(event.phase === "routing" ? "routing_ack" : "details_ack");
17592
+ return;
17593
+ }
17594
+ if (event.status !== "rejected") return;
17595
+ rejectedPhaseSeen = true;
17596
+ if (!contextOwnerIsCurrent()) {
17597
+ rejectContextAck(new Error("Chat workspace changed while preparing context."));
17598
+ return;
17599
+ }
17600
+ void notifyContextFailure("context_preparation_failed").catch(() => {
17601
+ }).then(() => {
17602
+ rejectContextAck(new Error("Chat context delivery was rejected."));
17603
+ });
17604
+ });
17605
+ const { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } = routing;
17606
+ traceSendTiming("routing_prepared");
17607
+ attemptedContextPhases.add("routing");
17608
+ await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "routing", context: useSelectionDetailsV2 ? { project_focus_candidates } : { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } });
17609
+ routingFrameSent = true;
17610
+ traceSendTiming("routing_frame");
17611
+ admissionContinuationPhase = "await_details";
17612
+ let context;
17472
17613
  try {
17473
- await notifyContextFailure("context_preparation_failed");
17474
- } catch {
17614
+ context = await Promise.race([pendingContext, contextAckRejection]);
17615
+ } catch (error) {
17616
+ try {
17617
+ await notifyContextFailure("context_preparation_failed");
17618
+ } catch {
17619
+ }
17620
+ throw error;
17475
17621
  }
17476
- throw error;
17622
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17623
+ traceSendTiming("details_prepared");
17624
+ attemptedContextPhases.add("details");
17625
+ await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "details", context: {
17626
+ custom_memory_documents: context.custom_memory_documents ?? [],
17627
+ project_focus_documents: context.project_focus_documents ?? [],
17628
+ project_context_documents: context.project_context_documents ?? [],
17629
+ ...useSelectionDetailsV2 ? {
17630
+ related_task_candidates: context.related_task_candidates ?? [],
17631
+ project_focus_catalog: context.project_focus_catalog ?? [],
17632
+ accepted_plan_context: context.accepted_plan_context ?? null,
17633
+ ...connectedAccountDirectory ? { connected_account_directory: connectedAccountDirectory } : {}
17634
+ } : {}
17635
+ } });
17636
+ detailsFrameSent = true;
17637
+ traceSendTiming("details_frame");
17477
17638
  }
17478
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17479
- traceSendTiming("details_prepared");
17480
- attemptedContextPhases.add("details");
17481
- await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "details", context: {
17482
- custom_memory_documents: context.custom_memory_documents ?? [],
17483
- project_focus_documents: context.project_focus_documents ?? [],
17484
- project_context_documents: context.project_context_documents ?? [],
17485
- ...useSelectionDetailsV2 ? {
17486
- related_task_candidates: context.related_task_candidates ?? [],
17487
- project_focus_catalog: context.project_focus_catalog ?? [],
17488
- accepted_plan_context: context.accepted_plan_context ?? null,
17489
- ...connectedAccountDirectory ? { connected_account_directory: connectedAccountDirectory } : {}
17490
- } : {}
17491
- } });
17492
- detailsFrameSent = true;
17493
- traceSendTiming("details_frame");
17639
+ admissionContinuationPhase = "await_confirmation";
17640
+ } else {
17641
+ await consumeSocketMemoryReceipt();
17642
+ await ws.sendAsync("chat_message_added", messagePayload);
17494
17643
  }
17495
- admissionContinuationPhase = "await_confirmation";
17496
- } else {
17497
- await consumeSocketMemoryReceipt();
17498
- await ws.sendAsync("chat_message_added", messagePayload);
17644
+ confirmedPayload = (await (contextAckRejection ? Promise.race([confirmed, contextAckRejection]) : confirmed)).payload;
17645
+ } catch (error) {
17646
+ if (process.env.OPENMATES_DEBUG === "1" && admissionPayload) {
17647
+ console.error(`[chat admission continuation] phase=${admissionContinuationPhase} ack_observed=${admissionAckObserved} routing_sent=${routingFrameSent} details_sent=${detailsFrameSent} socket_open=${ws.isOpen()} elapsed_ms=${Math.round(performance.now() - admissionContinuationStartedAt)}`);
17648
+ }
17649
+ dispatchHandlersReady();
17650
+ void confirmed.catch(() => {
17651
+ });
17652
+ if (precollectedResponse) void precollectedResponse.catch(() => {
17653
+ });
17654
+ ws.close();
17655
+ throw error;
17499
17656
  }
17500
- confirmedPayload = (await (contextAckRejection ? Promise.race([confirmed, contextAckRejection]) : confirmed)).payload;
17501
- } catch (error) {
17502
- if (process.env.OPENMATES_DEBUG === "1" && admissionPayload) {
17503
- console.error(`[chat admission continuation] phase=${admissionContinuationPhase} ack_observed=${admissionAckObserved} routing_sent=${routingFrameSent} details_sent=${detailsFrameSent} socket_open=${ws.isOpen()} elapsed_ms=${Math.round(performance.now() - admissionContinuationStartedAt)}`);
17657
+ if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
17658
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v;
17504
17659
  }
17505
- dispatchHandlersReady();
17506
- void confirmed.catch(() => {
17507
- });
17508
- if (precollectedResponse) void precollectedResponse.catch(() => {
17509
- });
17510
- ws.close();
17511
- throw error;
17512
- }
17513
- if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
17514
- terminalExpectedMessagesV = confirmedPayload.new_messages_v;
17515
- }
17516
- if (!params.incognito && (!teamId || shouldWaitForAi)) {
17517
- clearSyncCache(teamId);
17518
- }
17519
- let assistant = "";
17520
- let assistantMessageId = null;
17521
- let category = null;
17522
- let modelName = null;
17523
- let tokenUsage = null;
17524
- let promptBudget = null;
17525
- let followUpSuggestions = [];
17526
- let suggestedFollowupMessage = null;
17527
- let taskProposals = [];
17528
- let taskUpdateProposals = [];
17529
- let taskEvents = [];
17530
- let pendingTaskUpdateJobs = [];
17531
- let subChatEvents = [];
17532
- const appSettingsMemoryRequests = [];
17533
- const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
17534
- const isApprovalWithinServerLimits = (request) => {
17535
- const count = request.subChats.length;
17536
- if (count === 0) return false;
17537
- if (request.remainingSubChats !== null && count > request.remainingSubChats) {
17538
- return false;
17539
- }
17540
- if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17541
- return false;
17660
+ if (!params.incognito && (!teamId || shouldWaitForAi)) {
17661
+ clearSyncCache(teamId);
17542
17662
  }
17543
- return true;
17544
- };
17545
- const isAutoApprovalWithinServerLimits = (request) => {
17546
- if (!isApprovalWithinServerLimits(request)) return false;
17547
- return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
17548
- };
17549
- const handleSubChatEvent = async (event) => {
17550
- params.onSubChatEvent?.(event);
17551
- if (event.type === "spawn_sub_chats") {
17552
- if (params.incognito || !chatKeyBytes) return;
17553
- const rawSubChats = event.payload.sub_chats;
17554
- if (!Array.isArray(rawSubChats)) return;
17555
- const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
17556
- parentChatId: chatId,
17557
- parentChatKey: chatKeyBytes,
17558
- encryptedParentChatKey: encryptedChatKey,
17559
- subChats: rawSubChats
17560
- });
17561
- for (const metadataPayload of metadataPayloads) {
17562
- await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
17663
+ let assistant = "";
17664
+ let assistantMessageId = null;
17665
+ let category = null;
17666
+ let modelName = null;
17667
+ let tokenUsage = null;
17668
+ let promptBudget = null;
17669
+ let followUpSuggestions = [];
17670
+ let suggestedFollowupMessage = null;
17671
+ let taskProposals = [];
17672
+ let taskUpdateProposals = [];
17673
+ let taskEvents = [];
17674
+ let pendingTaskUpdateJobs = [];
17675
+ let subChatEvents = [];
17676
+ const appSettingsMemoryRequests = [];
17677
+ const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
17678
+ const isApprovalWithinServerLimits = (request) => {
17679
+ const count = request.subChats.length;
17680
+ if (count === 0) return false;
17681
+ if (request.remainingSubChats !== null && count > request.remainingSubChats) {
17682
+ return false;
17563
17683
  }
17564
- return;
17565
- }
17566
- if (event.type !== "sub_chat_confirmation_required") return;
17567
- const payload = event.payload;
17568
- const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
17569
- const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
17570
- const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
17571
- if (!taskId) return;
17572
- const request = {
17573
- chatId: confirmationChatId,
17574
- taskId,
17575
- subChats,
17576
- maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
17577
- maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
17578
- existingSubChats: numberOrNull(payload.existing_sub_chats),
17579
- remainingSubChats: numberOrNull(payload.remaining_sub_chats)
17580
- };
17581
- const withinLimits = isApprovalWithinServerLimits(request);
17582
- const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
17583
- await ws.sendAsync(
17584
- "sub_chat_confirmation",
17585
- buildSubChatConfirmationPayload({
17586
- chatId: request.chatId,
17587
- taskId: request.taskId,
17588
- approved,
17589
- approveCount: approved ? request.subChats.length : void 0
17590
- })
17591
- );
17592
- };
17593
- const persistSystemMessage = async (systemMessage) => {
17594
- if (!chatKeyBytes) return;
17595
- await ws.sendAsync("chat_system_message_added", {
17596
- chat_id: chatId,
17597
- message: {
17598
- message_id: systemMessage.message_id,
17599
- role: "system",
17600
- encrypted_content: await encryptWithAesGcmCombined(
17601
- systemMessage.content,
17602
- chatKeyBytes
17603
- ),
17604
- created_at: systemMessage.created_at,
17605
- user_message_id: systemMessage.user_message_id
17684
+ if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17685
+ return false;
17606
17686
  }
17607
- });
17608
- await ws.waitForMessage(
17609
- "system_message_confirmed",
17610
- (payload) => {
17611
- const p = payload;
17612
- return p.message_id === systemMessage.message_id;
17613
- },
17614
- 2e4
17615
- );
17616
- };
17617
- const persistMemoryRequestSystemMessage = async (event) => {
17618
- const requestId = event.requestId ?? `${messageId2}-memory-request`;
17619
- await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
17620
- userMessageId: messageId2,
17621
- requestId,
17622
- requestedKeys: event.requestedKeys,
17623
- entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
17624
- createdAt: Math.floor(Date.now() / 1e3)
17625
- }));
17626
- };
17627
- const responseCategoriesFromMemories = (approvedMemories) => {
17628
- const counts = /* @__PURE__ */ new Map();
17629
- for (const memory of approvedMemories) {
17630
- const key = `${memory.app_id}-${memory.item_key}`;
17631
- const existing = counts.get(key);
17632
- if (existing) {
17633
- existing.entryCount += 1;
17634
- } else {
17635
- counts.set(key, {
17636
- appId: memory.app_id,
17637
- itemType: memory.item_key,
17638
- entryCount: 1
17687
+ return true;
17688
+ };
17689
+ const isAutoApprovalWithinServerLimits = (request) => {
17690
+ if (!isApprovalWithinServerLimits(request)) return false;
17691
+ return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
17692
+ };
17693
+ const handleSubChatEvent = async (event) => {
17694
+ params.onSubChatEvent?.(event);
17695
+ if (event.type === "spawn_sub_chats") {
17696
+ if (params.incognito || !chatKeyBytes) return;
17697
+ const rawSubChats = event.payload.sub_chats;
17698
+ if (!Array.isArray(rawSubChats)) return;
17699
+ const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
17700
+ parentChatId: chatId,
17701
+ parentChatKey: chatKeyBytes,
17702
+ encryptedParentChatKey: encryptedChatKey,
17703
+ subChats: rawSubChats
17639
17704
  });
17705
+ for (const metadataPayload of metadataPayloads) {
17706
+ await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
17707
+ }
17708
+ return;
17640
17709
  }
17641
- }
17642
- return [...counts.values()];
17643
- };
17644
- const handleAppSettingsMemoriesRequest = async (event) => {
17645
- await persistMemoryRequestSystemMessage(event);
17646
- if (params.autoApproveMemories) {
17647
- const requested = new Set(event.requestedKeys);
17648
- const unadvertisedKeys = event.requestedKeys.filter(
17649
- (key) => !memoryMetadataKeys.includes(key)
17710
+ if (event.type !== "sub_chat_confirmation_required") return;
17711
+ const payload = event.payload;
17712
+ const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
17713
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
17714
+ const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
17715
+ if (!taskId) return;
17716
+ const request = {
17717
+ chatId: confirmationChatId,
17718
+ taskId,
17719
+ subChats,
17720
+ maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
17721
+ maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
17722
+ existingSubChats: numberOrNull(payload.existing_sub_chats),
17723
+ remainingSubChats: numberOrNull(payload.remaining_sub_chats)
17724
+ };
17725
+ const withinLimits = isApprovalWithinServerLimits(request);
17726
+ const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
17727
+ await ws.sendAsync(
17728
+ "sub_chat_confirmation",
17729
+ buildSubChatConfirmationPayload({
17730
+ chatId: request.chatId,
17731
+ taskId: request.taskId,
17732
+ approved,
17733
+ approveCount: approved ? request.subChats.length : void 0
17734
+ })
17650
17735
  );
17651
- if (unadvertisedKeys.length > 0) {
17652
- throw new Error(
17653
- `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
17654
- );
17655
- }
17656
- const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
17657
- app_id: memory.app_id,
17658
- item_key: memory.item_type,
17659
- content: memory.data
17660
- }));
17661
- const approvedKeys = [
17662
- ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
17663
- ];
17664
- const categories = responseCategoriesFromMemories(approvedMemories);
17665
- appSettingsMemoryRequests.push({
17666
- requestId: event.requestId,
17667
- requestedKeys: event.requestedKeys,
17668
- approvedKeys,
17669
- entryCount: approvedMemories.length
17670
- });
17671
- await ws.sendAsync("app_settings_memories_confirmed", {
17672
- chat_id: event.chatId,
17673
- app_settings_memories: approvedMemories
17736
+ };
17737
+ const persistSystemMessage = async (systemMessage) => {
17738
+ if (!chatKeyBytes) return;
17739
+ await ws.sendAsync("chat_system_message_added", {
17740
+ chat_id: chatId,
17741
+ message: {
17742
+ message_id: systemMessage.message_id,
17743
+ role: "system",
17744
+ encrypted_content: await encryptWithAesGcmCombined(
17745
+ systemMessage.content,
17746
+ chatKeyBytes
17747
+ ),
17748
+ created_at: systemMessage.created_at,
17749
+ user_message_id: systemMessage.user_message_id
17750
+ }
17674
17751
  });
17675
- await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17752
+ await ws.waitForMessage(
17753
+ "system_message_confirmed",
17754
+ (payload) => {
17755
+ const p = payload;
17756
+ return p.message_id === systemMessage.message_id;
17757
+ },
17758
+ 2e4
17759
+ );
17760
+ };
17761
+ const persistMemoryRequestSystemMessage = async (event) => {
17762
+ const requestId = event.requestId ?? `${messageId2}-memory-request`;
17763
+ await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
17676
17764
  userMessageId: messageId2,
17677
- messageId: `${messageId2}-memory-response`,
17678
- action: "included",
17679
- categories,
17765
+ requestId,
17766
+ requestedKeys: event.requestedKeys,
17767
+ entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
17680
17768
  createdAt: Math.floor(Date.now() / 1e3)
17681
17769
  }));
17682
- return;
17683
- }
17684
- appSettingsMemoryRequests.push({
17685
- requestId: event.requestId,
17686
- requestedKeys: event.requestedKeys,
17687
- approvedKeys: [],
17688
- entryCount: 0
17689
- });
17690
- throw new Error(
17691
- `The assistant requested memories (${event.requestedKeys.join(", ")}). Rerun with --auto-approve-memories to explicitly approve requested memory categories from the CLI, or continue this chat in the web app to approve or reject the request.`
17692
- );
17693
- };
17694
- const persistedTaskEventIds = /* @__PURE__ */ new Set();
17695
- const persistTaskEventSystemMessages = async (events) => {
17696
- if (!chatKeyBytes || events.length === 0) return;
17697
- for (const event of events) {
17698
- if (persistedTaskEventIds.has(event.event_id)) continue;
17699
- await this.persistEncryptedSystemMessage(
17700
- ws,
17701
- await buildTaskEventSystemMessage({
17702
- chatKey: chatKeyBytes,
17703
- userMessageId: messageId2,
17704
- event
17705
- }),
17706
- chatId
17707
- );
17708
- persistedTaskEventIds.add(event.event_id);
17709
- }
17710
- };
17711
- const streamOpts = {
17712
- waitForPostProcessing: false,
17713
- onStream: onTurnStream,
17714
- onSubChatEvent: handleSubChatEvent,
17715
- onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
17716
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17717
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17718
- };
17719
- dispatchHandlersReady();
17720
- if (!shouldWaitForAi) {
17721
- ws.close();
17722
- } else if (params.incognito) {
17723
- try {
17724
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17725
- ...streamOpts,
17726
- timeoutMs: params.responseTimeoutMs,
17727
- recoveryTurnId: savedTurnId
17728
- }));
17729
- assistantMessageId = resp.messageId;
17730
- assistant = resp.content;
17731
- category = resp.category;
17732
- modelName = resp.modelName;
17733
- tokenUsage = resp.tokenUsage;
17734
- promptBudget = resp.promptBudget;
17735
- taskEvents = resp.taskEvents;
17736
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17737
- subChatEvents = resp.subChatEvents;
17738
- if (resp.status === "waiting_for_user") {
17739
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17740
- return {
17741
- status: resp.status,
17742
- chatId,
17743
- messageId: assistantMessageId,
17744
- assistant,
17745
- category,
17746
- modelName,
17747
- mateName: category ? MATE_NAMES[category] ?? null : null,
17748
- followUpSuggestions,
17749
- taskProposals,
17750
- taskUpdateProposals,
17751
- taskEvents,
17752
- pendingTaskUpdateJobs,
17753
- subChatEvents,
17754
- appSettingsMemoryRequests,
17755
- tokenUsage,
17756
- promptBudget
17757
- };
17758
- }
17759
- } finally {
17760
- ws.close();
17761
- }
17762
- } else {
17763
- try {
17764
- const recoverCurrentTurnOutputs = async () => {
17765
- if (recoveryDiscovery) {
17766
- try {
17767
- await recoveryDiscovery;
17768
- } catch (error) {
17769
- if (!useDeferredContext) throw error;
17770
- return;
17771
- }
17772
- }
17773
- const pages = ws.drainAvailableRecoveryOutputPages();
17774
- if (pages.length === 0) return;
17775
- if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
17776
- if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
17777
- throw new Error("Saved chat recovery output requires the root chat key.");
17770
+ };
17771
+ const responseCategoriesFromMemories = (approvedMemories) => {
17772
+ const counts = /* @__PURE__ */ new Map();
17773
+ for (const memory of approvedMemories) {
17774
+ const key = `${memory.app_id}-${memory.item_key}`;
17775
+ const existing = counts.get(key);
17776
+ if (existing) {
17777
+ existing.entryCount += 1;
17778
+ } else {
17779
+ counts.set(key, {
17780
+ appId: memory.app_id,
17781
+ itemType: memory.item_key,
17782
+ entryCount: 1
17783
+ });
17778
17784
  }
17779
- const cache = loadSyncCache(teamId) ?? {
17780
- syncedAt: Date.now(),
17781
- totalChatCount: 0,
17782
- loadedChatCount: 0,
17783
- chats: [],
17784
- embeds: [],
17785
- embedKeys: [],
17786
- chatKeyWrappers: []
17787
- };
17788
- if (encryptedChatKey) {
17789
- const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
17790
- if (root) root.details.encrypted_chat_key = encryptedChatKey;
17791
- else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
17785
+ }
17786
+ return [...counts.values()];
17787
+ };
17788
+ const handleAppSettingsMemoriesRequest = async (event) => {
17789
+ await persistMemoryRequestSystemMessage(event);
17790
+ if (params.autoApproveMemories) {
17791
+ const requested = new Set(event.requestedKeys);
17792
+ const unadvertisedKeys = event.requestedKeys.filter(
17793
+ (key) => !memoryMetadataKeys.includes(key)
17794
+ );
17795
+ if (unadvertisedKeys.length > 0) {
17796
+ throw new Error(
17797
+ `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
17798
+ );
17792
17799
  }
17793
- await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
17794
- };
17795
- const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17796
- ...streamOpts,
17797
- timeoutMs: params.responseTimeoutMs,
17798
- recoveryTurnId: savedTurnId
17799
- });
17800
- const outcome = await Promise.race([
17801
- responsePromise.then((response) => ({ kind: "response", response })),
17802
- ...contextAckRejection ? [contextAckRejection] : [],
17803
- ...queuedAdmission ? [queuedAdmission.then(
17804
- (frame) => ({ kind: "queued", frame }),
17805
- () => new Promise(() => {
17806
- })
17807
- )] : [],
17808
- ...pausedHandoff ? [pausedHandoff.then(
17809
- (frame) => ({ kind: "paused", frame }),
17810
- () => new Promise(() => {
17811
- })
17812
- )] : []
17813
- ]);
17814
- if (outcome.kind === "paused") {
17815
- void responsePromise.catch(() => {
17800
+ const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
17801
+ app_id: memory.app_id,
17802
+ item_key: memory.item_type,
17803
+ content: memory.data
17804
+ }));
17805
+ const approvedKeys = [
17806
+ ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
17807
+ ];
17808
+ const categories = responseCategoriesFromMemories(approvedMemories);
17809
+ appSettingsMemoryRequests.push({
17810
+ requestId: event.requestId,
17811
+ requestedKeys: event.requestedKeys,
17812
+ approvedKeys,
17813
+ entryCount: approvedMemories.length
17816
17814
  });
17817
- throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
17818
- }
17819
- if (outcome.kind === "queued") {
17820
- void responsePromise.catch(() => {
17815
+ await ws.sendAsync("app_settings_memories_confirmed", {
17816
+ chat_id: event.chatId,
17817
+ app_settings_memories: approvedMemories
17821
17818
  });
17822
- const frame = outcome.frame.payload;
17823
- return {
17824
- status: "queued",
17825
- chatId,
17826
- messageId: null,
17819
+ await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17827
17820
  userMessageId: messageId2,
17828
- queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
17829
- queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
17830
- assistant: "",
17831
- category: null,
17832
- modelName: null,
17833
- mateName: null,
17834
- followUpSuggestions: [],
17835
- taskProposals: [],
17836
- taskUpdateProposals: [],
17837
- taskEvents: [],
17838
- pendingTaskUpdateJobs: [],
17839
- subChatEvents: [],
17840
- appSettingsMemoryRequests: [],
17841
- tokenUsage: null,
17842
- promptBudget: null
17843
- };
17821
+ messageId: `${messageId2}-memory-response`,
17822
+ action: "included",
17823
+ categories,
17824
+ createdAt: Math.floor(Date.now() / 1e3)
17825
+ }));
17826
+ return;
17844
17827
  }
17845
- const resp = outcome.response;
17846
- assistantMessageId = resp.messageId;
17847
- assistant = resp.content;
17848
- category = resp.category;
17849
- modelName = resp.modelName;
17850
- tokenUsage = resp.tokenUsage;
17851
- promptBudget = resp.promptBudget;
17852
- followUpSuggestions = resp.followUpSuggestions;
17853
- suggestedFollowupMessage = resp.suggestedFollowupMessage;
17854
- taskProposals = resp.taskProposals;
17855
- taskUpdateProposals = resp.taskUpdateProposals;
17856
- taskEvents = resp.taskEvents;
17857
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17858
- subChatEvents = resp.subChatEvents;
17859
- if (resp.status === "waiting_for_user") {
17860
- await persistTaskEventSystemMessages(taskEvents);
17861
- await recoverCurrentTurnOutputs();
17862
- return {
17863
- status: resp.status,
17864
- chatId,
17865
- messageId: assistantMessageId,
17866
- assistant,
17867
- category,
17868
- modelName,
17869
- mateName: category ? MATE_NAMES[category] ?? null : null,
17870
- followUpSuggestions,
17871
- taskProposals,
17872
- taskUpdateProposals,
17873
- taskEvents,
17874
- pendingTaskUpdateJobs,
17875
- subChatEvents,
17876
- appSettingsMemoryRequests,
17877
- tokenUsage,
17878
- promptBudget
17879
- };
17828
+ appSettingsMemoryRequests.push({
17829
+ requestId: event.requestId,
17830
+ requestedKeys: event.requestedKeys,
17831
+ approvedKeys: [],
17832
+ entryCount: 0
17833
+ });
17834
+ throw new Error(
17835
+ `The assistant requested memories (${event.requestedKeys.join(", ")}). Rerun with --auto-approve-memories to explicitly approve requested memory categories from the CLI, or continue this chat in the web app to approve or reject the request.`
17836
+ );
17837
+ };
17838
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
17839
+ const persistTaskEventSystemMessages = async (events) => {
17840
+ if (!chatKeyBytes || events.length === 0) return;
17841
+ for (const event of events) {
17842
+ if (persistedTaskEventIds.has(event.event_id)) continue;
17843
+ await this.persistEncryptedSystemMessage(
17844
+ ws,
17845
+ await buildTaskEventSystemMessage({
17846
+ chatKey: chatKeyBytes,
17847
+ userMessageId: messageId2,
17848
+ event
17849
+ }),
17850
+ chatId
17851
+ );
17852
+ persistedTaskEventIds.add(event.event_id);
17880
17853
  }
17881
- if (chatKeyBytes) {
17882
- if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
17883
- throw new Error("Saved chat completion did not include recoverable terminal identity.");
17854
+ };
17855
+ const streamOpts = {
17856
+ waitForPostProcessing: false,
17857
+ onStream: onTurnStream,
17858
+ onSubChatEvent: handleSubChatEvent,
17859
+ onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
17860
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17861
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17862
+ };
17863
+ dispatchHandlersReady();
17864
+ if (!shouldWaitForAi) {
17865
+ ws.close();
17866
+ } else if (params.incognito) {
17867
+ try {
17868
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17869
+ ...streamOpts,
17870
+ timeoutMs: params.responseTimeoutMs,
17871
+ recoveryTurnId: savedTurnId
17872
+ }));
17873
+ assistantMessageId = resp.messageId;
17874
+ assistant = resp.content;
17875
+ category = resp.category;
17876
+ modelName = resp.modelName;
17877
+ tokenUsage = resp.tokenUsage;
17878
+ promptBudget = resp.promptBudget;
17879
+ taskEvents = resp.taskEvents;
17880
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17881
+ subChatEvents = resp.subChatEvents;
17882
+ if (resp.status === "waiting_for_user") {
17883
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17884
+ return {
17885
+ status: resp.status,
17886
+ chatId,
17887
+ messageId: assistantMessageId,
17888
+ assistant,
17889
+ category,
17890
+ modelName,
17891
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17892
+ followUpSuggestions,
17893
+ taskProposals,
17894
+ taskUpdateProposals,
17895
+ taskEvents,
17896
+ pendingTaskUpdateJobs,
17897
+ subChatEvents,
17898
+ appSettingsMemoryRequests,
17899
+ tokenUsage,
17900
+ promptBudget
17901
+ };
17884
17902
  }
17885
- const recoveryJobId = resp.recoveryJobId;
17886
- let claimRetryDeadline = null;
17887
- let claim;
17888
- let claimRetryDelayMs = 500;
17889
- while (true) {
17890
- const requestId = randomUUID9();
17891
- let matchingLeaseConflict = false;
17892
- const stopWatchingErrors = ws.onMessageType("error", (payload) => {
17893
- if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
17894
- matchingLeaseConflict = true;
17895
- }
17896
- });
17897
- const claimPromise = ws.waitForMessage(
17898
- "recovery_job_claimed",
17899
- (payload) => {
17900
- const received = payload;
17901
- return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
17902
- },
17903
- claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
17904
- );
17905
- try {
17903
+ } finally {
17904
+ ws.close();
17905
+ }
17906
+ } else {
17907
+ try {
17908
+ const recoverCurrentTurnOutputs = async () => {
17909
+ if (recoveryDiscovery) {
17906
17910
  try {
17907
- await ws.sendAsync("recovery_job_claim", {
17908
- protocol_version: 1,
17909
- job_id: recoveryJobId,
17910
- request_id: requestId
17911
- });
17911
+ await recoveryDiscovery;
17912
17912
  } catch (error) {
17913
- void claimPromise.catch(() => {
17914
- });
17915
- throw error;
17913
+ if (!useDeferredContext) throw error;
17914
+ return;
17916
17915
  }
17917
- claim = (await claimPromise).payload;
17918
- break;
17919
- } catch (error) {
17920
- if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
17921
- claimRetryDeadline ??= Date.now() + Math.min(
17922
- 1e4,
17923
- Math.max(0, params.responseTimeoutMs ?? 9e4)
17924
- );
17925
- const remainingMs = claimRetryDeadline - Date.now();
17926
- if (remainingMs <= claimRetryDelayMs) throw error;
17927
- await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
17928
- claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
17929
- } finally {
17930
- stopWatchingErrors();
17931
17916
  }
17917
+ const pages = ws.drainAvailableRecoveryOutputPages();
17918
+ if (pages.length === 0) return;
17919
+ if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
17920
+ if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
17921
+ throw new Error("Saved chat recovery output requires the root chat key.");
17922
+ }
17923
+ const cache = loadSyncCache(teamId) ?? {
17924
+ syncedAt: Date.now(),
17925
+ totalChatCount: 0,
17926
+ loadedChatCount: 0,
17927
+ chats: [],
17928
+ embeds: [],
17929
+ embedKeys: [],
17930
+ chatKeyWrappers: []
17931
+ };
17932
+ if (encryptedChatKey) {
17933
+ const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
17934
+ if (root) root.details.encrypted_chat_key = encryptedChatKey;
17935
+ else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
17936
+ }
17937
+ await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
17938
+ };
17939
+ const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17940
+ ...streamOpts,
17941
+ timeoutMs: params.responseTimeoutMs,
17942
+ recoveryTurnId: savedTurnId
17943
+ });
17944
+ const outcome = await Promise.race([
17945
+ responsePromise.then((response) => ({ kind: "response", response })),
17946
+ ...contextAckRejection ? [contextAckRejection] : [],
17947
+ ...queuedAdmission ? [queuedAdmission.then(
17948
+ (frame) => ({ kind: "queued", frame }),
17949
+ () => new Promise(() => {
17950
+ })
17951
+ )] : [],
17952
+ ...pausedHandoff ? [pausedHandoff.then(
17953
+ (frame) => ({ kind: "paused", frame }),
17954
+ () => new Promise(() => {
17955
+ })
17956
+ )] : []
17957
+ ]);
17958
+ if (outcome.kind === "paused") {
17959
+ void responsePromise.catch(() => {
17960
+ });
17961
+ throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
17932
17962
  }
17933
- const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
17934
- const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
17935
- const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
17936
- const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
17937
- const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
17938
- if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
17939
- assistant = resp.content;
17940
- category = resp.category;
17941
- modelName = resp.modelName;
17942
- assistantMessageId = assistantId;
17943
- await this.persistStreamedEmbeds({
17944
- ws,
17945
- embeds: resp.embeds,
17946
- chatId,
17947
- chatKeyBytes,
17948
- fallbackMessageId: assistantId,
17949
- ownerId: ownerId2,
17950
- teamId
17963
+ if (outcome.kind === "queued") {
17964
+ void responsePromise.catch(() => {
17951
17965
  });
17952
- clearSyncCache(teamId);
17953
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17966
+ const frame = outcome.frame.payload;
17967
+ return {
17968
+ status: "queued",
17969
+ chatId,
17970
+ messageId: null,
17971
+ userMessageId: messageId2,
17972
+ queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
17973
+ queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
17974
+ assistant: "",
17975
+ category: null,
17976
+ modelName: null,
17977
+ mateName: null,
17978
+ followUpSuggestions: [],
17979
+ taskProposals: [],
17980
+ taskUpdateProposals: [],
17981
+ taskEvents: [],
17982
+ pendingTaskUpdateJobs: [],
17983
+ subChatEvents: [],
17984
+ appSettingsMemoryRequests: [],
17985
+ tokenUsage: null,
17986
+ promptBudget: null
17987
+ };
17988
+ }
17989
+ const resp = outcome.response;
17990
+ assistantMessageId = resp.messageId;
17991
+ assistant = resp.content;
17992
+ category = resp.category;
17993
+ modelName = resp.modelName;
17994
+ tokenUsage = resp.tokenUsage;
17995
+ promptBudget = resp.promptBudget;
17996
+ followUpSuggestions = resp.followUpSuggestions;
17997
+ suggestedFollowupMessage = resp.suggestedFollowupMessage;
17998
+ taskProposals = resp.taskProposals;
17999
+ taskUpdateProposals = resp.taskUpdateProposals;
18000
+ taskEvents = resp.taskEvents;
18001
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
18002
+ subChatEvents = resp.subChatEvents;
18003
+ if (resp.status === "waiting_for_user") {
17954
18004
  await persistTaskEventSystemMessages(taskEvents);
17955
18005
  await recoverCurrentTurnOutputs();
17956
- await projectFocusCountdown?.flush();
17957
- await focusPersistence;
17958
- await chatContextEvents?.flush();
17959
- if (focusPersistenceError) throw focusPersistenceError;
17960
- const mateName2 = category ? MATE_NAMES[category] ?? null : null;
17961
18006
  return {
17962
- status: "completed",
18007
+ status: resp.status,
17963
18008
  chatId,
17964
18009
  messageId: assistantMessageId,
17965
18010
  assistant,
17966
18011
  category,
17967
18012
  modelName,
17968
- mateName: mateName2,
18013
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17969
18014
  followUpSuggestions,
17970
18015
  taskProposals,
17971
18016
  taskUpdateProposals,
@@ -17977,234 +18022,338 @@ var OpenMatesClient = class _OpenMatesClient {
17977
18022
  promptBudget
17978
18023
  };
17979
18024
  }
17980
- if (claim.state !== "LEASED" || !leaseToken || leaseGeneration === null || leaseGeneration < 1 || !assistantId || keyVersion === null || keyVersion !== 1 || claim.chat_id !== chatId || claim.turn_id !== savedTurnId || typeof claim.sealed_payload !== "string") {
17981
- throw new Error("Recovery job claim returned invalid lease or identity data.");
17982
- }
17983
- const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
17984
- Buffer.from(chatKeyBytes).toString("base64url"),
17985
- chatId,
17986
- keyVersion
17987
- );
17988
- let envelope;
17989
- try {
17990
- envelope = JSON.parse(claim.sealed_payload);
17991
- } catch {
17992
- throw new Error("Recovery job contained an invalid sealed envelope.");
17993
- }
17994
- const plaintext = await openChatCompletionRecoveryEnvelope(
17995
- envelope,
17996
- {
17997
- recoveryPrivateKey: recoveryKeypair.privateKey,
17998
- ownerId: ownerId2,
18025
+ if (chatKeyBytes) {
18026
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
18027
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
18028
+ }
18029
+ const recoveryJobId = resp.recoveryJobId;
18030
+ let claimRetryDeadline = null;
18031
+ let claim;
18032
+ let claimRetryDelayMs = 500;
18033
+ while (true) {
18034
+ const requestId = randomUUID9();
18035
+ let matchingLeaseConflict = false;
18036
+ const stopWatchingErrors = ws.onMessageType("error", (payload) => {
18037
+ if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
18038
+ matchingLeaseConflict = true;
18039
+ }
18040
+ });
18041
+ const claimPromise = ws.waitForMessage(
18042
+ "recovery_job_claimed",
18043
+ (payload) => {
18044
+ const received = payload;
18045
+ return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
18046
+ },
18047
+ claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
18048
+ );
18049
+ try {
18050
+ try {
18051
+ await ws.sendAsync("recovery_job_claim", {
18052
+ protocol_version: 1,
18053
+ job_id: recoveryJobId,
18054
+ request_id: requestId
18055
+ });
18056
+ } catch (error) {
18057
+ void claimPromise.catch(() => {
18058
+ });
18059
+ throw error;
18060
+ }
18061
+ claim = (await claimPromise).payload;
18062
+ break;
18063
+ } catch (error) {
18064
+ if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
18065
+ claimRetryDeadline ??= Date.now() + Math.min(
18066
+ 1e4,
18067
+ Math.max(0, params.responseTimeoutMs ?? 9e4)
18068
+ );
18069
+ const remainingMs = claimRetryDeadline - Date.now();
18070
+ if (remainingMs <= claimRetryDelayMs) throw error;
18071
+ await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
18072
+ claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
18073
+ } finally {
18074
+ stopWatchingErrors();
18075
+ }
18076
+ }
18077
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
18078
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
18079
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
18080
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
18081
+ const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
18082
+ if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
18083
+ assistant = resp.content;
18084
+ category = resp.category;
18085
+ modelName = resp.modelName;
18086
+ assistantMessageId = assistantId;
18087
+ await this.persistStreamedEmbeds({
18088
+ ws,
18089
+ embeds: resp.embeds,
18090
+ chatId,
18091
+ chatKeyBytes,
18092
+ fallbackMessageId: assistantId,
18093
+ ownerId: ownerId2,
18094
+ teamId
18095
+ });
18096
+ clearSyncCache(teamId);
18097
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18098
+ await persistTaskEventSystemMessages(taskEvents);
18099
+ await recoverCurrentTurnOutputs();
18100
+ await projectFocusCountdown?.flush();
18101
+ await focusPersistence;
18102
+ await chatContextEvents?.flush();
18103
+ if (focusPersistenceError) throw focusPersistenceError;
18104
+ const mateName2 = category ? MATE_NAMES[category] ?? null : null;
18105
+ return {
18106
+ status: "completed",
18107
+ chatId,
18108
+ messageId: assistantMessageId,
18109
+ assistant,
18110
+ category,
18111
+ modelName,
18112
+ mateName: mateName2,
18113
+ followUpSuggestions,
18114
+ taskProposals,
18115
+ taskUpdateProposals,
18116
+ taskEvents,
18117
+ pendingTaskUpdateJobs,
18118
+ subChatEvents,
18119
+ appSettingsMemoryRequests,
18120
+ tokenUsage,
18121
+ promptBudget
18122
+ };
18123
+ }
18124
+ if (claim.state !== "LEASED" || !leaseToken || leaseGeneration === null || leaseGeneration < 1 || !assistantId || keyVersion === null || keyVersion !== 1 || claim.chat_id !== chatId || claim.turn_id !== savedTurnId || typeof claim.sealed_payload !== "string") {
18125
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
18126
+ }
18127
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
18128
+ Buffer.from(chatKeyBytes).toString("base64url"),
17999
18129
  chatId,
18000
- turnId: savedTurnId,
18001
- jobId: recoveryJobId,
18002
- assistantMessageId: assistantId,
18003
18130
  keyVersion
18131
+ );
18132
+ let envelope;
18133
+ try {
18134
+ envelope = JSON.parse(claim.sealed_payload);
18135
+ } catch {
18136
+ throw new Error("Recovery job contained an invalid sealed envelope.");
18004
18137
  }
18005
- );
18006
- let recovered;
18007
- try {
18008
- recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
18009
- } catch {
18010
- throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
18011
- }
18012
- const expectedRecoveryFields = [
18013
- "assistant_message_id",
18014
- "category",
18015
- "chat_id",
18016
- "content",
18017
- "job_id",
18018
- "key_version",
18019
- "model_name",
18020
- "turn_id"
18021
- ];
18022
- if (Object.keys(recovered).filter((field3) => ![
18023
- "thinking_content",
18024
- "thinking_signature",
18025
- "has_thinking",
18026
- "thinking_token_count",
18027
- "thinking_duration_seconds"
18028
- ].includes(field3)).sort().join(",") !== expectedRecoveryFields.join(",") || recovered.assistant_message_id !== assistantId || recovered.chat_id !== chatId || recovered.turn_id !== savedTurnId || recovered.job_id !== recoveryJobId || recovered.key_version !== keyVersion || typeof recovered.content !== "string" || recovered.category !== null && typeof recovered.category !== "string" || recovered.model_name !== null && typeof recovered.model_name !== "string") {
18029
- throw new Error("Recovery job plaintext did not match the terminal completion identity.");
18030
- }
18031
- assistant = recovered.content;
18032
- category = recovered.category;
18033
- modelName = recovered.model_name;
18034
- const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
18035
- const encryptedAssistantContent = await encryptWithAesGcmCombined(
18036
- assistant,
18037
- chatKeyBytes
18038
- );
18039
- const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
18040
- const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
18041
- const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
18042
- const terminalPersistPayload = {
18043
- protocol_version: 1,
18044
- job_id: recoveryJobId,
18045
- lease_token: leaseToken,
18046
- lease_generation: leaseGeneration,
18047
- encrypted_assistant_message: {
18048
- client_message_id: assistantId,
18049
- chat_id: chatId,
18050
- encrypted_content: encryptedAssistantContent,
18051
- ...await this.encryptThinkingFields(recovered, chatKeyBytes),
18052
- encrypted_sender_name: encryptedSenderName,
18053
- encrypted_category: encryptedCategory,
18054
- encrypted_model_name: encryptedModelName,
18055
- role: "assistant",
18056
- user_message_id: messageId2,
18057
- created_at: completedAt,
18058
- updated_at: completedAt
18138
+ const plaintext = await openChatCompletionRecoveryEnvelope(
18139
+ envelope,
18140
+ {
18141
+ recoveryPrivateKey: recoveryKeypair.privateKey,
18142
+ ownerId: ownerId2,
18143
+ chatId,
18144
+ turnId: savedTurnId,
18145
+ jobId: recoveryJobId,
18146
+ assistantMessageId: assistantId,
18147
+ keyVersion
18148
+ }
18149
+ );
18150
+ let recovered;
18151
+ try {
18152
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
18153
+ } catch {
18154
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
18059
18155
  }
18060
- };
18061
- const persistTerminalRecovery = async (expectedMessagesV) => {
18062
- const persistedPromise = ws.waitForMessage(
18063
- "recovery_job_persisted",
18064
- (payload) => payload.job_id === recoveryJobId,
18065
- 2e4
18156
+ const expectedRecoveryFields = [
18157
+ "assistant_message_id",
18158
+ "category",
18159
+ "chat_id",
18160
+ "content",
18161
+ "job_id",
18162
+ "key_version",
18163
+ "model_name",
18164
+ "turn_id"
18165
+ ];
18166
+ if (Object.keys(recovered).filter((field3) => ![
18167
+ "thinking_content",
18168
+ "thinking_signature",
18169
+ "has_thinking",
18170
+ "thinking_token_count",
18171
+ "thinking_duration_seconds"
18172
+ ].includes(field3)).sort().join(",") !== expectedRecoveryFields.join(",") || recovered.assistant_message_id !== assistantId || recovered.chat_id !== chatId || recovered.turn_id !== savedTurnId || recovered.job_id !== recoveryJobId || recovered.key_version !== keyVersion || typeof recovered.content !== "string" || recovered.category !== null && typeof recovered.category !== "string" || recovered.model_name !== null && typeof recovered.model_name !== "string") {
18173
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
18174
+ }
18175
+ assistant = recovered.content;
18176
+ category = recovered.category;
18177
+ modelName = recovered.model_name;
18178
+ const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
18179
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
18180
+ assistant,
18181
+ chatKeyBytes
18066
18182
  );
18067
- await ws.sendAsync("recovery_job_persist", {
18068
- ...terminalPersistPayload,
18069
- expected_messages_v: expectedMessagesV
18070
- });
18071
- await persistedPromise;
18072
- };
18073
- let terminalPersisted = false;
18074
- try {
18075
- await persistTerminalRecovery(terminalExpectedMessagesV);
18076
- terminalPersisted = true;
18077
- } catch (error) {
18078
- if (error instanceof WebSocketProtocolError) {
18079
- if (error.code === "version_conflict") {
18080
- const latestCache = await this.ensureSynced(
18081
- true,
18082
- [chatId],
18083
- { teamId },
18084
- false,
18085
- void 0,
18086
- void 0,
18087
- { skipTypedRecoveryOutputReplay: true }
18088
- );
18089
- const latestChat = latestCache.chats.find(
18090
- (chat) => String(chat.details.id ?? "") === chatId
18091
- );
18092
- const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
18093
- if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
18094
- await persistTerminalRecovery(latestMessagesV);
18095
- terminalExpectedMessagesV = latestMessagesV;
18096
- terminalPersisted = true;
18097
- }
18183
+ const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
18184
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
18185
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
18186
+ const terminalPersistPayload = {
18187
+ protocol_version: 1,
18188
+ job_id: recoveryJobId,
18189
+ lease_token: leaseToken,
18190
+ lease_generation: leaseGeneration,
18191
+ encrypted_assistant_message: {
18192
+ client_message_id: assistantId,
18193
+ chat_id: chatId,
18194
+ encrypted_content: encryptedAssistantContent,
18195
+ ...await this.encryptThinkingFields(recovered, chatKeyBytes),
18196
+ encrypted_sender_name: encryptedSenderName,
18197
+ encrypted_category: encryptedCategory,
18198
+ encrypted_model_name: encryptedModelName,
18199
+ role: "assistant",
18200
+ user_message_id: messageId2,
18201
+ created_at: completedAt,
18202
+ updated_at: completedAt
18098
18203
  }
18099
- if (!terminalPersisted) {
18100
- throw new Error(`${error.message} (${error.code})`);
18204
+ };
18205
+ const persistTerminalRecovery = async (expectedMessagesV) => {
18206
+ const persistedPromise = ws.waitForMessage(
18207
+ "recovery_job_persisted",
18208
+ (payload) => payload.job_id === recoveryJobId,
18209
+ 2e4
18210
+ );
18211
+ await ws.sendAsync("recovery_job_persist", {
18212
+ ...terminalPersistPayload,
18213
+ expected_messages_v: expectedMessagesV
18214
+ });
18215
+ await persistedPromise;
18216
+ };
18217
+ let terminalPersisted = false;
18218
+ try {
18219
+ await persistTerminalRecovery(terminalExpectedMessagesV);
18220
+ terminalPersisted = true;
18221
+ } catch (error) {
18222
+ if (error instanceof WebSocketProtocolError) {
18223
+ if (error.code === "version_conflict") {
18224
+ const latestCache = await this.ensureSynced(
18225
+ true,
18226
+ [chatId],
18227
+ { teamId },
18228
+ false,
18229
+ void 0,
18230
+ void 0,
18231
+ { skipTypedRecoveryOutputReplay: true }
18232
+ );
18233
+ const latestChat = latestCache.chats.find(
18234
+ (chat) => String(chat.details.id ?? "") === chatId
18235
+ );
18236
+ const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
18237
+ if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
18238
+ await persistTerminalRecovery(latestMessagesV);
18239
+ terminalExpectedMessagesV = latestMessagesV;
18240
+ terminalPersisted = true;
18241
+ }
18242
+ }
18243
+ if (!terminalPersisted) {
18244
+ throw new Error(`${error.message} (${error.code})`);
18245
+ }
18246
+ } else {
18247
+ throw error;
18101
18248
  }
18102
- } else {
18103
- throw error;
18104
18249
  }
18105
- }
18106
- if (!terminalPersisted) {
18107
- throw new Error("Encrypted completion recovery did not persist terminal state.");
18108
- }
18109
- assistantMessageId = assistantId;
18110
- await this.persistStreamedEmbeds({
18111
- ws,
18112
- embeds: resp.embeds,
18113
- chatId,
18114
- chatKeyBytes,
18115
- fallbackMessageId: assistantId,
18116
- ownerId: ownerId2,
18117
- teamId
18118
- });
18119
- if (initialHeaderWork) persistOptionalHeader = () => initialHeaderWork;
18120
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18121
- if (taskUpdateJobsEnabled) {
18122
- const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
18250
+ if (!terminalPersisted) {
18251
+ throw new Error("Encrypted completion recovery did not persist terminal state.");
18252
+ }
18253
+ assistantMessageId = assistantId;
18254
+ await this.persistStreamedEmbeds({
18123
18255
  ws,
18124
- jobs: pendingTaskUpdateJobs,
18125
- events: taskEvents,
18126
- teamId,
18127
- activeChatId: chatId,
18128
- activeChatKeyBytes: chatKeyBytes,
18129
- fallbackUserMessageId: messageId2,
18130
- requireActiveTurnEvent: true
18256
+ embeds: resp.embeds,
18257
+ chatId,
18258
+ chatKeyBytes,
18259
+ fallbackMessageId: assistantId,
18260
+ ownerId: ownerId2,
18261
+ teamId
18131
18262
  });
18132
- pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
18133
- }
18134
- await persistTaskEventSystemMessages(taskEvents);
18135
- await recoverCurrentTurnOutputs();
18136
- clearSyncCache(teamId);
18137
- if (optionalMetadata) {
18138
- metadataHandoffReady = true;
18139
- metadataHandoffSuggestion = resp.suggestedFollowupMessage;
18140
- }
18141
- }
18142
- } finally {
18143
- try {
18144
- await projectFocusCountdown?.flush();
18145
- await focusPersistence;
18146
- if (projectAuthoringAvailable && assistant && chatContextEvents) {
18147
- try {
18148
- const history = boundedAuthoringHistory([
18149
- ...messageHistoryForRequest ?? [],
18150
- { role: "user", content: finalMessage },
18151
- { role: "assistant", content: assistant }
18152
- ]);
18153
- const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
18154
- for (const event of recommendations) chatContextEvents.persist(event);
18155
- } catch {
18263
+ if (initialHeaderWork) persistOptionalHeader = () => initialHeaderWork;
18264
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18265
+ if (taskUpdateJobsEnabled) {
18266
+ const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
18267
+ ws,
18268
+ jobs: pendingTaskUpdateJobs,
18269
+ events: taskEvents,
18270
+ teamId,
18271
+ activeChatId: chatId,
18272
+ activeChatKeyBytes: chatKeyBytes,
18273
+ fallbackUserMessageId: messageId2,
18274
+ requireActiveTurnEvent: true
18275
+ });
18276
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
18277
+ }
18278
+ await persistTaskEventSystemMessages(taskEvents);
18279
+ await recoverCurrentTurnOutputs();
18280
+ clearSyncCache(teamId);
18281
+ if (optionalMetadata) {
18282
+ metadataHandoffReady = true;
18283
+ metadataHandoffSuggestion = resp.suggestedFollowupMessage;
18156
18284
  }
18157
18285
  }
18158
- await chatContextEvents?.flush();
18159
18286
  } finally {
18160
- projectFocusCountdown?.();
18161
- chatContextEvents?.stop();
18162
- if (!metadataHandoffReady) ws.close();
18287
+ try {
18288
+ await projectFocusCountdown?.flush();
18289
+ await focusPersistence;
18290
+ if (projectAuthoringAvailable && assistant && chatContextEvents) {
18291
+ try {
18292
+ const history = boundedAuthoringHistory([
18293
+ ...messageHistoryForRequest ?? [],
18294
+ { role: "user", content: finalMessage },
18295
+ { role: "assistant", content: assistant }
18296
+ ]);
18297
+ const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
18298
+ for (const event of recommendations) chatContextEvents.persist(event);
18299
+ } catch {
18300
+ }
18301
+ }
18302
+ await chatContextEvents?.flush();
18303
+ } finally {
18304
+ projectFocusCountdown?.();
18305
+ chatContextEvents?.stop();
18306
+ if (!metadataHandoffReady) ws.close();
18307
+ }
18308
+ }
18309
+ if (focusPersistenceError) throw focusPersistenceError;
18310
+ if (metadataHandoffReady && optionalMetadata) {
18311
+ metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
18312
+ if (!metadataTransportOwned) ws.close();
18163
18313
  }
18164
18314
  }
18165
- if (focusPersistenceError) throw focusPersistenceError;
18166
- if (metadataHandoffReady && optionalMetadata) {
18167
- metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
18168
- if (!metadataTransportOwned) ws.close();
18315
+ const mateName = category ? MATE_NAMES[category] ?? null : null;
18316
+ if (params.interactiveHuman && shouldWaitForAi && !params.incognito) this.rearmSendSocketWhenClosed(ws, teamId);
18317
+ return {
18318
+ status: "completed",
18319
+ chatId,
18320
+ messageId: assistantMessageId,
18321
+ userMessageId: messageId2,
18322
+ assistant,
18323
+ category,
18324
+ modelName,
18325
+ mateName,
18326
+ followUpSuggestions,
18327
+ suggestedFollowupMessage,
18328
+ taskProposals,
18329
+ taskUpdateProposals,
18330
+ taskEvents,
18331
+ pendingTaskUpdateJobs,
18332
+ subChatEvents,
18333
+ appSettingsMemoryRequests,
18334
+ tokenUsage,
18335
+ promptBudget
18336
+ };
18337
+ } finally {
18338
+ removeContextAckWatcher?.();
18339
+ earlyContextAbort.abort();
18340
+ if (!metadataTransportOwned) {
18341
+ initialHeaderStopped = true;
18342
+ optionalMetadata?.stop();
18343
+ ws.close();
18169
18344
  }
18345
+ if (earlyContext) await earlyContext;
18170
18346
  }
18171
- const mateName = category ? MATE_NAMES[category] ?? null : null;
18172
- return {
18173
- status: "completed",
18174
- chatId,
18175
- messageId: assistantMessageId,
18176
- userMessageId: messageId2,
18177
- assistant,
18178
- category,
18179
- modelName,
18180
- mateName,
18181
- followUpSuggestions,
18182
- suggestedFollowupMessage,
18183
- taskProposals,
18184
- taskUpdateProposals,
18185
- taskEvents,
18186
- pendingTaskUpdateJobs,
18187
- subChatEvents,
18188
- appSettingsMemoryRequests,
18189
- tokenUsage,
18190
- promptBudget
18191
- };
18192
- } finally {
18193
- removeContextAckWatcher?.();
18194
- earlyContextAbort.abort();
18195
- if (!metadataTransportOwned) {
18196
- initialHeaderStopped = true;
18197
- optionalMetadata?.stop();
18198
- ws.close();
18199
- }
18200
- if (earlyContext) await earlyContext;
18347
+ } catch (error) {
18348
+ memorySocket?.ws.close();
18349
+ throw error;
18201
18350
  }
18202
- } catch (error) {
18203
- memorySocket?.ws.close();
18204
- throw error;
18351
+ } finally {
18352
+ historyAbort.abort();
18205
18353
  }
18206
18354
  } finally {
18207
- historyAbort.abort();
18355
+ this.foregroundSendCount--;
18356
+ this.maybeRearmSendSocket();
18208
18357
  }
18209
18358
  }
18210
18359
  async persistEncryptedSystemMessage(ws, systemMessage, targetChatId) {
@@ -18439,6 +18588,30 @@ var OpenMatesClient = class _OpenMatesClient {
18439
18588
  return handledJobIds;
18440
18589
  }
18441
18590
  async persistStreamedEmbeds(params) {
18591
+ const assertOwner = this.captureRecoveryOwner();
18592
+ const ws = {
18593
+ waitForMessage: (...args) => {
18594
+ assertOwner();
18595
+ const result = params.ws.waitForMessage(...args).then((value) => {
18596
+ assertOwner();
18597
+ return value;
18598
+ });
18599
+ void result.catch(() => {
18600
+ });
18601
+ return result;
18602
+ },
18603
+ sendAsync: async (...args) => {
18604
+ assertOwner();
18605
+ await params.ws.sendAsync(...args);
18606
+ assertOwner();
18607
+ }
18608
+ };
18609
+ const http = { get: async (path2, headers) => {
18610
+ assertOwner();
18611
+ const result = await this.http.get(path2, headers);
18612
+ assertOwner();
18613
+ return result;
18614
+ } };
18442
18615
  const finalized = new Map(
18443
18616
  params.embeds.filter((embed) => {
18444
18617
  const status = embed.status ?? "finished";
@@ -18449,7 +18622,7 @@ var OpenMatesClient = class _OpenMatesClient {
18449
18622
  for (const parent of finalized.values()) {
18450
18623
  for (const childId of parent.embed_ids ?? []) {
18451
18624
  if (finalized.has(childId)) continue;
18452
- const childResponse = params.ws.waitForMessage(
18625
+ const childResponse = ws.waitForMessage(
18453
18626
  "send_embed_data",
18454
18627
  (payload) => {
18455
18628
  const frame = payload;
@@ -18457,7 +18630,8 @@ var OpenMatesClient = class _OpenMatesClient {
18457
18630
  return nested.embed_id === childId;
18458
18631
  }
18459
18632
  );
18460
- await params.ws.sendAsync("request_embed", { embed_id: childId });
18633
+ assertOwner();
18634
+ await ws.sendAsync("request_embed", { embed_id: childId });
18461
18635
  const envelope = (await childResponse).payload;
18462
18636
  const child = envelope.payload ?? envelope;
18463
18637
  if (child.parent_embed_id !== parent.embed_id || !child.content || (child.status ?? "finished") !== "finished" || child.chat_id && child.chat_id !== params.chatId && child.chat_id !== computeSHA256(params.chatId)) {
@@ -18471,7 +18645,6 @@ var OpenMatesClient = class _OpenMatesClient {
18471
18645
  }
18472
18646
  const masterKey = this.getMasterKeyBytes();
18473
18647
  const parentKeys = /* @__PURE__ */ new Map();
18474
- const processed = /* @__PURE__ */ new Set();
18475
18648
  const cachedEmbeds = loadSyncCache();
18476
18649
  const resolveCachedParentKey = async (parentId) => {
18477
18650
  if (!cachedEmbeds) return void 0;
@@ -18489,6 +18662,7 @@ var OpenMatesClient = class _OpenMatesClient {
18489
18662
  ) ?? void 0;
18490
18663
  };
18491
18664
  const persistOne = async (embed, embedKey, isChild) => {
18665
+ assertOwner();
18492
18666
  const now = Math.floor(Date.now() / 1e3);
18493
18667
  const createdAt = normalizeUnixSeconds(embed.createdAt, now);
18494
18668
  const updatedAt = normalizeUnixSeconds(embed.updatedAt, now);
@@ -18528,14 +18702,15 @@ var OpenMatesClient = class _OpenMatesClient {
18528
18702
  }
18529
18703
  ] : [];
18530
18704
  const storeRequestId = randomUUID9();
18531
- const storeConfirmed = params.ws.waitForMessage(
18705
+ const storeConfirmed = ws.waitForMessage(
18532
18706
  "store_embed_confirmed",
18533
18707
  (payload) => matchesRecoveryEmbedReceipt(payload, storeRequestId, embed.embed_id),
18534
18708
  3e4
18535
18709
  );
18536
18710
  void storeConfirmed.catch(() => {
18537
18711
  });
18538
- await params.ws.sendAsync("store_embed", {
18712
+ assertOwner();
18713
+ await ws.sendAsync("store_embed", {
18539
18714
  request_id: storeRequestId,
18540
18715
  embed_id: embed.embed_id,
18541
18716
  encrypted_type: encryptedType,
@@ -18566,10 +18741,11 @@ var OpenMatesClient = class _OpenMatesClient {
18566
18741
  let storedHead = true;
18567
18742
  try {
18568
18743
  await storeConfirmed;
18744
+ assertOwner();
18569
18745
  } catch (error) {
18570
18746
  if (!(error instanceof WebSocketProtocolError) || error.code !== "embed_write_denied") throw error;
18571
18747
  const query = params.teamId ? `?team_id=${encodeURIComponent(params.teamId)}` : "";
18572
- const canonical = await this.http.get(
18748
+ const canonical = await http.get(
18573
18749
  `/v1/embeds/chats/${encodeURIComponent(params.chatId)}/embeds/${encodeURIComponent(embed.embed_id)}${query}`,
18574
18750
  this.getCliRequestHeaders()
18575
18751
  );
@@ -18634,14 +18810,15 @@ var OpenMatesClient = class _OpenMatesClient {
18634
18810
  const expectedEncryptedType = head.encrypted_type;
18635
18811
  const expectedDigest = computeSHA256(expectedCiphertext);
18636
18812
  const requestId = randomUUID9();
18637
- const confirmed = params.ws.waitForMessage(
18813
+ const confirmed = ws.waitForMessage(
18638
18814
  "store_embed_confirmed",
18639
18815
  (payload) => matchesRecoveryEmbedReceipt(payload, requestId, embed.embed_id),
18640
18816
  3e4
18641
18817
  );
18642
18818
  void confirmed.catch(() => {
18643
18819
  });
18644
- await params.ws.sendAsync("store_embed", {
18820
+ assertOwner();
18821
+ await ws.sendAsync("store_embed", {
18645
18822
  request_id: requestId,
18646
18823
  embed_id: embed.embed_id,
18647
18824
  chat_id: params.chatId,
@@ -18661,7 +18838,7 @@ var OpenMatesClient = class _OpenMatesClient {
18661
18838
  if (receipt.canonical_source !== "head" || receipt.canonical_digest !== expectedDigest) {
18662
18839
  throw new Error(`Embed ${embed.embed_id} metadata enrichment receipt does not match canonical head.`);
18663
18840
  }
18664
- const enriched = await this.http.get(
18841
+ const enriched = await http.get(
18665
18842
  `/v1/embeds/chats/${encodeURIComponent(params.chatId)}/embeds/${encodeURIComponent(embed.embed_id)}${query}`,
18666
18843
  this.getCliRequestHeaders()
18667
18844
  );
@@ -18674,7 +18851,7 @@ var OpenMatesClient = class _OpenMatesClient {
18674
18851
  }
18675
18852
  if (storedHead && keys2.length > 0) {
18676
18853
  const keysRequestId = randomUUID9();
18677
- const keysConfirmed = params.ws.waitForMessage(
18854
+ const keysConfirmed = ws.waitForMessage(
18678
18855
  "store_embed_keys_confirmed",
18679
18856
  (payload) => {
18680
18857
  const p = payload;
@@ -18682,8 +18859,10 @@ var OpenMatesClient = class _OpenMatesClient {
18682
18859
  },
18683
18860
  3e4
18684
18861
  );
18685
- await params.ws.sendAsync("store_embed_keys", { request_id: keysRequestId, keys: keys2 });
18862
+ assertOwner();
18863
+ await ws.sendAsync("store_embed_keys", { request_id: keysRequestId, keys: keys2 });
18686
18864
  const keysReceipt = (await keysConfirmed).payload;
18865
+ assertOwner();
18687
18866
  if (keysReceipt.failed_count !== 0 || keysReceipt.created_count !== keys2.length) {
18688
18867
  throw new Error(`Embed ${embed.embed_id} key wrappers were not durably stored.`);
18689
18868
  }
@@ -18698,7 +18877,7 @@ var OpenMatesClient = class _OpenMatesClient {
18698
18877
  if (row3.version_number === 1) {
18699
18878
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: params.chatId });
18700
18879
  if (params.teamId) query.set("team_id", params.teamId);
18701
- const existing = await this.http.get(
18880
+ const existing = await http.get(
18702
18881
  `/v1/embeds/${encodeURIComponent(embed.embed_id)}/versions/1?${query}`,
18703
18882
  this.getCliRequestHeaders()
18704
18883
  );
@@ -18721,7 +18900,7 @@ var OpenMatesClient = class _OpenMatesClient {
18721
18900
  const encryptedPatch = typeof row3.patch === "string" ? await encryptWithAesGcmCombined(row3.patch, embedKey) : void 0;
18722
18901
  if (!encryptedSnapshot && !encryptedPatch) continue;
18723
18902
  const diffRequestId = randomUUID9();
18724
- const diffConfirmed = params.ws.waitForMessage(
18903
+ const diffConfirmed = ws.waitForMessage(
18725
18904
  "store_embed_diff_confirmed",
18726
18905
  (payload) => {
18727
18906
  const p = payload;
@@ -18731,7 +18910,8 @@ var OpenMatesClient = class _OpenMatesClient {
18731
18910
  );
18732
18911
  void diffConfirmed.catch(() => {
18733
18912
  });
18734
- await params.ws.sendAsync("store_embed_diff", {
18913
+ assertOwner();
18914
+ await ws.sendAsync("store_embed_diff", {
18735
18915
  request_id: diffRequestId,
18736
18916
  embed_id: row3.embed_id,
18737
18917
  version_number: row3.version_number,
@@ -18741,6 +18921,7 @@ var OpenMatesClient = class _OpenMatesClient {
18741
18921
  created_at: normalizeUnixSeconds(row3.created_at, now)
18742
18922
  });
18743
18923
  const diffReceipt = (await diffConfirmed).payload;
18924
+ assertOwner();
18744
18925
  const expectedDigest = computeSHA256(JSON.stringify([encryptedSnapshot ?? null, encryptedPatch ?? null]));
18745
18926
  if (diffReceipt.canonical_source !== "version_row" || diffReceipt.canonical_digest !== expectedDigest) {
18746
18927
  throw new Error(`Embed ${embed.embed_id} v${row3.version_number} durable receipt did not match the stored source.`);
@@ -18748,34 +18929,19 @@ var OpenMatesClient = class _OpenMatesClient {
18748
18929
  }
18749
18930
  }
18750
18931
  };
18751
- let madeProgress = true;
18752
- while (processed.size < finalized.size && madeProgress) {
18753
- madeProgress = false;
18754
- for (const embed of finalized.values()) {
18755
- if (processed.has(embed.embed_id)) continue;
18756
- const parentId = embed.parent_embed_id || null;
18757
- if (parentId && finalized.has(parentId) && !parentKeys.has(parentId)) {
18758
- continue;
18759
- }
18760
- const parentKey = parentId ? parentKeys.get(parentId) ?? await resolveCachedParentKey(parentId) : void 0;
18761
- if (parentId && !parentKey) continue;
18762
- const embedKey = parentKey ?? await deriveEmbedKeyFromChatKey(params.chatKeyBytes, embed.embed_id);
18763
- await persistOne(embed, embedKey, Boolean(parentId));
18764
- processed.add(embed.embed_id);
18765
- madeProgress = true;
18766
- }
18767
- }
18768
- for (const embed of finalized.values()) {
18769
- if (processed.has(embed.embed_id)) continue;
18932
+ await runRecoveryBatch([...finalized.values()], async (embed) => {
18933
+ assertOwner();
18770
18934
  const parentId = embed.parent_embed_id || null;
18771
18935
  const parentKey = parentId ? parentKeys.get(parentId) ?? await resolveCachedParentKey(parentId) : void 0;
18936
+ assertOwner();
18772
18937
  if (parentId && !parentKey) {
18773
18938
  throw new Error(`Cannot persist child embed ${embed.embed_id}: parent embed key ${parentId} is unavailable.`);
18774
18939
  }
18775
18940
  const embedKey = parentKey ?? await deriveEmbedKeyFromChatKey(params.chatKeyBytes, embed.embed_id);
18941
+ assertOwner();
18776
18942
  await persistOne(embed, embedKey, Boolean(parentId));
18777
- processed.add(embed.embed_id);
18778
- }
18943
+ assertOwner();
18944
+ }, (embed, dependency) => embed.parent_embed_id === dependency.embed_id);
18779
18945
  }
18780
18946
  async persistPostProcessingMetadata(params) {
18781
18947
  const encryptedFollowUps = params.followUpSuggestions.length > 0 ? await encryptWithAesGcmCombined(
@@ -23103,6 +23269,7 @@ Required: ${schema.required.join(", ")}`
23103
23269
  interactiveViewerAllowed = false;
23104
23270
  interactiveViewerGeneration = 0;
23105
23271
  beginInteractiveViewerSession() {
23272
+ this.cancelSendSocketRearm();
23106
23273
  this.interactiveAdmissionEnabled = true;
23107
23274
  this.interactiveAdmissionOwner = null;
23108
23275
  this.interactiveAdmissionAuth = null;
@@ -23110,6 +23277,7 @@ Required: ${schema.required.join(", ")}`
23110
23277
  this.preparedSendSocketAttemptOwner = null;
23111
23278
  }
23112
23279
  endInteractiveViewerSession() {
23280
+ this.cancelSendSocketRearm();
23113
23281
  this.interactiveAdmissionEnabled = false;
23114
23282
  this.interactiveAdmissionOwner = null;
23115
23283
  this.interactiveAdmissionAuth = null;
@@ -42590,6 +42758,20 @@ async function verifyCaddyCoreRoutes(apiUrl, origin) {
42590
42758
  "Access-Control-Request-Headers": "content-type"
42591
42759
  } });
42592
42760
  if (!embedPreflight.ok || embedPreflight.headers.get("access-control-allow-origin") !== origin || embedPreflight.headers.get("access-control-allow-credentials") !== "true") throw new Error("caddy_embed_reference_cors_failed");
42761
+ let accountKeyPreflight;
42762
+ try {
42763
+ accountKeyPreflight = await request("/v1/account/chat-encryption-key", { method: "OPTIONS", headers: {
42764
+ Origin: origin,
42765
+ "Access-Control-Request-Method": "GET",
42766
+ "Access-Control-Request-Headers": "content-type"
42767
+ } });
42768
+ } catch {
42769
+ throw new Error("caddy_account_chat_key_preflight_failed");
42770
+ }
42771
+ const accountKeyMethods = accountKeyPreflight.headers.get("access-control-allow-methods")?.split(",").map((value) => value.trim().toUpperCase()) ?? [];
42772
+ if (accountKeyPreflight.status !== 200 || accountKeyPreflight.headers.get("access-control-allow-origin") !== origin || accountKeyPreflight.headers.get("access-control-allow-credentials") !== "true" || !accountKeyMethods.includes("GET")) {
42773
+ throw new Error("caddy_account_chat_key_preflight_failed");
42774
+ }
42593
42775
  const availability = await request("/v1/features/availability");
42594
42776
  if (!availability.ok) throw new Error("caddy_availability_route_failed");
42595
42777
  const features = await availability.json();