openmates 0.27.0-alpha.80 → 0.27.0-alpha.82

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 {
@@ -6217,6 +6256,68 @@ function parseFocusAuthoringDocument(document) {
6217
6256
  throw new Error("invalid_focus_document");
6218
6257
  }
6219
6258
  }
6259
+ async function collectSelectedCliProjectFocusRoutingCatalog(client2, projectId, options) {
6260
+ const detail = await client2.getProject(projectId, options);
6261
+ if (detail.project.project_id !== projectId || detail.project.archived) throw new Error("Project catalog unavailable.");
6262
+ const key = await client2.decryptProjectKey(detail.project, options);
6263
+ const focuses = [];
6264
+ for (const item of detail.items) {
6265
+ if (!(["embed", "upload"].includes(item.item_type) && item.target_id_hash && !item.deleted_target_state && typeof item.encrypted_metadata === "string")) continue;
6266
+ const decrypted = await decryptWithAesGcmCombined(item.encrypted_metadata, key);
6267
+ if (!decrypted) throw new Error("Project catalog metadata unavailable.");
6268
+ const metadata = JSON.parse(decrypted);
6269
+ const path2 = metadata.display_path;
6270
+ if (typeof path2 !== "string" || !/^\.openmates\/focuses\/[a-zA-Z0-9_-]+\/SKILL\.md$/.test(path2) || typeof metadata.focus_title !== "string" || typeof metadata.focus_description !== "string" || typeof metadata.focus_when_to_use !== "string") continue;
6271
+ focuses.push({
6272
+ item_id: item.project_item_id,
6273
+ revision: createHash9("sha256").update(["updated_at", "encrypted_metadata", "encrypted_note", "target_id_hash", "deleted_target_state"].map((field3) => item[field3] ? String(item[field3]) : "").join("\0")).digest("hex"),
6274
+ title: metadata.focus_title.slice(0, 180),
6275
+ description: metadata.focus_description.slice(0, 640),
6276
+ when_to_use: metadata.focus_when_to_use.slice(0, 640)
6277
+ });
6278
+ if (focuses.length >= 20) break;
6279
+ }
6280
+ return focuses;
6281
+ }
6282
+ function registerCliProjectFocusCatalogResponder(input) {
6283
+ let active = true;
6284
+ const answered = /* @__PURE__ */ new Set();
6285
+ const stopEvent = input.ws.onMessageType("project_focus_catalog_requested", (event) => {
6286
+ if (!active || !input.ws.isOpen() || !input.isCurrent() || event.chat_id !== input.chatId || typeof event.request_id !== "string" || !event.request_id || event.request_id.length > 128 || typeof event.project_id !== "string" || !event.project_id || event.project_id.length > 128 || event.team_id !== input.teamId || !input.isOfferedProject(event.project_id) || answered.has(event.request_id)) return;
6287
+ answered.add(event.request_id);
6288
+ void (async () => {
6289
+ let focuses;
6290
+ try {
6291
+ focuses = await collectSelectedCliProjectFocusRoutingCatalog(
6292
+ input.client,
6293
+ event.project_id,
6294
+ { teamId: input.teamId, personal: !input.teamId }
6295
+ );
6296
+ } catch {
6297
+ focuses = null;
6298
+ }
6299
+ if (!active || !input.ws.isOpen() || !input.isCurrent()) return;
6300
+ await input.ws.sendAsync("project_focus_catalog_result", {
6301
+ chat_id: input.chatId,
6302
+ request_id: event.request_id,
6303
+ project_id: event.project_id,
6304
+ focuses
6305
+ });
6306
+ })().catch((error) => {
6307
+ if (!active || !input.ws.isOpen() || !input.isCurrent()) return;
6308
+ input.onDeliveryFailure(error instanceof Error ? error : new Error("Project catalog delivery failed."));
6309
+ });
6310
+ });
6311
+ const stopClose = input.ws.onClose(() => {
6312
+ active = false;
6313
+ stopEvent();
6314
+ });
6315
+ return () => {
6316
+ active = false;
6317
+ stopEvent();
6318
+ stopClose();
6319
+ };
6320
+ }
6220
6321
  async function discoverCliProjectCandidates(client2, options, requireComplete = false) {
6221
6322
  const records = (await client2.listProjects(options)).filter((record10) => !record10.archived).slice(0, 40);
6222
6323
  const entries = await Promise.all(records.map(async (record10) => {
@@ -13605,6 +13706,39 @@ var OpenMatesClient = class _OpenMatesClient {
13605
13706
  chatActivitySocket = null;
13606
13707
  preparedSendSocket = null;
13607
13708
  preparedSendSocketAttemptOwner = null;
13709
+ foregroundSendCount = 0;
13710
+ pendingSendSocketRearm = null;
13711
+ cancelSendSocketRearm() {
13712
+ const pending = this.pendingSendSocketRearm;
13713
+ this.pendingSendSocketRearm = null;
13714
+ pending?.removeClose();
13715
+ }
13716
+ maybeRearmSendSocket() {
13717
+ const pending = this.pendingSendSocketRearm;
13718
+ if (!pending || this.foregroundSendCount > 0 || pending.ws.isOpen()) return;
13719
+ this.cancelSendSocketRearm();
13720
+ 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;
13721
+ this.preparedSendSocketAttemptOwner = null;
13722
+ this.warmSendSocket();
13723
+ }
13724
+ rearmSendSocketWhenClosed(ws, teamId) {
13725
+ if (!this.interactiveViewerAllowed || !this.hasSession()) return;
13726
+ this.cancelSendSocketRearm();
13727
+ const session = this.requireSession();
13728
+ const pending = {
13729
+ ws,
13730
+ teamId,
13731
+ owner: this.preparedChatOwner(session, teamId),
13732
+ ownerKey: this.sendSocketOwnerKey(session, teamId),
13733
+ removeClose: () => {
13734
+ }
13735
+ };
13736
+ this.pendingSendSocketRearm = pending;
13737
+ pending.removeClose = ws.onClose(() => {
13738
+ this.maybeRearmSendSocket();
13739
+ });
13740
+ this.maybeRearmSendSocket();
13741
+ }
13608
13742
  sendSocketOwnerKey(session, teamId) {
13609
13743
  return JSON.stringify([
13610
13744
  this.preparedChatOwner(session, teamId),
@@ -13631,7 +13765,7 @@ var OpenMatesClient = class _OpenMatesClient {
13631
13765
  }
13632
13766
  /** Prepare one authenticated send socket for the interactive TUI's next turn. */
13633
13767
  warmSendSocket() {
13634
- if (!this.interactiveViewerAllowed || this.explicitSession || !this.hasSession()) return;
13768
+ if (!this.interactiveViewerAllowed || this.foregroundSendCount > 0 || this.explicitSession || !this.hasSession()) return;
13635
13769
  const session = this.requireSession();
13636
13770
  const teamId = this.getActiveTeamId();
13637
13771
  const owner2 = this.preparedChatOwner(session, teamId);
@@ -15562,14 +15696,32 @@ var OpenMatesClient = class _OpenMatesClient {
15562
15696
  }
15563
15697
  return { bySubject, acknowledged: /* @__PURE__ */ new Map(), visiting: /* @__PURE__ */ new Set() };
15564
15698
  }
15699
+ captureRecoveryOwner() {
15700
+ const account = this.captureTeamAccountOwner();
15701
+ const team = this.getActiveTeamId();
15702
+ const master = Buffer.from(this.getMasterKeyBytes());
15703
+ return () => {
15704
+ if (!this.hasSession() || !account.isCurrent() || this.getActiveTeamId() !== team || !master.equals(Buffer.from(this.getMasterKeyBytes()))) {
15705
+ throw new Error("Recovery account, Team or key changed; outputs remain pending.");
15706
+ }
15707
+ };
15708
+ }
15565
15709
  async persistAvailableRecoveryOutputs(ws, ownerId2, cache, outputs, teamId, dependencies = this.recoveryReplayDependencies([outputs])) {
15566
15710
  if (outputs.length > 100) throw new Error("Recovery output discovery exceeded the bounded batch size.");
15567
15711
  const order = { message: 0, embed: 1, diff: 2, summary: 3, checkpoint: 4 };
15568
15712
  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);
15713
+ const assertOwner = this.captureRecoveryOwner();
15569
15714
  const masterKey = this.getMasterKeyBytes();
15570
15715
  const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
15716
+ const http = { get: async (path2, headers) => {
15717
+ assertOwner();
15718
+ const result = await this.http.get(path2, headers);
15719
+ assertOwner();
15720
+ return result;
15721
+ } };
15571
15722
  let acknowledged = 0;
15572
15723
  const request = async (type, responseType, recordId, payload) => {
15724
+ assertOwner();
15573
15725
  const requestId = randomUUID9();
15574
15726
  const response = ws.waitForMessage(responseType, (value) => {
15575
15727
  const frame = value;
@@ -15577,10 +15729,14 @@ var OpenMatesClient = class _OpenMatesClient {
15577
15729
  }, 2e4);
15578
15730
  void response.catch(() => {
15579
15731
  });
15732
+ assertOwner();
15580
15733
  await ws.sendAsync(type, { protocol_version: 1, record_id: recordId, request_id: requestId, ...payload });
15581
- return (await response).payload;
15734
+ const received = await response;
15735
+ assertOwner();
15736
+ return received.payload;
15582
15737
  };
15583
15738
  const storeWithReceipt = async (type, responseType, embedId, payload, recoveryRecordId, version2) => {
15739
+ assertOwner();
15584
15740
  const requestId = randomUUID9();
15585
15741
  const receipt = ws.waitForMessage(
15586
15742
  responseType,
@@ -15589,8 +15745,11 @@ var OpenMatesClient = class _OpenMatesClient {
15589
15745
  );
15590
15746
  void receipt.catch(() => {
15591
15747
  });
15748
+ assertOwner();
15592
15749
  await ws.sendAsync(type, { ...payload, request_id: requestId, recovery_record_id: recoveryRecordId });
15593
- return (await receipt).payload;
15750
+ const received = await receipt;
15751
+ assertOwner();
15752
+ return received.payload;
15594
15753
  };
15595
15754
  const reconcileAcknowledgedEmbed = async (output, chatKey, sealedContent) => {
15596
15755
  if (output.output_kind !== "embed" && output.output_kind !== "diff") {
@@ -15613,7 +15772,7 @@ var OpenMatesClient = class _OpenMatesClient {
15613
15772
  throw new Error(`Recovery output ${output.record_id} lacks verified canonical acknowledgement.`);
15614
15773
  }
15615
15774
  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());
15775
+ const canonical = await http.get(embedPath, this.getCliRequestHeaders());
15617
15776
  const row3 = canonical.data?.embed;
15618
15777
  if (!canonical.ok || !row3 || row3.hashed_chat_id !== computeSHA256(output.target_chat_id) || typeof row3.encrypted_content !== "string" || typeof row3.encrypted_type !== "string") {
15619
15778
  throw new Error(`Recovery output ${output.record_id} canonical embed is unavailable.`);
@@ -15638,7 +15797,7 @@ var OpenMatesClient = class _OpenMatesClient {
15638
15797
  } else {
15639
15798
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15640
15799
  if (teamId) query.set("team_id", teamId);
15641
- const history = await this.http.get(
15800
+ const history = await http.get(
15642
15801
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15643
15802
  this.getCliRequestHeaders()
15644
15803
  );
@@ -15652,12 +15811,14 @@ var OpenMatesClient = class _OpenMatesClient {
15652
15811
  if (typeof encryptedPatch === "string") await decryptWithAesGcmCombined(encryptedPatch, embedKey);
15653
15812
  }
15654
15813
  };
15655
- for (const output of ordered) {
15814
+ const rootKeys = /* @__PURE__ */ new Map();
15815
+ const prepareOne = async (output) => {
15816
+ assertOwner();
15656
15817
  const identity3 = recoveryOutputIdentity(output);
15657
15818
  const committed = dependencies.acknowledged.get(output.record_id);
15658
15819
  if (committed !== void 0) {
15659
15820
  if (committed !== identity3) throw new Error("Acknowledged recovery dependency changed identity.");
15660
- continue;
15821
+ return null;
15661
15822
  }
15662
15823
  if (dependencies.visiting.has(output.record_id) || dependencies.visiting.size >= 32) {
15663
15824
  throw new Error("Recovery embed dependency is cyclic or exceeds the bounded depth.");
@@ -15665,26 +15826,35 @@ var OpenMatesClient = class _OpenMatesClient {
15665
15826
  if (output.root_hashed_team_id !== void 0 && output.root_hashed_team_id !== (teamId ? computeSHA256(teamId) : null)) {
15666
15827
  throw new Error(`Recovery output ${output.record_id} Team key context is unavailable; output remains pending.`);
15667
15828
  }
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.`);
15829
+ let rootKeyPromise = rootKeys.get(output.root_chat_id);
15830
+ if (!rootKeyPromise) {
15831
+ rootKeyPromise = (async () => {
15832
+ let root = cache.chats.find((chat) => String(chat.details.id ?? "") === output.root_chat_id);
15833
+ let rootKey2 = root ? await this.resolveChatKey(cache, root, wrappingKey, teamId) : null;
15834
+ if (!rootKey2) {
15835
+ const response = await http.get(
15836
+ `/v1/chats/${encodeURIComponent(output.root_chat_id)}/wrappers/window` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : ""),
15837
+ this.getCliRequestHeaders()
15838
+ );
15839
+ if (!response.ok) throw new Error(`Recovery root key lookup failed (HTTP ${response.status}); output remains pending.`);
15840
+ const keyType = teamId ? "team" : "master";
15841
+ const wrapper = (response.data.wrappers ?? []).find((item) => item.key_type === keyType && typeof item.encrypted_chat_key === "string" && item.encrypted_chat_key);
15842
+ if (!wrapper) throw new Error(`Recovery root chat ${output.root_chat_id} key wrapper is unavailable.`);
15843
+ cache.chatKeyWrappers ??= [];
15844
+ cache.chatKeyWrappers.push(wrapper);
15845
+ if (!root) {
15846
+ root = { details: { id: output.root_chat_id, encrypted_chat_key: wrapper.encrypted_chat_key }, messages: [] };
15847
+ cache.chats.push(root);
15848
+ }
15849
+ rootKey2 = await this.resolveChatKey(cache, root, wrappingKey, teamId);
15850
+ }
15851
+ if (!rootKey2) throw new Error(`Recovery root chat ${output.root_chat_id} key is unavailable.`);
15852
+ return rootKey2;
15853
+ })();
15854
+ rootKeys.set(output.root_chat_id, rootKeyPromise);
15855
+ }
15856
+ const rootKey = await rootKeyPromise;
15857
+ assertOwner();
15688
15858
  let ready2;
15689
15859
  try {
15690
15860
  ready2 = await request("recovery_output_get", "recovery_output_ready", output.record_id, {});
@@ -15693,7 +15863,7 @@ var OpenMatesClient = class _OpenMatesClient {
15693
15863
  await reconcileAcknowledgedEmbed(output, rootKey);
15694
15864
  dependencies.acknowledged.set(output.record_id, identity3);
15695
15865
  acknowledged += 1;
15696
- continue;
15866
+ return null;
15697
15867
  }
15698
15868
  for (const field3 of ["record_id", "root_chat_id", "target_chat_id", "turn_id", "subject_id", "output_kind", "output_version", "chat_key_version"]) {
15699
15869
  if (ready2[field3] !== output[field3]) throw new Error(`Recovery output ${output.record_id} identity mismatch: ${field3}.`);
@@ -15729,6 +15899,27 @@ var OpenMatesClient = class _OpenMatesClient {
15729
15899
  throw new Error("Recovery output plaintext identity mismatch.");
15730
15900
  }
15731
15901
  const content = recovered.content;
15902
+ assertOwner();
15903
+ if (typeof content.parent_embed_id === "string") {
15904
+ const candidates = (dependencies.bySubject.get(content.parent_embed_id) ?? []).filter((candidate) => candidate.output_kind === "embed");
15905
+ 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))) {
15906
+ const parentPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(content.parent_embed_id)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
15907
+ const parent = await http.get(parentPath, this.getCliRequestHeaders());
15908
+ if (!parent.ok) throw new Error("Canonical recovery parent embed key scope was unavailable.");
15909
+ }
15910
+ }
15911
+ return { output, ready: ready2, content, rootKey, identity: identity3 };
15912
+ };
15913
+ const prepared = [];
15914
+ for (const output of ordered) prepared.push(await prepareOne(output));
15915
+ const persistOne = async ({ output, ready: ready2, content, rootKey, identity: identity3 }) => {
15916
+ assertOwner();
15917
+ const committed = dependencies.acknowledged.get(output.record_id);
15918
+ if (committed !== void 0) {
15919
+ if (committed !== identity3) throw new Error("Acknowledged recovery dependency changed identity.");
15920
+ return;
15921
+ }
15922
+ const messageRole = output.message_role ?? "assistant";
15732
15923
  const childKey = rootKey;
15733
15924
  const now = Math.floor(Date.now() / 1e3);
15734
15925
  const encryptedChatKey = typeof ready2.encrypted_chat_key === "string" && ready2.encrypted_chat_key ? ready2.encrypted_chat_key : await encryptBytesWithAesGcm(childKey, wrappingKey);
@@ -15803,6 +15994,7 @@ var OpenMatesClient = class _OpenMatesClient {
15803
15994
  }, 2e4);
15804
15995
  void stored.catch(() => {
15805
15996
  });
15997
+ assertOwner();
15806
15998
  await ws.sendAsync("store_chat_compression_checkpoint", {
15807
15999
  chat_id: output.target_chat_id,
15808
16000
  checkpoint_id: output.subject_id,
@@ -15830,7 +16022,7 @@ var OpenMatesClient = class _OpenMatesClient {
15830
16022
  } else {
15831
16023
  if (content.embed_id !== output.subject_id) throw new Error("Recovery embed identity mismatch.");
15832
16024
  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(
16025
+ let canonicalBefore = await http.get(
15834
16026
  embedPath,
15835
16027
  this.getCliRequestHeaders()
15836
16028
  );
@@ -15929,7 +16121,7 @@ var OpenMatesClient = class _OpenMatesClient {
15929
16121
  }
15930
16122
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15931
16123
  if (teamId) query.set("team_id", teamId);
15932
- const existing = await this.http.get(
16124
+ const existing = await http.get(
15933
16125
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15934
16126
  this.getCliRequestHeaders()
15935
16127
  );
@@ -15985,7 +16177,7 @@ var OpenMatesClient = class _OpenMatesClient {
15985
16177
  } else {
15986
16178
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
15987
16179
  if (teamId) query.set("team_id", teamId);
15988
- const history = await this.http.get(
16180
+ const history = await http.get(
15989
16181
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
15990
16182
  this.getCliRequestHeaders()
15991
16183
  );
@@ -16106,7 +16298,7 @@ var OpenMatesClient = class _OpenMatesClient {
16106
16298
  }
16107
16299
  const keyPath = `/v1/embeds/chats/${encodeURIComponent(output.target_chat_id)}/embeds/${encodeURIComponent(keySubject)}` + (teamId ? `?team_id=${encodeURIComponent(teamId)}` : "");
16108
16300
  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(
16301
+ const keyPage = bootstrapRootKeys ? null : keySubject === output.subject_id && canonicalBefore.ok ? canonicalBefore : parentPage ?? await http.get(
16110
16302
  keyPath,
16111
16303
  this.getCliRequestHeaders()
16112
16304
  );
@@ -16133,20 +16325,24 @@ var OpenMatesClient = class _OpenMatesClient {
16133
16325
  }
16134
16326
  ].filter((key) => !existingTypes.has(key.key_type));
16135
16327
  if (keys2.length > 0) {
16328
+ assertOwner();
16136
16329
  const requestId = randomUUID9();
16137
16330
  const receipt = ws.waitForMessage("store_embed_keys_confirmed", (value) => value.request_id === requestId, 3e4);
16138
16331
  void receipt.catch(() => {
16139
16332
  });
16333
+ assertOwner();
16140
16334
  await ws.sendAsync("store_embed_keys", {
16141
16335
  request_id: requestId,
16142
16336
  recovery_record_id: output.record_id,
16143
16337
  keys: keys2
16144
16338
  });
16145
- const result = (await receipt).payload;
16339
+ const received = await receipt;
16340
+ assertOwner();
16341
+ const result = received.payload;
16146
16342
  if (result.failed_count !== 0 || result.created_count !== keys2.length || result.requested_count !== keys2.length) {
16147
16343
  throw new Error("Recovery embed key wrappers were not exactly confirmed.");
16148
16344
  }
16149
- const durable = await this.http.get(
16345
+ const durable = await http.get(
16150
16346
  keyPath,
16151
16347
  this.getCliRequestHeaders()
16152
16348
  );
@@ -16156,7 +16352,7 @@ var OpenMatesClient = class _OpenMatesClient {
16156
16352
  throw new Error("Recovery embed key wrappers are not both durable.");
16157
16353
  }
16158
16354
  }
16159
- const canonicalAfter = await this.http.get(
16355
+ const canonicalAfter = await http.get(
16160
16356
  embedPath,
16161
16357
  this.getCliRequestHeaders()
16162
16358
  );
@@ -16171,7 +16367,7 @@ var OpenMatesClient = class _OpenMatesClient {
16171
16367
  } else {
16172
16368
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: output.target_chat_id });
16173
16369
  if (teamId) query.set("team_id", teamId);
16174
- const history = await this.http.get(
16370
+ const history = await http.get(
16175
16371
  `/v1/embeds/${encodeURIComponent(output.subject_id)}/versions/${output.output_version}?${query}`,
16176
16372
  this.getCliRequestHeaders()
16177
16373
  );
@@ -16195,7 +16391,14 @@ var OpenMatesClient = class _OpenMatesClient {
16195
16391
  }
16196
16392
  }
16197
16393
  dependencies.acknowledged.set(output.record_id, identity3);
16394
+ assertOwner();
16198
16395
  acknowledged += 1;
16396
+ };
16397
+ for (const kind of ["message", "embed", "diff", "summary", "checkpoint"]) {
16398
+ const group = prepared.filter((item) => item !== null && item.output.output_kind === kind);
16399
+ if (kind === "embed" || kind === "diff") {
16400
+ 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));
16401
+ } else for (const item of group) await persistOne(item);
16199
16402
  }
16200
16403
  return acknowledged;
16201
16404
  }
@@ -16321,1651 +16524,1577 @@ var OpenMatesClient = class _OpenMatesClient {
16321
16524
  }
16322
16525
  }
16323
16526
  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(
16527
+ this.cancelSendSocketRearm();
16528
+ this.foregroundSendCount++;
16529
+ try {
16530
+ const preWsStartedAt = performance.now();
16531
+ const traceSendTiming = (phase) => {
16532
+ if (process.env.OPENMATES_TRACE_SEND_TIMING === "1") {
16533
+ console.error(`[chat send timing] phase=${phase} elapsed_ms=${Math.round(performance.now() - preWsStartedAt)}`);
16534
+ }
16535
+ };
16536
+ traceSendTiming("send_begin");
16537
+ if (this.preparedSendSocket && !this.preparedSendSocket.opened) this.closePreparedSendSocket();
16538
+ const teamId = this.resolveTeamContext({ teamId: params.teamId, personal: params.personal });
16539
+ const teamSenderName = teamId ? await this.ownTeamSenderName(teamId) : "User";
16540
+ if (params.chatId && params.newChatId) {
16541
+ throw new Error("chatId and newChatId cannot be used together.");
16542
+ }
16543
+ if (params.newChatId && !CANONICAL_UUID_PATTERN.test(params.newChatId)) {
16544
+ throw new Error("newChatId must be a canonical UUID.");
16545
+ }
16546
+ if (params.incognito && params.projectId) {
16547
+ throw new Error("Project file execution requires a saved chat.");
16548
+ }
16549
+ const ownsPreparedId = Boolean(params.newChatId && this.preparedChatKey?.chatId === params.newChatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId));
16550
+ const sdkMintedChatId = params.chatId === void 0 && params.projectId === void 0 && (params.newChatId === void 0 || ownsPreparedId);
16551
+ let chatId;
16552
+ if (params.newChatId) {
16553
+ chatId = params.newChatId;
16554
+ } else if (!params.chatId) {
16555
+ chatId = randomUUID9();
16556
+ } else if (!CANONICAL_UUID_PATTERN.test(params.chatId)) {
16557
+ const resolved = await this.resolveFullChatId(params.chatId, { teamId });
16558
+ if (!resolved) {
16559
+ throw new Error(
16560
+ `Chat not found for '${params.chatId}'. Use a slug, full UUID, or the first 8 characters of an existing chat ID.`
16561
+ );
16562
+ }
16563
+ chatId = resolved;
16564
+ } else {
16565
+ chatId = params.chatId;
16566
+ }
16567
+ const historyAbort = new AbortController();
16568
+ let savedPrivacyHistoryRead = null;
16569
+ let historySnapshotOwner = null;
16570
+ const readSavedPrivacyHistory = () => {
16571
+ if (!savedPrivacyHistoryRead) {
16572
+ savedPrivacyHistoryRead = withHttpAbortSignal(historyAbort.signal, () => this.getChatMessages(
16573
+ chatId,
16574
+ { personal: !teamId, teamId, freshChatOnly: true, signal: historyAbort.signal }
16575
+ )).then((history) => {
16576
+ historySnapshotOwner = this.preparedChatOwner(this.requireSession(), teamId);
16577
+ return history;
16578
+ });
16579
+ void savedPrivacyHistoryRead.catch(() => {
16580
+ });
16581
+ }
16582
+ return savedPrivacyHistoryRead;
16583
+ };
16584
+ const joinSavedPrivacyHistory = async () => {
16585
+ const history = await readSavedPrivacyHistory();
16586
+ if (!historySnapshotOwner || !this.preparedChatOwnerCurrent(historySnapshotOwner, teamId)) {
16587
+ throw new Error("Chat workspace changed during history preparation.");
16588
+ }
16589
+ return history;
16590
+ };
16591
+ try {
16592
+ let finalMessage = params.message;
16593
+ if (hasRememberMessageReference(finalMessage)) {
16594
+ const rememberedHistory = (params.messageHistory ?? []).map((message) => ({ id: message.message_id, content: message.content })).filter((message) => Boolean(message.id) && typeof message.content === "string");
16595
+ finalMessage = rewriteRememberMessageReferences(finalMessage, rememberedHistory);
16596
+ }
16597
+ if (params.chatId && !params.incognito && hasRememberMessageReference(finalMessage)) {
16598
+ const { messages } = await joinSavedPrivacyHistory();
16599
+ finalMessage = rewriteRememberMessageReferences(finalMessage, messages);
16600
+ }
16601
+ const shouldWaitForAi = shouldWaitForTeamAi(finalMessage, teamId);
16602
+ const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
16603
+ const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
16604
+ const sendSocketOptions = {
16605
+ taskUpdateJobs: taskUpdateJobsEnabled,
16606
+ projectFileJobs: !params.incognito,
16607
+ remoteCommandJobs: !params.incognito,
16608
+ interactiveHuman: params.interactiveHuman === true && this.interactiveViewerAllowed && process.stdin.isTTY === true && process.stdout.isTTY === true
16609
+ };
16610
+ const independentKeyOwner = this.preparedChatOwner(this.requireSession(), teamId);
16611
+ const independentNewChatKey = !params.incognito && !params.chatId ? (async () => {
16612
+ if (ownsPreparedId && this.preparedChatKey?.chatId === chatId) {
16613
+ const prepared = await this.preparedChatKey.value;
16614
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(prepared.wrappingKey)).digest("hex");
16615
+ return prepared;
16616
+ }
16617
+ const wrappingKey = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
16618
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(wrappingKey)).digest("hex");
16619
+ const key = new Uint8Array(randomBytes4(32));
16620
+ return { key, wrappingKey, encryptedKey: await encryptBytesWithAesGcm(key, wrappingKey) };
16621
+ })() : null;
16622
+ void independentNewChatKey?.catch(() => {
16623
+ });
16624
+ const independentRecoveryKeypair = independentNewChatKey?.then((prepared) => deriveChatCompletionRecoveryKeypair(
16625
+ Buffer.from(prepared.key).toString("base64url"),
16367
16626
  chatId,
16368
- { personal: !teamId, teamId, freshChatOnly: true, signal: historyAbort.signal }
16369
- )).then((history) => {
16370
- historySnapshotOwner = this.preparedChatOwner(this.requireSession(), teamId);
16371
- return history;
16627
+ 1
16628
+ ));
16629
+ void independentRecoveryKeypair?.catch(() => {
16372
16630
  });
16373
- void savedPrivacyHistoryRead.catch(() => {
16631
+ const privacyTurnId = params.incognito ? null : randomUUID9();
16632
+ let conditionalPrivacyReceipt = null;
16633
+ let compiledPrivacy;
16634
+ let memorySocket = null;
16635
+ let socketSnapshotAttempted = false;
16636
+ let socketSnapshotResultRows = null;
16637
+ let socketProjectCandidateRows = null;
16638
+ let verifiedProjectCandidates = null;
16639
+ let deferredProjectDirectory = false;
16640
+ let usePersonalRoutingDirectory = false;
16641
+ let projectCandidatesReadyFromSendStartMs = null;
16642
+ let socketMemoryReceipt = null;
16643
+ const socketMemoryOptionsKey = () => JSON.stringify([
16644
+ this.sendSocketOptionsKey(sendSocketOptions),
16645
+ params.projectId ?? null,
16646
+ params.teamId ?? null,
16647
+ params.personal === true,
16648
+ params.incognito === true,
16649
+ params.autoApproveMemories === true,
16650
+ params.piiDetection !== false,
16651
+ params.learningMode?.enabled ?? null,
16652
+ params.learningMode?.ageGroup ?? null
16653
+ ]);
16654
+ if (!params.incognito && params.chatId) readSavedPrivacyHistory();
16655
+ const followupDiscoveryOwner = !params.incognito && params.chatId && !teamId && !params.projectId ? this.preparedChatOwner(this.requireSession(), teamId) : null;
16656
+ const followupDiscoveryRead = followupDiscoveryOwner ? withHttpAbortSignal(historyAbort.signal, async () => prefetchCliJevDiscovery(this, chatId, { personal: true, teamId })) : null;
16657
+ void followupDiscoveryRead?.catch(() => {
16374
16658
  });
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);
16659
+ const memoryStartedAt = performance.now();
16660
+ let availableMemories = params.memorySnapshot ?? [];
16661
+ let memoryCountsLoaded = false;
16662
+ let memoryMetadataKeys = [];
16663
+ if (!params.incognito) {
16664
+ try {
16665
+ if (!teamId) {
16666
+ try {
16667
+ socketSnapshotAttempted = true;
16668
+ memorySocket = (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient(sendSocketOptions);
16669
+ const ownerKey = JSON.stringify([
16670
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16671
+ authCookieFingerprint(this.http.getCookieMap())
16672
+ ]);
16673
+ const conditionalSupported = await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v2");
16674
+ const combinedSupported = conditionalSupported || await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v1");
16675
+ const supported = combinedSupported || await memorySocket.ws.waitForServerCapability("personal_memory_snapshot_v1");
16676
+ if (supported) {
16677
+ 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");
16678
+ deferredProjectDirectory = deferProjectDirectory;
16679
+ usePersonalRoutingDirectory = deferProjectDirectory && memorySocket.ws.supportsServerCapability("personal_project_routing_directory_v1");
16680
+ traceSendTiming("snapshot_request");
16681
+ const privacyOwnerKey = this.preparedPrivacyOwnerKey(null);
16682
+ const preferencesKey = JSON.stringify(await readPrivacyPreferences());
16683
+ const preparedBundle = conditionalSupported ? this.preparedPrivacy.get(privacyOwnerKey, preferencesKey) : null;
16684
+ const conditional = conditionalSupported ? await memorySocket.ws.requestPersonalSendContextSnapshotV2({
16685
+ request_id: randomUUID9(),
16686
+ purpose: "send",
16687
+ turn_id: privacyTurnId,
16688
+ ...preparedBundle ? { known_generation: preparedBundle.generation } : {}
16689
+ }) : null;
16690
+ const combined = !conditional && combinedSupported && !deferProjectDirectory ? await memorySocket.ws.requestPersonalSendContextSnapshot(randomUUID9()) : null;
16691
+ const snapshot2 = conditional ? conditional.personal_memories ?? null : combined?.personal_memories ?? await memorySocket.ws.requestPersonalMemorySnapshot(randomUUID9());
16692
+ traceSendTiming("snapshot_ready");
16693
+ if (conditional || combined) {
16694
+ socketProjectCandidateRows = (conditional ?? combined).project_candidates;
16695
+ verifiedProjectCandidates = await decodePersonalProjectCandidates(this, (conditional ?? combined).project_candidates);
16696
+ projectCandidatesReadyFromSendStartMs = Math.round(performance.now() - preWsStartedAt);
16697
+ }
16698
+ if (conditional?.unchanged) {
16699
+ if (!preparedBundle || conditional.privacy_generation !== preparedBundle.generation) throw new Error("Unchanged privacy snapshot has no matching prepared bundle.");
16700
+ availableMemories = preparedBundle.memories;
16701
+ compiledPrivacy = preparedBundle.privacy;
16702
+ socketSnapshotResultRows = [];
16703
+ } else {
16704
+ if (!snapshot2) throw new Error("Full Personal privacy snapshot is required.");
16705
+ socketSnapshotResultRows = snapshot2.memories;
16706
+ availableMemories = await this.listMemoriesUnshared({ personal: true }, snapshot2);
16707
+ if (conditional) {
16708
+ this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16709
+ compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16710
+ }
16711
+ }
16501
16712
  if (conditional) {
16502
- this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16503
- compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16713
+ if (!conditional.receipt_id || conditional.turn_id !== privacyTurnId) throw new Error("Conditional privacy snapshot has no valid send receipt.");
16714
+ conditionalPrivacyReceipt = {
16715
+ receiptId: conditional.receipt_id,
16716
+ generation: conditional.privacy_generation,
16717
+ preferencesKey,
16718
+ epoch: this.preparedPrivacyEpoch
16719
+ };
16504
16720
  }
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
16721
+ traceSendTiming("memory_decrypt_ready");
16722
+ if (!memorySocket.ws.isOpen() || JSON.stringify([
16723
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16724
+ authCookieFingerprint(this.http.getCookieMap())
16725
+ ]) !== ownerKey) {
16726
+ throw new Error("Personal Memory snapshot owner or socket changed during preparation.");
16727
+ }
16728
+ socketMemoryReceipt = {
16729
+ ws: memorySocket.ws,
16730
+ snapshot: availableMemories,
16731
+ resultRows: socketSnapshotResultRows,
16732
+ projectRows: socketProjectCandidateRows,
16733
+ candidates: verifiedProjectCandidates,
16734
+ ownerKey,
16735
+ optionsKey: socketMemoryOptionsKey(),
16736
+ used: false
16513
16737
  };
16738
+ } else {
16739
+ memorySocket.ws.close();
16740
+ memorySocket = null;
16741
+ socketSnapshotAttempted = false;
16514
16742
  }
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();
16743
+ } catch (error) {
16744
+ memorySocket?.ws.close();
16534
16745
  memorySocket = null;
16535
- socketSnapshotAttempted = false;
16746
+ throw error;
16536
16747
  }
16537
- } catch (error) {
16538
- memorySocket?.ws.close();
16539
- memorySocket = null;
16540
- throw error;
16541
16748
  }
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}`);
16749
+ if (!socketMemoryReceipt) availableMemories = teamId && params.memorySnapshot ? params.memorySnapshot : await this.listMemoriesUnshared({ teamId }, void 0, !teamId);
16750
+ memoryCountsLoaded = true;
16751
+ memoryMetadataKeys = [
16752
+ ...new Set(
16753
+ availableMemories.filter((memory) => memory.app_id && memory.item_type).map((memory) => `${memory.app_id}-${memory.item_type}`)
16754
+ )
16755
+ ];
16756
+ } catch (error) {
16757
+ if (socketSnapshotAttempted || !teamId) throw error;
16758
+ if (params.autoApproveMemories) {
16759
+ const message = error instanceof Error ? error.message : String(error);
16760
+ throw new Error(`Failed to load memories for auto-approval: ${message}`);
16761
+ }
16555
16762
  }
16556
16763
  }
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
16764
  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;
16765
+ const snapshotAuthHandoff = this.personalMemoryAuthHandoff?.snapshot.deref() === availableMemories ? this.personalMemoryAuthHandoff.auth : null;
16766
+ this.personalMemoryAuthHandoff = null;
16767
+ this.pendingWhoAmIAuthHandoff = null;
16768
+ const personalSendAuthHandoff = !teamId && !params.incognito ? snapshotAuthHandoff : null;
16769
+ const memoryPrepMs = Math.round(performance.now() - memoryStartedAt);
16770
+ const privacyStartedAt = performance.now();
16771
+ traceSendTiming("privacy_begin");
16772
+ const savedPrivacyHistory = !params.incognito && params.chatId ? await joinSavedPrivacyHistory() : null;
16773
+ let piiMappings = [...savedPrivacyHistory?.messages.flatMap((message) => (message.piiMappings ?? []).map((mapping) => ({ ...mapping, type: mapping.type ?? "OTHER" }))) ?? [], ...params.piiMappings ?? []];
16774
+ try {
16775
+ if (params.piiDetection !== false) {
16776
+ const prepared = await prepareCliMessagePrivacy(finalMessage, availableMemories, piiMappings, { kind: "message" }, params.onPrivacyProgress, compiledPrivacy);
16777
+ finalMessage = prepared.message;
16778
+ piiMappings = prepared.mappings;
16779
+ for (const embed of params.preparedEmbeds ?? []) {
16780
+ if (!["code-code", "docs-doc", "sheets-sheet"].includes(embed.type)) continue;
16781
+ const decoded = toonDecode(embed.content);
16782
+ for (const field3 of ["code", "html", "table", "text"]) if (typeof decoded[field3] === "string") {
16783
+ const value = await prepareCliMessagePrivacy(decoded[field3], availableMemories, piiMappings, { kind: "document" }, params.onPrivacyProgress, compiledPrivacy);
16784
+ decoded[field3] = value.message;
16785
+ piiMappings = value.mappings;
16786
+ }
16787
+ embed.content = toonEncodeContent(decoded);
16788
+ const content = decoded.code ?? decoded.table ?? decoded.html ?? decoded.text;
16789
+ if (typeof content === "string") {
16790
+ embed.contentHash = createHash14("sha256").update(content).digest("hex");
16791
+ embed.textLengthChars = content.length;
16792
+ }
16586
16793
  }
16587
16794
  }
16795
+ params.onPrivacyPrepared?.(finalMessage);
16796
+ } catch (error) {
16797
+ memorySocket?.ws.close();
16798
+ throw error;
16588
16799
  }
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
- }
16800
+ const privacyPrepMs = Math.round(performance.now() - privacyStartedAt);
16801
+ traceSendTiming("privacy_ready");
16802
+ const preWsMs = Math.round(performance.now() - preWsStartedAt);
16803
+ const contextSession = this.requireSession();
16804
+ const {
16805
+ apiUrl: contextApiUrl,
16806
+ hashedEmail: contextHashedEmail,
16807
+ sessionId: contextSessionId,
16808
+ createdAt: contextSessionCreatedAt,
16809
+ masterKeyExportedB64: contextMasterKey,
16810
+ activeTeamId: contextActiveTeamId
16811
+ } = contextSession;
16812
+ const contextScope = { teamId, personal: !teamId };
16813
+ const suppliedContext = deferredProjectDirectory ? { ...params.jevContext, project_focus_candidates: void 0 } : params.jevContext;
16814
+ const contextStartedAt = Date.now();
16815
+ let publishRouting;
16816
+ const routingContext = new Promise((resolve20) => {
16817
+ publishRouting = resolve20;
16818
+ });
16819
+ let publishProjectRouting;
16820
+ let rejectProjectRouting;
16821
+ const projectRoutingContext = new Promise((resolve20, reject) => {
16822
+ publishProjectRouting = resolve20;
16823
+ rejectProjectRouting = reject;
16665
16824
  });
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.");
16825
+ void projectRoutingContext.catch(() => {
16826
+ });
16827
+ const catalogEligibleProjectIds = /* @__PURE__ */ new Set();
16828
+ const publishProjectRoutingTimed = (context) => {
16829
+ projectCandidatesReadyFromSendStartMs ??= Math.round(performance.now() - preWsStartedAt);
16830
+ const offered = context.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) ?? [];
16831
+ for (const candidate of offered) catalogEligibleProjectIds.add(candidate.project_id);
16832
+ publishProjectRouting(deferredProjectDirectory ? { ...context, project_focus_candidates: offered } : context);
16833
+ };
16834
+ let recoveryMs = 0;
16835
+ const contextOwnerIsCurrent = () => {
16836
+ const current2 = this.session;
16837
+ return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
16838
+ };
16839
+ const earlyContextAbort = new AbortController();
16840
+ const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16841
+ sdkMintedChatId,
16842
+ onRouting: publishRouting,
16843
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16844
+ usePersonalRoutingDirectory,
16845
+ onProjectRouting: publishProjectRoutingTimed,
16846
+ onProjectRoutingError: rejectProjectRouting
16847
+ })).then(
16848
+ (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
16849
+ (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
16850
+ ) : null;
16851
+ const authSocketStartedAt = Date.now();
16852
+ let opened;
16853
+ try {
16854
+ opened = memorySocket ?? (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient({ ...sendSocketOptions, personalSendAuthHandoff });
16855
+ } catch (error) {
16856
+ earlyContextAbort.abort();
16857
+ if (earlyContext) await earlyContext;
16858
+ throw error;
16668
16859
  }
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 {
16860
+ const { ws, ownerId: ownerId2 } = opened;
16861
+ let catalogTurnId = null;
16862
+ let rejectCatalogDelivery;
16863
+ const catalogDeliveryFailure = new Promise((_resolve, reject) => {
16864
+ rejectCatalogDelivery = reject;
16865
+ });
16866
+ void catalogDeliveryFailure.catch(() => {
16867
+ });
16868
+ const stopProjectCatalogResponder = !params.incognito ? registerCliProjectFocusCatalogResponder({
16869
+ ws,
16870
+ client: this,
16871
+ chatId,
16872
+ teamId: teamId ?? null,
16873
+ isCurrent: () => contextOwnerIsCurrent() && !!catalogTurnId && this.activeSubmittedChatTurns.get(chatId) === catalogTurnId,
16874
+ isOfferedProject: (projectId) => catalogEligibleProjectIds.has(projectId),
16875
+ onDeliveryFailure: (error) => rejectCatalogDelivery(error)
16876
+ }) : () => {
16877
+ };
16878
+ const consumeSocketMemoryReceipt = async () => {
16879
+ if (!socketMemoryReceipt) return;
16880
+ const receipt = socketMemoryReceipt;
16881
+ if (receipt.used || receipt.ws !== ws || receipt.snapshot !== availableMemories || receipt.resultRows !== socketSnapshotResultRows || receipt.projectRows !== socketProjectCandidateRows || receipt.candidates !== verifiedProjectCandidates || !ws.isOpen() || receipt.optionsKey !== socketMemoryOptionsKey()) {
16882
+ throw new Error("Personal Memory snapshot socket receipt is invalid.");
16704
16883
  }
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;
16884
+ await this.withFreshStoredSession(async () => {
16885
+ if (!ws.isOpen() || JSON.stringify([
16886
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16887
+ authCookieFingerprint(this.http.getCookieMap())
16888
+ ]) !== receipt.ownerKey) {
16889
+ throw new Error("Personal Memory snapshot owner changed before send.");
16713
16890
  }
16714
- );
16715
- void recoveryDiscovery.catch(() => {
16716
16891
  });
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);
16892
+ if (conditionalPrivacyReceipt && (conditionalPrivacyReceipt.epoch !== this.preparedPrivacyEpoch || conditionalPrivacyReceipt.preferencesKey !== JSON.stringify(await readPrivacyPreferences()))) {
16893
+ throw new Error("Privacy preferences changed before send; prepare the turn again.");
16894
+ }
16895
+ receipt.used = true;
16896
+ };
16897
+ let metadataTransportOwned = false;
16898
+ let metadataHandoffReady = false;
16899
+ let metadataHandoffSuggestion = null;
16900
+ let initialHeaderWork = null;
16901
+ let initialHeaderNotice = null;
16902
+ let initialAdmissionDurable = false;
16903
+ let initialHeaderStopped = false;
16904
+ let initialTitleScheduled;
16905
+ let initialIconScheduled;
16906
+ let persistOptionalHeader;
16907
+ let optionalMetadata = null;
16908
+ const socketReadyForSendAt = performance.now();
16909
+ let capabilitiesWaitMs = 0;
16910
+ let keyWaitMs = 0;
16911
+ let recoveryKeypairMs = 0;
16912
+ let encryptPayloadMs = 0;
16913
+ if (!params.incognito && !ownerId2) {
16914
+ earlyContextAbort.abort();
16915
+ ws.close();
16916
+ if (earlyContext) await earlyContext;
16917
+ throw new Error("Authenticated user identity is required for saved chat recovery.");
16731
16918
  }
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)
16919
+ const authSocketMs = Date.now() - authSocketStartedAt;
16920
+ const mayDeferRecovery = !params.incognito && sdkMintedChatId && !teamId && !params.projectId && shouldWaitForAi;
16921
+ let recoveryDiscovery = null;
16922
+ let recoveryDiscoveryError = null;
16923
+ let contextAckRejection = null;
16924
+ let removeContextAckWatcher = null;
16925
+ try {
16926
+ if (!params.incognito) {
16927
+ try {
16928
+ ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
16929
+ } catch {
16930
+ }
16931
+ const recoveryStartedAt = Date.now();
16932
+ recoveryDiscovery = ws.waitForRecoveryOutputDiscovery().then(
16933
+ () => {
16934
+ recoveryMs = Date.now() - recoveryStartedAt;
16935
+ },
16936
+ (error) => {
16937
+ recoveryDiscoveryError = error;
16938
+ throw error;
16939
+ }
16780
16940
  );
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 {
16941
+ void recoveryDiscovery.catch(() => {
16942
+ });
16943
+ if (mayDeferRecovery) {
16944
+ const capabilitiesWaitStartedAt = performance.now();
16945
+ await Promise.race([ws.waitForServerCapabilities(), recoveryDiscovery]);
16946
+ capabilitiesWaitMs = Math.round(performance.now() - capabilitiesWaitStartedAt);
16947
+ } else await recoveryDiscovery;
16948
+ }
16949
+ const messageId2 = randomUUID9();
16950
+ const wallCreatedAt = Math.floor(Date.now() / 1e3);
16951
+ const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
16952
+ const isNewChat = !params.chatId;
16953
+ let messageHistoryForRequest = params.messageHistory;
16954
+ if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
16955
+ const { messages } = savedPrivacyHistory;
16956
+ messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
16957
+ }
16958
+ ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
16959
+ const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
16960
+ params.connectedAccountDirectory
16961
+ );
16962
+ const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
16963
+ chatId,
16964
+ messageId: messageId2,
16965
+ refs: params.connectedAccountTokenRefInputs,
16966
+ teamId
16967
+ }) : [];
16968
+ assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
16969
+ let chatKeyBytes = null;
16970
+ let activeFocusId = null;
16971
+ let encryptedChatKey = null;
16972
+ let chatSlugLookupKey = null;
16973
+ let focusPhaseState = {};
16974
+ let baselineMessagesV = 0;
16975
+ let notificationTitle = null;
16976
+ let terminalExpectedMessagesV = 1;
16977
+ let savedTurnId = null;
16978
+ if (!params.incognito) {
16979
+ traceSendTiming("key_begin");
16980
+ const masterKey = this.getMasterKeyBytes();
16981
+ const preparedKey = isNewChat && ownsPreparedId && this.preparedChatKey?.chatId === chatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId) ? this.preparedChatKey : null;
16982
+ if (preparedKey) this.preparedChatKey = null;
16983
+ const keyWaitStartedAt = performance.now();
16984
+ const prepared = independentNewChatKey ? await independentNewChatKey : preparedKey ? await preparedKey.value : null;
16985
+ if (prepared && !this.preparedChatOwnerCurrent(independentKeyOwner, teamId)) throw new Error("Chat key preparation owner changed before send.");
16986
+ const wrappingKey = prepared?.wrappingKey ?? await this.getChatWrappingKey(teamId, masterKey);
16987
+ keyWaitMs = Math.round(performance.now() - keyWaitStartedAt);
16988
+ chatSlugLookupKey = wrappingKey;
16989
+ if (isNewChat && prepared) {
16990
+ chatKeyBytes = prepared.key;
16991
+ encryptedChatKey = prepared.encryptedKey;
16992
+ } else if (isNewChat) {
16993
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
16994
+ (await import("crypto")).webcrypto.getRandomValues(
16995
+ new Uint8Array(32)
16996
+ )
16997
+ );
16998
+ encryptedChatKey = await encryptBytesWithAesGcm(
16999
+ chatKeyBytes,
17000
+ wrappingKey
17001
+ );
17002
+ } else {
17003
+ const cache = (savedPrivacyHistory && this.scopedChatHistory.get(savedPrivacyHistory)) ?? loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
17004
+ const chat = cache.chats.find(
17005
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
17006
+ );
17007
+ if (chat) {
17008
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
17009
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
17010
+ if (encKey) {
17011
+ chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
17012
+ encryptedChatKey = encKey;
17013
+ if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
17014
+ if (typeof chat.details.encrypted_title === "string") {
17015
+ try {
17016
+ notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
17017
+ } catch {
17018
+ }
16792
17019
  }
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.");
17020
+ const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
17021
+ if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
17022
+ const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
17023
+ if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
17024
+ focusPhaseState = JSON.parse(phaseText);
17025
+ }
17026
+ const encryptedFocusId = chat.details.encrypted_active_focus_id;
17027
+ if (typeof encryptedFocusId === "string" && encryptedFocusId) {
17028
+ activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
17029
+ if (!activeFocusId) {
17030
+ throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
17031
+ }
16805
17032
  }
16806
17033
  }
16807
17034
  }
17035
+ if (!chatKeyBytes || !encryptedChatKey) {
17036
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
17037
+ }
16808
17038
  }
16809
- if (!chatKeyBytes || !encryptedChatKey) {
16810
- throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
16811
- }
17039
+ traceSendTiming("key_ready");
16812
17040
  }
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;
16840
- }
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;
17041
+ let focusPersistence = Promise.resolve();
17042
+ let focusPersistenceError = null;
17043
+ const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
17044
+ ws,
17045
+ chatId,
17046
+ chatKey: chatKeyBytes,
17047
+ existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
17048
+ previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
17049
+ onApplied: params.onChatContextApplied
17050
+ }) : null;
17051
+ let projectAuthoringAvailable = null;
17052
+ if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
17053
+ if (!value || typeof value !== "object") return;
17054
+ const frame = value;
17055
+ if (frame.chat_id === chatId && frame.user_message_id === messageId2 && typeof frame.project_id === "string")
17056
+ projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId2 };
16855
17057
  });
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];
17058
+ const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
17059
+ client: this,
17060
+ ws,
17061
+ chatId,
17062
+ teamId,
17063
+ onPending: params.onProjectFocusPending,
17064
+ onError: (error) => {
17065
+ focusPersistenceError = error;
16867
17066
  }
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", {
17067
+ }) : null;
17068
+ ws.onMessageType("focus_mode_activated", (event) => {
17069
+ if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
17070
+ const focusId = event.focus_id;
17071
+ if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
17072
+ const key = chatKeyBytes;
17073
+ focusPersistence = focusPersistence.then(async () => {
17074
+ const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
17075
+ await ws.sendAsync("update_encrypted_active_focus_id", {
16887
17076
  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
- }
17077
+ encrypted_active_focus_id: encryptedFocusId
16895
17078
  });
16896
- }
16897
- clearSyncCache(teamId);
16898
- }).catch((error) => {
16899
- focusPersistenceError = error;
17079
+ }).catch((error) => {
17080
+ focusPersistenceError = error;
17081
+ });
16900
17082
  });
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
17083
+ ws.onMessageType("focus_phases_updated", (event) => {
17084
+ if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
17085
+ const key = chatKeyBytes;
17086
+ const states2 = event.states;
17087
+ focusPersistence = focusPersistence.then(async () => {
17088
+ for (const [focusId, phase] of Object.entries(states2)) {
17089
+ 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) {
17090
+ throw new Error("Invalid focus phase state received");
17091
+ }
17092
+ if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
17093
+ }
17094
+ focusPhaseState = states2;
17095
+ const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
17096
+ const persisted = ws.waitForMessage(
17097
+ "encrypted_metadata_stored",
17098
+ (payload) => payload.chat_id === chatId,
17099
+ 2e4
17100
+ );
17101
+ await ws.sendAsync("encrypted_chat_metadata", {
17102
+ chat_id: chatId,
17103
+ ...teamId ? { team_id: teamId } : {},
17104
+ encrypted_focus_phase_state: encryptedState
17105
+ });
17106
+ await persisted;
17107
+ const eventIds = /* @__PURE__ */ new Set();
17108
+ for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
17109
+ if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
17110
+ eventIds.add(transition.event_id);
17111
+ const content = JSON.stringify(transition);
17112
+ await ws.sendAsync("chat_system_message_added", {
17113
+ chat_id: chatId,
17114
+ message: {
17115
+ message_id: transition.event_id,
17116
+ role: "system",
17117
+ status: "synced",
17118
+ encrypted_content: await encryptWithAesGcmCombined(content, key),
17119
+ created_at: transition.created_at
17120
+ }
17121
+ });
17122
+ }
17123
+ clearSyncCache(teamId);
17124
+ }).catch((error) => {
17125
+ focusPersistenceError = error;
17126
+ });
16914
17127
  });
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;
17128
+ const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
17129
+ if (!params.incognito && chatKeyBytes) {
17130
+ registerCliProjectFileExecutor({
17131
+ client: this,
17132
+ ws,
17133
+ chatId,
17134
+ chatKey: chatKeyBytes,
17135
+ piiMappings,
17136
+ memories: availableMemories,
17137
+ requestApproval: params.onProjectWriteApproval,
17138
+ requestReadApproval: params.onProjectReadApproval,
17139
+ onHostedVersionCommitted: params.onHostedVersionCommitted
17140
+ });
17141
+ clientCapabilities.push("project_file_jobs");
17142
+ registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
17143
+ clientCapabilities.push("remote_command_jobs");
16947
17144
  }
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"}`);
17145
+ const messagePayload = {
17146
+ chat_id: chatId,
17147
+ ...teamId ? { team_id: teamId } : {},
17148
+ client_capabilities: clientCapabilities,
17149
+ // Only decrypted current metadata restores focus; history is never authority.
17150
+ active_focus_id: activeFocusId,
17151
+ focus_phase_state: focusPhaseState,
17152
+ is_incognito: Boolean(params.incognito),
17153
+ message: {
17154
+ message_id: messageId2,
17155
+ chat_id: chatId,
17156
+ role: "user",
17157
+ sender_name: "User",
17158
+ status: "sent",
17159
+ content: finalMessage,
17160
+ created_at: createdAt,
17161
+ chat_has_title: Boolean(params.chatId)
16966
17162
  }
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
17163
  };
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.");
17164
+ const useCombinedAdmission = ws.supportsServerCapability?.("chat_turn_admit_v1") === true;
17165
+ const useDeferredContext = useCombinedAdmission && !params.chatId && !params.projectId && !teamId && ws.supportsServerCapability?.("chat_turn_context_v1") === true;
17166
+ const useSelectionDetailsV2 = useDeferredContext && sdkMintedChatId && ws.supportsServerCapability?.("chat_turn_context_selection_details_v2") === true;
17167
+ const contextRequestId = !params.incognito && shouldWaitForAi && useDeferredContext ? randomUUID9() : null;
17168
+ let pendingContext = null;
17169
+ if (!params.incognito && shouldWaitForAi) {
17170
+ if (contextRequestId) {
17171
+ messagePayload.client_context_pending = true;
17172
+ messagePayload.client_context_request_id = contextRequestId;
17173
+ }
17174
+ pendingContext = (async () => {
17175
+ const prefetchedDiscovery2 = followupDiscoveryRead ? await followupDiscoveryRead : void 0;
17176
+ if (followupDiscoveryOwner && !this.preparedChatOwnerCurrent(followupDiscoveryOwner, teamId)) {
17177
+ throw new Error("Chat workspace changed during context discovery.");
17178
+ }
17179
+ const prepared = earlyContext ? await earlyContext : { ok: true, value: await withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
17180
+ prefetchedDiscovery: prefetchedDiscovery2,
17181
+ sdkMintedChatId,
17182
+ onRouting: publishRouting,
17183
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
17184
+ usePersonalRoutingDirectory,
17185
+ onProjectRouting: publishProjectRoutingTimed,
17186
+ onProjectRoutingError: rejectProjectRouting
17187
+ })), elapsedMs: Date.now() - contextStartedAt };
17188
+ if (!prepared.ok) throw prepared.error;
17189
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17190
+ if (process.env.OPENMATES_DEBUG === "1") {
17191
+ 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"}`);
17192
+ }
17193
+ return deferredProjectDirectory ? { ...prepared.value, project_focus_candidates: prepared.value.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : prepared.value;
17194
+ })();
17195
+ void pendingContext.catch(() => {
17196
+ });
17031
17197
  }
17032
- if (!chatKeyBytes) {
17033
- throw new Error("Saved chat key is required to encrypt every prepared embed.");
17198
+ if (params.testMockMarker !== void 0) {
17199
+ const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
17200
+ messagePayload.test_mock_marker = capacityReplayMarker(
17201
+ params.testMockMarker,
17202
+ this.apiUrl,
17203
+ process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
17204
+ );
17034
17205
  }
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.");
17206
+ if (isNewChat && encryptedChatKey) {
17207
+ messagePayload.encrypted_chat_key = encryptedChatKey;
17037
17208
  }
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.");
17209
+ if (memoryMetadataKeys.length > 0) {
17210
+ messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
17041
17211
  }
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);
17212
+ if (connectedAccountDirectory) {
17213
+ messagePayload.connected_account_directory = connectedAccountDirectory;
17058
17214
  }
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
17215
+ if (connectedAccountTokenRefs.length > 0) {
17216
+ messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
17217
+ }
17218
+ if (params.benchmarkMetadata) {
17219
+ messagePayload.benchmark_metadata = params.benchmarkMetadata;
17220
+ }
17221
+ if (params.learningMode) {
17222
+ messagePayload.learning_mode = {
17223
+ enabled: params.learningMode.enabled,
17224
+ age_group: params.learningMode.ageGroup ?? null
17225
+ };
17226
+ }
17227
+ if (params.incognito) {
17228
+ const providedHistory = (params.messageHistory ?? []).map(
17229
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
17230
+ );
17231
+ messagePayload.message_history = [...providedHistory, {
17232
+ message_id: messageId2,
17233
+ chat_id: chatId,
17234
+ role: "user",
17235
+ sender_name: "User",
17236
+ content: finalMessage,
17237
+ created_at: createdAt
17238
+ }];
17239
+ } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
17240
+ messagePayload.message_history = messageHistoryForRequest.map(
17241
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
17069
17242
  );
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
17243
  }
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
17244
+ if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17245
+ messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
17246
+ embed_id: embed.embedId,
17247
+ type: embed.type,
17248
+ content: embed.content,
17249
+ status: embed.status,
17250
+ text_preview: embed.textPreview
17251
+ }));
17252
+ }
17253
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
17254
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
17255
+ if (!ownerId2) {
17256
+ throw new Error("Authenticated user identity is required to encrypt embeds.");
17257
+ }
17258
+ if (!chatKeyBytes) {
17259
+ throw new Error("Saved chat key is required to encrypt every prepared embed.");
17260
+ }
17261
+ if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
17262
+ throw new Error("Prepared message embed IDs must be unique.");
17263
+ }
17264
+ const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
17265
+ if (preparedIds.some((id) => !id || id.length > 128)) {
17266
+ throw new Error("Prepared message embed ID is invalid.");
17267
+ }
17268
+ const canonicalReady = /* @__PURE__ */ new Set();
17269
+ for (let offset = 0; offset < preparedIds.length; offset += 20) {
17270
+ const selectedIds = preparedIds.slice(offset, offset + 20);
17271
+ if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
17272
+ throw new Error("Message embed availability request exceeds its byte limit.");
17141
17273
  }
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
- };
17274
+ const path2 = this.appendTeamQuery(
17275
+ `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
17276
+ { teamId }
17277
+ );
17278
+ const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
17279
+ if (!availability.ok) {
17280
+ throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
17281
+ }
17282
+ const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
17283
+ for (const id of page.ready) canonicalReady.add(id);
17284
+ }
17285
+ const masterKey = this.getMasterKeyBytes();
17286
+ for (const embed of params.preparedEmbeds) {
17287
+ if (canonicalReady.has(embed.embedId)) continue;
17288
+ const encrypted = await encryptEmbed(
17289
+ embed,
17290
+ masterKey,
17291
+ chatKeyBytes,
17292
+ chatId,
17293
+ messageId2,
17294
+ ownerId2
17295
+ );
17296
+ if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
17297
+ throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
17298
+ }
17299
+ encryptedEmbeds.push(encrypted);
17152
17300
  }
17153
17301
  }
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
- } : {}
17302
+ if (!params.incognito && encryptedEmbeds.length > 0) {
17303
+ if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
17304
+ assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId: messageId2, ownerId: ownerId2 });
17305
+ }
17306
+ if (encryptedEmbeds.length > 0) {
17307
+ messagePayload.encrypted_embeds = encryptedEmbeds;
17308
+ }
17309
+ let dispatchHandlersReady = () => {
17170
17310
  };
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,
17311
+ const handlersReady = new Promise((resolve20) => {
17312
+ dispatchHandlersReady = resolve20;
17313
+ });
17314
+ let admissionPayload = null;
17315
+ let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId2, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
17316
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
17317
+ traceSendTiming("preflight_begin");
17318
+ const protocolVersion = 1;
17319
+ const chatKeyVersion = 1;
17320
+ const turnId = privacyTurnId;
17321
+ savedTurnId = turnId;
17322
+ catalogTurnId = turnId;
17323
+ terminalExpectedMessagesV = baselineMessagesV + 1;
17324
+ const recoveryKeypairStartedAt = performance.now();
17325
+ const recoveryKeypair = independentRecoveryKeypair ? await independentRecoveryKeypair : await deriveChatCompletionRecoveryKeypair(
17326
+ Buffer.from(chatKeyBytes).toString("base64url"),
17327
+ chatId,
17328
+ chatKeyVersion
17329
+ );
17330
+ recoveryKeypairMs = Math.round(performance.now() - recoveryKeypairStartedAt);
17331
+ const encryptPayloadStartedAt = performance.now();
17332
+ const [encryptedContent, encryptedSenderName, encryptedPiiMappings] = await Promise.all([
17333
+ encryptWithAesGcmCombined(finalMessage, chatKeyBytes),
17334
+ encryptWithAesGcmCombined(teamSenderName, chatKeyBytes),
17335
+ piiMappings.length ? encryptWithAesGcmCombined(JSON.stringify(piiMappings), chatKeyBytes) : null
17336
+ ]);
17337
+ const encryptedUserMessage = {
17338
+ client_message_id: messageId2,
17339
+ chat_id: chatId,
17340
+ encrypted_content: encryptedContent,
17341
+ role: "user",
17190
17342
  created_at: createdAt,
17191
17343
  updated_at: createdAt
17192
17344
  };
17193
- }
17194
- encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17195
- try {
17345
+ encryptedUserMessage.encrypted_sender_name = encryptedSenderName;
17346
+ if (encryptedPiiMappings) encryptedUserMessage.encrypted_pii_mappings = encryptedPiiMappings;
17347
+ Object.assign(messagePayload, {
17348
+ turn_id: turnId,
17349
+ recovery_public_key: recoveryKeypair.publicKey,
17350
+ chat_key_version: chatKeyVersion
17351
+ });
17352
+ let preflightInferenceRequest = messagePayload;
17196
17353
  if (teamId) {
17197
- await stageCliTeamNotificationPreview(ws, {
17198
- teamId,
17199
- chatId,
17200
- messageId: messageId2,
17201
- content: finalMessage,
17202
- title: notificationTitle
17354
+ const teamInferenceHistory = [
17355
+ ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
17356
+ role: historyMessage.role,
17357
+ content: historyMessage.content,
17358
+ category: historyMessage.category ?? null,
17359
+ sender_name: historyMessage.sender_name ?? historyMessage.role,
17360
+ created_at: historyMessage.created_at
17361
+ })),
17362
+ {
17363
+ role: "user",
17364
+ content: finalMessage,
17365
+ category: null,
17366
+ sender_name: teamSenderName,
17367
+ created_at: createdAt
17368
+ }
17369
+ ];
17370
+ const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId2 };
17371
+ delete messagePayload.message_history;
17372
+ messagePayload.message = teamMessageEnvelope;
17373
+ if (shouldWaitForAi) {
17374
+ messagePayload.team_ai_invocation = { history: teamInferenceHistory };
17375
+ preflightInferenceRequest = {
17376
+ ...messagePayload,
17377
+ message_history: teamInferenceHistory
17378
+ };
17379
+ }
17380
+ }
17381
+ const preflightPayload = {
17382
+ protocol_version: protocolVersion,
17383
+ chat_id: chatId,
17384
+ ...teamId ? { team_id: teamId } : {},
17385
+ turn_id: turnId,
17386
+ message_id: messageId2,
17387
+ chat_key_version: chatKeyVersion,
17388
+ encrypted_chat_key: encryptedChatKey,
17389
+ recovery_public_key: recoveryKeypair.publicKey,
17390
+ expected_messages_v: baselineMessagesV,
17391
+ encrypted_user_message: encryptedUserMessage,
17392
+ inference_request: preflightInferenceRequest,
17393
+ ...conditionalPrivacyReceipt ? {
17394
+ privacy_receipt_id: conditionalPrivacyReceipt.receiptId,
17395
+ privacy_generation: conditionalPrivacyReceipt.generation
17396
+ } : {}
17397
+ };
17398
+ if (isNewChat) {
17399
+ if (!chatSlugLookupKey) {
17400
+ throw new Error("Encrypted chat slug lookup key was not initialized.");
17401
+ }
17402
+ const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
17403
+ const slugMetadata = await buildEncryptedObjectSlugMetadata({
17404
+ value: selectNewChatSlugValue({
17405
+ message: finalMessage,
17406
+ chatId,
17407
+ explicitSlug: params.slug
17408
+ }),
17409
+ encryptionKey: chatKeyBytes,
17410
+ lookupKey: chatSlugLookupKey
17203
17411
  });
17412
+ preflightPayload.encrypted_chat_metadata = {
17413
+ encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
17414
+ encrypted_slug: slugMetadata.encrypted_slug,
17415
+ slug_lookup_hash: slugMetadata.slug_lookup_hash,
17416
+ encrypted_chat_key: encryptedChatKey,
17417
+ created_at: createdAt,
17418
+ updated_at: createdAt
17419
+ };
17420
+ }
17421
+ encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17422
+ try {
17423
+ if (teamId) {
17424
+ await stageCliTeamNotificationPreview(ws, {
17425
+ teamId,
17426
+ chatId,
17427
+ messageId: messageId2,
17428
+ content: finalMessage,
17429
+ title: notificationTitle
17430
+ });
17431
+ }
17432
+ if (params.projectId) {
17433
+ await prepareCliProjectFocusForPreflight({
17434
+ ws,
17435
+ chatId,
17436
+ teamId,
17437
+ isNewChat,
17438
+ encryptedChatKey,
17439
+ createdAt,
17440
+ activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17441
+ });
17442
+ const focusedContext = await prepareCliJevContext(
17443
+ this,
17444
+ chatId,
17445
+ { teamId, personal: !teamId },
17446
+ params.jevContext,
17447
+ finalMessage
17448
+ );
17449
+ pendingContext = Promise.resolve(focusedContext);
17450
+ }
17451
+ admissionPayload = preflightPayload;
17452
+ traceSendTiming("preflight_ready");
17453
+ } catch (error) {
17454
+ ws.close();
17455
+ throw error;
17204
17456
  }
17205
- if (params.projectId) {
17206
- await prepareCliProjectFocusForPreflight({
17457
+ }
17458
+ const initialHeaderOwner = this.preparedChatOwner(this.requireSession(), teamId);
17459
+ const startInitialHeader = () => {
17460
+ if (!initialAdmissionDurable || !initialHeaderNotice || !chatKeyBytes || !isNewChat || params.incognito || !savedTurnId) return;
17461
+ const notice = { ...initialHeaderNotice }, key = chatKeyBytes, turn = savedTurnId;
17462
+ const titleChanged = !!notice.chatTitle && notice.chatTitle !== initialTitleScheduled;
17463
+ const iconChanged = !!notice.chatIcon && notice.chatIcon !== initialIconScheduled;
17464
+ if (!titleChanged && !iconChanged) return;
17465
+ initialTitleScheduled = notice.chatTitle ?? initialTitleScheduled;
17466
+ initialIconScheduled = notice.chatIcon ?? initialIconScheduled;
17467
+ const current2 = () => !initialHeaderStopped && ws.isOpen() && this.preparedChatOwnerCurrent(initialHeaderOwner, teamId) && this.activeSubmittedChatTurns.get(chatId) === turn;
17468
+ initialHeaderWork = (initialHeaderWork ?? Promise.resolve()).then(async () => {
17469
+ if (!current2()) return;
17470
+ if (titleChanged) await this.persistPostProcessingMetadata({
17207
17471
  ws,
17208
17472
  chatId,
17209
17473
  teamId,
17210
- isNewChat,
17474
+ chatKeyBytes: key,
17211
17475
  encryptedChatKey,
17212
- createdAt,
17213
- activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17476
+ followUpSuggestions: [],
17477
+ suggestedFollowupMessage: null,
17478
+ newChatSuggestions: [],
17479
+ chatSummary: null,
17480
+ chatTags: [],
17481
+ updatedChatTitle: notice.chatTitle ?? null,
17482
+ sourceTitleVersion: notice.sourceTitleVersion ?? null,
17483
+ sourceMetadataVersion: notice.sourceMetadataVersion ?? null,
17484
+ unversionedGeneratedTitle: notice.sourceTitleVersion === void 0 && notice.sourceMetadataVersion === void 0,
17485
+ ownerCurrent: current2
17214
17486
  });
17215
- const focusedContext = await prepareCliJevContext(
17216
- this,
17217
- chatId,
17218
- { teamId, personal: !teamId },
17219
- params.jevContext,
17220
- finalMessage
17221
- );
17222
- pendingContext = Promise.resolve(focusedContext);
17487
+ if (!current2()) return;
17488
+ if (iconChanged || notice.category) {
17489
+ const [encryptedIcon, encryptedCategory] = await Promise.all([
17490
+ notice.chatIcon ? encryptWithAesGcmCombined(notice.chatIcon, key) : null,
17491
+ notice.category ? encryptWithAesGcmCombined(notice.category, key) : null
17492
+ ]);
17493
+ if (!current2()) return;
17494
+ const stored = ws.waitForMessage("encrypted_metadata_stored", (value) => value.chat_id === chatId, 2e4);
17495
+ void stored.catch(() => {
17496
+ });
17497
+ await ws.sendAsync("encrypted_chat_metadata", {
17498
+ chat_id: chatId,
17499
+ ...teamId ? { team_id: teamId } : {},
17500
+ encrypted_chat_key: encryptedChatKey,
17501
+ ...encryptedIcon ? { encrypted_icon: encryptedIcon } : {},
17502
+ ...encryptedCategory ? { encrypted_chat_category: encryptedCategory } : {}
17503
+ });
17504
+ await stored;
17505
+ }
17506
+ if (current2()) clearSyncCache(teamId);
17507
+ }).catch(() => {
17508
+ });
17509
+ };
17510
+ const onTurnStream = (event) => {
17511
+ 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)) {
17512
+ initialHeaderNotice = { ...initialHeaderNotice, ...event };
17513
+ startInitialHeader();
17223
17514
  }
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({
17515
+ params.onStream?.(event);
17516
+ };
17517
+ if (!params.incognito && chatKeyBytes && savedTurnId) {
17518
+ const key = chatKeyBytes;
17519
+ const ownedTurnId = savedTurnId;
17520
+ this.activeSubmittedChatTurns.set(chatId, ownedTurnId);
17521
+ const owner2 = this.preparedChatOwner(this.requireSession(), teamId);
17522
+ const stopOwned = () => optionalMetadata?.stop();
17523
+ optionalMetadata = observeOwnedTurnMetadata({
17244
17524
  ws,
17245
17525
  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
17526
+ turnId: savedTurnId,
17527
+ userMessageId: messageId2,
17528
+ closeAfterMetadata: true,
17529
+ ownerCurrent: () => this.preparedChatOwnerCurrent(owner2, teamId),
17530
+ persist: async (metadata) => {
17531
+ await this.persistPostProcessingMetadata({ ws, teamId, chatKeyBytes: key, encryptedChatKey, ...metadata });
17532
+ clearSyncCache(teamId);
17533
+ },
17534
+ onMetadata: params.onMetadata,
17535
+ onReleased: () => {
17536
+ this.ownedMetadataSubscriptions.delete(stopOwned);
17537
+ if (this.activeSubmittedChatTurns.get(chatId) === ownedTurnId) this.activeSubmittedChatTurns.delete(chatId);
17538
+ if (metadataTransportOwned) ws.close();
17539
+ }
17259
17540
  });
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();
17541
+ while (this.ownedMetadataSubscriptions.size >= 8) this.ownedMetadataSubscriptions.values().next().value?.();
17542
+ this.ownedMetadataSubscriptions.add(stopOwned);
17287
17543
  }
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
- }
17544
+ if (!params.incognito && shouldWaitForAi) {
17545
+ precollectedResponse = ws.collectAiResponse(messageId2, chatId, {
17546
+ waitForPostProcessing: false,
17547
+ onStream: onTurnStream,
17548
+ onSubChatEvent: async (event) => {
17549
+ await handlersReady;
17550
+ await handleSubChatEvent(event);
17551
+ },
17552
+ onAppSettingsMemoriesRequest: async (event) => {
17553
+ await handlersReady;
17554
+ await handleAppSettingsMemoriesRequest(event);
17555
+ },
17556
+ timeoutMs: params.responseTimeoutMs,
17557
+ recoveryTurnId: savedTurnId,
17558
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17559
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17560
+ });
17561
+ }
17562
+ const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
17563
+ const event = payload;
17564
+ return event.chat_id === chatId && event.user_message_id === messageId2;
17565
+ }, params.responseTimeoutMs ?? 9e4) : null;
17566
+ if (queuedAdmission) void queuedAdmission.catch(() => {
17313
17567
  });
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)
17568
+ const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
17569
+ const event = payload;
17570
+ return event.chat_id === chatId && (event.message_id === messageId2 || event.message_id === void 0);
17571
+ }, params.responseTimeoutMs ?? 9e4) : null;
17572
+ if (pausedHandoff) void pausedHandoff.catch(() => {
17333
17573
  });
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;
17574
+ const confirmed = ws.waitForMessage(
17575
+ "chat_message_confirmed",
17576
+ (payload) => {
17577
+ const eventPayload = payload;
17578
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId2;
17579
+ },
17580
+ params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
17581
+ );
17582
+ let confirmedPayload;
17583
+ let admissionContinuationPhase = "before_dispatch";
17584
+ let admissionAckObserved = false;
17585
+ let routingFrameSent = false;
17586
+ let detailsFrameSent = false;
17587
+ const admissionContinuationStartedAt = performance.now();
17588
+ try {
17589
+ if (admissionPayload) {
17590
+ if (!useDeferredContext && recoveryDiscovery) await recoveryDiscovery;
17591
+ if (recoveryDiscoveryError) throw recoveryDiscoveryError;
17592
+ if (!contextRequestId && pendingContext) {
17593
+ const context = await pendingContext;
17594
+ Object.assign(messagePayload, context);
17595
+ Object.assign(admissionPayload.inference_request, context);
17434
17596
  }
17435
17597
  if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17436
- let rejectContextAck;
17437
- contextAckRejection = new Promise((_resolve, reject) => {
17438
- rejectContextAck = reject;
17598
+ const admissionAck = ws.waitForMessage("chat_turn_preflight_ack", (payload) => payload.turn_id === savedTurnId);
17599
+ void admissionAck.catch(() => {
17439
17600
  });
17440
- void contextAckRejection.catch(() => {
17441
- });
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;
17601
+ const frameDispatchAt = performance.now();
17602
+ admissionContinuationPhase = "await_ack";
17603
+ await consumeSocketMemoryReceipt();
17604
+ await ws.sendAsync(useCombinedAdmission ? "chat_turn_admit" : "chat_turn_preflight", admissionPayload);
17605
+ traceSendTiming("admission_frame");
17606
+ if (process.env.OPENMATES_DEBUG === "1") {
17607
+ 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)}`);
17608
+ }
17609
+ const ackPayload = (await admissionAck).payload;
17610
+ traceSendTiming("admission_ack");
17611
+ admissionAckObserved = true;
17612
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
17613
+ throw new Error("Encrypted chat admission acknowledgement omitted preflight_id.");
17614
+ }
17615
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
17616
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
17617
+ }
17618
+ initialAdmissionDurable = true;
17619
+ startInitialHeader();
17620
+ if (!useCombinedAdmission) {
17621
+ Object.assign(messagePayload, { protocol_version: admissionPayload.protocol_version, preflight_id: ackPayload.preflight_id });
17622
+ await ws.sendAsync("chat_message_added", messagePayload);
17623
+ }
17624
+ if (pendingContext && contextRequestId) {
17625
+ admissionContinuationPhase = "await_routing";
17626
+ const identity3 = {
17627
+ chat_id: chatId,
17628
+ turn_id: savedTurnId,
17629
+ message_id: messageId2,
17630
+ request_id: contextRequestId,
17631
+ ...teamId ? { team_id: teamId } : {}
17632
+ };
17633
+ let failureNotice = null;
17634
+ const notifyContextFailure = (reason) => {
17635
+ if (failureNotice) return failureNotice;
17636
+ if (!ws.supportsServerCapability("chat_turn_context_failure_v1") || !ws.isOpen() || !contextOwnerIsCurrent() || !savedTurnId) return Promise.resolve();
17637
+ failureNotice = (async () => {
17638
+ const failureAck = ws.waitForMessage("chat_turn_context_failed_ack", (payload) => {
17639
+ const event = payload;
17640
+ return event.chat_id === chatId && event.turn_id === savedTurnId && event.message_id === messageId2 && event.request_id === contextRequestId;
17641
+ }, 5e3);
17642
+ void failureAck.catch(() => {
17643
+ });
17644
+ await ws.sendAsync("chat_turn_context_failed", { ...identity3, reason });
17645
+ const status = (await failureAck).payload;
17646
+ if (status.status !== "accepted" && status.status !== "duplicate") {
17647
+ throw new Error("Chat context failure receipt was rejected.");
17648
+ }
17649
+ })();
17650
+ return failureNotice;
17651
+ };
17652
+ let routing;
17653
+ try {
17654
+ routing = useSelectionDetailsV2 ? await projectRoutingContext : deferredProjectDirectory ? (await Promise.all([pendingContext, projectRoutingContext]))[0] : params.projectId ? await pendingContext : await Promise.race([routingContext, pendingContext]);
17655
+ } catch (error) {
17656
+ try {
17657
+ await notifyContextFailure(useSelectionDetailsV2 ? "project_discovery_failed" : "context_preparation_failed");
17658
+ } catch {
17659
+ }
17660
+ throw error;
17455
17661
  }
17456
- void notifyContextFailure("context_preparation_failed").catch(() => {
17457
- }).then(() => {
17458
- rejectContextAck(new Error("Chat context delivery was rejected."));
17662
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17663
+ let rejectContextAck;
17664
+ contextAckRejection = new Promise((_resolve, reject) => {
17665
+ rejectContextAck = reject;
17459
17666
  });
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) {
17667
+ void contextAckRejection.catch(() => {
17668
+ });
17669
+ let rejectedPhaseSeen = false;
17670
+ const attemptedContextPhases = /* @__PURE__ */ new Set();
17671
+ removeContextAckWatcher = ws.onMessageType("chat_turn_context_ack", (event) => {
17672
+ 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;
17673
+ if (event.status === "accepted" || event.status === "duplicate") {
17674
+ traceSendTiming(event.phase === "routing" ? "routing_ack" : "details_ack");
17675
+ return;
17676
+ }
17677
+ if (event.status !== "rejected") return;
17678
+ rejectedPhaseSeen = true;
17679
+ if (!contextOwnerIsCurrent()) {
17680
+ rejectContextAck(new Error("Chat workspace changed while preparing context."));
17681
+ return;
17682
+ }
17683
+ void notifyContextFailure("context_preparation_failed").catch(() => {
17684
+ }).then(() => {
17685
+ rejectContextAck(new Error("Chat context delivery was rejected."));
17686
+ });
17687
+ });
17688
+ const { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } = routing;
17689
+ traceSendTiming("routing_prepared");
17690
+ attemptedContextPhases.add("routing");
17691
+ 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 } });
17692
+ routingFrameSent = true;
17693
+ traceSendTiming("routing_frame");
17694
+ admissionContinuationPhase = "await_details";
17695
+ let context;
17472
17696
  try {
17473
- await notifyContextFailure("context_preparation_failed");
17474
- } catch {
17697
+ context = await Promise.race([pendingContext, contextAckRejection, catalogDeliveryFailure]);
17698
+ } catch (error) {
17699
+ try {
17700
+ await notifyContextFailure("context_preparation_failed");
17701
+ } catch {
17702
+ }
17703
+ throw error;
17475
17704
  }
17476
- throw error;
17705
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17706
+ traceSendTiming("details_prepared");
17707
+ attemptedContextPhases.add("details");
17708
+ await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "details", context: {
17709
+ custom_memory_documents: context.custom_memory_documents ?? [],
17710
+ project_focus_documents: context.project_focus_documents ?? [],
17711
+ project_context_documents: context.project_context_documents ?? [],
17712
+ ...useSelectionDetailsV2 ? {
17713
+ related_task_candidates: context.related_task_candidates ?? [],
17714
+ project_focus_catalog: context.project_focus_catalog ?? [],
17715
+ accepted_plan_context: context.accepted_plan_context ?? null,
17716
+ ...connectedAccountDirectory ? { connected_account_directory: connectedAccountDirectory } : {}
17717
+ } : {}
17718
+ } });
17719
+ detailsFrameSent = true;
17720
+ traceSendTiming("details_frame");
17477
17721
  }
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");
17722
+ admissionContinuationPhase = "await_confirmation";
17723
+ } else {
17724
+ await consumeSocketMemoryReceipt();
17725
+ await ws.sendAsync("chat_message_added", messagePayload);
17494
17726
  }
17495
- admissionContinuationPhase = "await_confirmation";
17496
- } else {
17497
- await consumeSocketMemoryReceipt();
17498
- await ws.sendAsync("chat_message_added", messagePayload);
17499
- }
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)}`);
17727
+ confirmedPayload = (await Promise.race([confirmed, catalogDeliveryFailure, ...contextAckRejection ? [contextAckRejection] : []])).payload;
17728
+ } catch (error) {
17729
+ if (process.env.OPENMATES_DEBUG === "1" && admissionPayload) {
17730
+ 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)}`);
17731
+ }
17732
+ dispatchHandlersReady();
17733
+ void confirmed.catch(() => {
17734
+ });
17735
+ if (precollectedResponse) void precollectedResponse.catch(() => {
17736
+ });
17737
+ ws.close();
17738
+ throw error;
17504
17739
  }
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;
17740
+ if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
17741
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v;
17539
17742
  }
17540
- if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17541
- return false;
17743
+ if (!params.incognito && (!teamId || shouldWaitForAi)) {
17744
+ clearSyncCache(teamId);
17542
17745
  }
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);
17746
+ let assistant = "";
17747
+ let assistantMessageId = null;
17748
+ let category = null;
17749
+ let modelName = null;
17750
+ let tokenUsage = null;
17751
+ let promptBudget = null;
17752
+ let followUpSuggestions = [];
17753
+ let suggestedFollowupMessage = null;
17754
+ let taskProposals = [];
17755
+ let taskUpdateProposals = [];
17756
+ let taskEvents = [];
17757
+ let pendingTaskUpdateJobs = [];
17758
+ let subChatEvents = [];
17759
+ const appSettingsMemoryRequests = [];
17760
+ const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
17761
+ const isApprovalWithinServerLimits = (request) => {
17762
+ const count = request.subChats.length;
17763
+ if (count === 0) return false;
17764
+ if (request.remainingSubChats !== null && count > request.remainingSubChats) {
17765
+ return false;
17563
17766
  }
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
17767
+ if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17768
+ return false;
17606
17769
  }
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
17770
+ return true;
17771
+ };
17772
+ const isAutoApprovalWithinServerLimits = (request) => {
17773
+ if (!isApprovalWithinServerLimits(request)) return false;
17774
+ return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
17775
+ };
17776
+ const handleSubChatEvent = async (event) => {
17777
+ params.onSubChatEvent?.(event);
17778
+ if (event.type === "spawn_sub_chats") {
17779
+ if (params.incognito || !chatKeyBytes) return;
17780
+ const rawSubChats = event.payload.sub_chats;
17781
+ if (!Array.isArray(rawSubChats)) return;
17782
+ const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
17783
+ parentChatId: chatId,
17784
+ parentChatKey: chatKeyBytes,
17785
+ encryptedParentChatKey: encryptedChatKey,
17786
+ subChats: rawSubChats
17639
17787
  });
17788
+ for (const metadataPayload of metadataPayloads) {
17789
+ await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
17790
+ }
17791
+ return;
17640
17792
  }
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)
17793
+ if (event.type !== "sub_chat_confirmation_required") return;
17794
+ const payload = event.payload;
17795
+ const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
17796
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
17797
+ const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
17798
+ if (!taskId) return;
17799
+ const request = {
17800
+ chatId: confirmationChatId,
17801
+ taskId,
17802
+ subChats,
17803
+ maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
17804
+ maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
17805
+ existingSubChats: numberOrNull(payload.existing_sub_chats),
17806
+ remainingSubChats: numberOrNull(payload.remaining_sub_chats)
17807
+ };
17808
+ const withinLimits = isApprovalWithinServerLimits(request);
17809
+ const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
17810
+ await ws.sendAsync(
17811
+ "sub_chat_confirmation",
17812
+ buildSubChatConfirmationPayload({
17813
+ chatId: request.chatId,
17814
+ taskId: request.taskId,
17815
+ approved,
17816
+ approveCount: approved ? request.subChats.length : void 0
17817
+ })
17650
17818
  );
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
17819
+ };
17820
+ const persistSystemMessage = async (systemMessage) => {
17821
+ if (!chatKeyBytes) return;
17822
+ await ws.sendAsync("chat_system_message_added", {
17823
+ chat_id: chatId,
17824
+ message: {
17825
+ message_id: systemMessage.message_id,
17826
+ role: "system",
17827
+ encrypted_content: await encryptWithAesGcmCombined(
17828
+ systemMessage.content,
17829
+ chatKeyBytes
17830
+ ),
17831
+ created_at: systemMessage.created_at,
17832
+ user_message_id: systemMessage.user_message_id
17833
+ }
17674
17834
  });
17675
- await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17835
+ await ws.waitForMessage(
17836
+ "system_message_confirmed",
17837
+ (payload) => {
17838
+ const p = payload;
17839
+ return p.message_id === systemMessage.message_id;
17840
+ },
17841
+ 2e4
17842
+ );
17843
+ };
17844
+ const persistMemoryRequestSystemMessage = async (event) => {
17845
+ const requestId = event.requestId ?? `${messageId2}-memory-request`;
17846
+ await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
17676
17847
  userMessageId: messageId2,
17677
- messageId: `${messageId2}-memory-response`,
17678
- action: "included",
17679
- categories,
17848
+ requestId,
17849
+ requestedKeys: event.requestedKeys,
17850
+ entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
17680
17851
  createdAt: Math.floor(Date.now() / 1e3)
17681
17852
  }));
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.");
17853
+ };
17854
+ const responseCategoriesFromMemories = (approvedMemories) => {
17855
+ const counts = /* @__PURE__ */ new Map();
17856
+ for (const memory of approvedMemories) {
17857
+ const key = `${memory.app_id}-${memory.item_key}`;
17858
+ const existing = counts.get(key);
17859
+ if (existing) {
17860
+ existing.entryCount += 1;
17861
+ } else {
17862
+ counts.set(key, {
17863
+ appId: memory.app_id,
17864
+ itemType: memory.item_key,
17865
+ entryCount: 1
17866
+ });
17778
17867
  }
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: [] });
17868
+ }
17869
+ return [...counts.values()];
17870
+ };
17871
+ const handleAppSettingsMemoriesRequest = async (event) => {
17872
+ await persistMemoryRequestSystemMessage(event);
17873
+ if (params.autoApproveMemories) {
17874
+ const requested = new Set(event.requestedKeys);
17875
+ const unadvertisedKeys = event.requestedKeys.filter(
17876
+ (key) => !memoryMetadataKeys.includes(key)
17877
+ );
17878
+ if (unadvertisedKeys.length > 0) {
17879
+ throw new Error(
17880
+ `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
17881
+ );
17792
17882
  }
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(() => {
17883
+ const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
17884
+ app_id: memory.app_id,
17885
+ item_key: memory.item_type,
17886
+ content: memory.data
17887
+ }));
17888
+ const approvedKeys = [
17889
+ ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
17890
+ ];
17891
+ const categories = responseCategoriesFromMemories(approvedMemories);
17892
+ appSettingsMemoryRequests.push({
17893
+ requestId: event.requestId,
17894
+ requestedKeys: event.requestedKeys,
17895
+ approvedKeys,
17896
+ entryCount: approvedMemories.length
17816
17897
  });
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(() => {
17898
+ await ws.sendAsync("app_settings_memories_confirmed", {
17899
+ chat_id: event.chatId,
17900
+ app_settings_memories: approvedMemories
17821
17901
  });
17822
- const frame = outcome.frame.payload;
17823
- return {
17824
- status: "queued",
17825
- chatId,
17826
- messageId: null,
17902
+ await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17827
17903
  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
- };
17904
+ messageId: `${messageId2}-memory-response`,
17905
+ action: "included",
17906
+ categories,
17907
+ createdAt: Math.floor(Date.now() / 1e3)
17908
+ }));
17909
+ return;
17844
17910
  }
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
- };
17911
+ appSettingsMemoryRequests.push({
17912
+ requestId: event.requestId,
17913
+ requestedKeys: event.requestedKeys,
17914
+ approvedKeys: [],
17915
+ entryCount: 0
17916
+ });
17917
+ throw new Error(
17918
+ `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.`
17919
+ );
17920
+ };
17921
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
17922
+ const persistTaskEventSystemMessages = async (events) => {
17923
+ if (!chatKeyBytes || events.length === 0) return;
17924
+ for (const event of events) {
17925
+ if (persistedTaskEventIds.has(event.event_id)) continue;
17926
+ await this.persistEncryptedSystemMessage(
17927
+ ws,
17928
+ await buildTaskEventSystemMessage({
17929
+ chatKey: chatKeyBytes,
17930
+ userMessageId: messageId2,
17931
+ event
17932
+ }),
17933
+ chatId
17934
+ );
17935
+ persistedTaskEventIds.add(event.event_id);
17880
17936
  }
17881
- if (chatKeyBytes) {
17882
- if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
17883
- throw new Error("Saved chat completion did not include recoverable terminal identity.");
17937
+ };
17938
+ const streamOpts = {
17939
+ waitForPostProcessing: false,
17940
+ onStream: onTurnStream,
17941
+ onSubChatEvent: handleSubChatEvent,
17942
+ onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
17943
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17944
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17945
+ };
17946
+ dispatchHandlersReady();
17947
+ if (!shouldWaitForAi) {
17948
+ ws.close();
17949
+ } else if (params.incognito) {
17950
+ try {
17951
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17952
+ ...streamOpts,
17953
+ timeoutMs: params.responseTimeoutMs,
17954
+ recoveryTurnId: savedTurnId
17955
+ }));
17956
+ assistantMessageId = resp.messageId;
17957
+ assistant = resp.content;
17958
+ category = resp.category;
17959
+ modelName = resp.modelName;
17960
+ tokenUsage = resp.tokenUsage;
17961
+ promptBudget = resp.promptBudget;
17962
+ taskEvents = resp.taskEvents;
17963
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17964
+ subChatEvents = resp.subChatEvents;
17965
+ if (resp.status === "waiting_for_user") {
17966
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17967
+ return {
17968
+ status: resp.status,
17969
+ chatId,
17970
+ messageId: assistantMessageId,
17971
+ assistant,
17972
+ category,
17973
+ modelName,
17974
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17975
+ followUpSuggestions,
17976
+ taskProposals,
17977
+ taskUpdateProposals,
17978
+ taskEvents,
17979
+ pendingTaskUpdateJobs,
17980
+ subChatEvents,
17981
+ appSettingsMemoryRequests,
17982
+ tokenUsage,
17983
+ promptBudget
17984
+ };
17884
17985
  }
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 {
17986
+ } finally {
17987
+ ws.close();
17988
+ }
17989
+ } else {
17990
+ try {
17991
+ const recoverCurrentTurnOutputs = async () => {
17992
+ if (recoveryDiscovery) {
17906
17993
  try {
17907
- await ws.sendAsync("recovery_job_claim", {
17908
- protocol_version: 1,
17909
- job_id: recoveryJobId,
17910
- request_id: requestId
17911
- });
17994
+ await recoveryDiscovery;
17912
17995
  } catch (error) {
17913
- void claimPromise.catch(() => {
17914
- });
17915
- throw error;
17996
+ if (!useDeferredContext) throw error;
17997
+ return;
17916
17998
  }
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
17999
  }
18000
+ const pages = ws.drainAvailableRecoveryOutputPages();
18001
+ if (pages.length === 0) return;
18002
+ if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
18003
+ if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
18004
+ throw new Error("Saved chat recovery output requires the root chat key.");
18005
+ }
18006
+ const cache = loadSyncCache(teamId) ?? {
18007
+ syncedAt: Date.now(),
18008
+ totalChatCount: 0,
18009
+ loadedChatCount: 0,
18010
+ chats: [],
18011
+ embeds: [],
18012
+ embedKeys: [],
18013
+ chatKeyWrappers: []
18014
+ };
18015
+ if (encryptedChatKey) {
18016
+ const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
18017
+ if (root) root.details.encrypted_chat_key = encryptedChatKey;
18018
+ else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
18019
+ }
18020
+ await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
18021
+ };
18022
+ const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
18023
+ ...streamOpts,
18024
+ timeoutMs: params.responseTimeoutMs,
18025
+ recoveryTurnId: savedTurnId
18026
+ });
18027
+ const outcome = await Promise.race([
18028
+ responsePromise.then((response) => ({ kind: "response", response })),
18029
+ ...contextAckRejection ? [contextAckRejection] : [],
18030
+ catalogDeliveryFailure,
18031
+ ...queuedAdmission ? [queuedAdmission.then(
18032
+ (frame) => ({ kind: "queued", frame }),
18033
+ () => new Promise(() => {
18034
+ })
18035
+ )] : [],
18036
+ ...pausedHandoff ? [pausedHandoff.then(
18037
+ (frame) => ({ kind: "paused", frame }),
18038
+ () => new Promise(() => {
18039
+ })
18040
+ )] : []
18041
+ ]);
18042
+ if (outcome.kind === "paused") {
18043
+ void responsePromise.catch(() => {
18044
+ });
18045
+ throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
17932
18046
  }
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
18047
+ if (outcome.kind === "queued") {
18048
+ void responsePromise.catch(() => {
17951
18049
  });
17952
- clearSyncCache(teamId);
17953
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18050
+ const frame = outcome.frame.payload;
18051
+ return {
18052
+ status: "queued",
18053
+ chatId,
18054
+ messageId: null,
18055
+ userMessageId: messageId2,
18056
+ queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
18057
+ queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
18058
+ assistant: "",
18059
+ category: null,
18060
+ modelName: null,
18061
+ mateName: null,
18062
+ followUpSuggestions: [],
18063
+ taskProposals: [],
18064
+ taskUpdateProposals: [],
18065
+ taskEvents: [],
18066
+ pendingTaskUpdateJobs: [],
18067
+ subChatEvents: [],
18068
+ appSettingsMemoryRequests: [],
18069
+ tokenUsage: null,
18070
+ promptBudget: null
18071
+ };
18072
+ }
18073
+ const resp = outcome.response;
18074
+ assistantMessageId = resp.messageId;
18075
+ assistant = resp.content;
18076
+ category = resp.category;
18077
+ modelName = resp.modelName;
18078
+ tokenUsage = resp.tokenUsage;
18079
+ promptBudget = resp.promptBudget;
18080
+ followUpSuggestions = resp.followUpSuggestions;
18081
+ suggestedFollowupMessage = resp.suggestedFollowupMessage;
18082
+ taskProposals = resp.taskProposals;
18083
+ taskUpdateProposals = resp.taskUpdateProposals;
18084
+ taskEvents = resp.taskEvents;
18085
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
18086
+ subChatEvents = resp.subChatEvents;
18087
+ if (resp.status === "waiting_for_user") {
17954
18088
  await persistTaskEventSystemMessages(taskEvents);
17955
18089
  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
18090
  return {
17962
- status: "completed",
18091
+ status: resp.status,
17963
18092
  chatId,
17964
18093
  messageId: assistantMessageId,
17965
18094
  assistant,
17966
18095
  category,
17967
18096
  modelName,
17968
- mateName: mateName2,
18097
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17969
18098
  followUpSuggestions,
17970
18099
  taskProposals,
17971
18100
  taskUpdateProposals,
@@ -17977,234 +18106,339 @@ var OpenMatesClient = class _OpenMatesClient {
17977
18106
  promptBudget
17978
18107
  };
17979
18108
  }
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,
18109
+ if (chatKeyBytes) {
18110
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
18111
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
18112
+ }
18113
+ const recoveryJobId = resp.recoveryJobId;
18114
+ let claimRetryDeadline = null;
18115
+ let claim;
18116
+ let claimRetryDelayMs = 500;
18117
+ while (true) {
18118
+ const requestId = randomUUID9();
18119
+ let matchingLeaseConflict = false;
18120
+ const stopWatchingErrors = ws.onMessageType("error", (payload) => {
18121
+ if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
18122
+ matchingLeaseConflict = true;
18123
+ }
18124
+ });
18125
+ const claimPromise = ws.waitForMessage(
18126
+ "recovery_job_claimed",
18127
+ (payload) => {
18128
+ const received = payload;
18129
+ return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
18130
+ },
18131
+ claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
18132
+ );
18133
+ try {
18134
+ try {
18135
+ await ws.sendAsync("recovery_job_claim", {
18136
+ protocol_version: 1,
18137
+ job_id: recoveryJobId,
18138
+ request_id: requestId
18139
+ });
18140
+ } catch (error) {
18141
+ void claimPromise.catch(() => {
18142
+ });
18143
+ throw error;
18144
+ }
18145
+ claim = (await claimPromise).payload;
18146
+ break;
18147
+ } catch (error) {
18148
+ if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
18149
+ claimRetryDeadline ??= Date.now() + Math.min(
18150
+ 1e4,
18151
+ Math.max(0, params.responseTimeoutMs ?? 9e4)
18152
+ );
18153
+ const remainingMs = claimRetryDeadline - Date.now();
18154
+ if (remainingMs <= claimRetryDelayMs) throw error;
18155
+ await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
18156
+ claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
18157
+ } finally {
18158
+ stopWatchingErrors();
18159
+ }
18160
+ }
18161
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
18162
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
18163
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
18164
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
18165
+ const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
18166
+ if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
18167
+ assistant = resp.content;
18168
+ category = resp.category;
18169
+ modelName = resp.modelName;
18170
+ assistantMessageId = assistantId;
18171
+ await this.persistStreamedEmbeds({
18172
+ ws,
18173
+ embeds: resp.embeds,
18174
+ chatId,
18175
+ chatKeyBytes,
18176
+ fallbackMessageId: assistantId,
18177
+ ownerId: ownerId2,
18178
+ teamId
18179
+ });
18180
+ clearSyncCache(teamId);
18181
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18182
+ await persistTaskEventSystemMessages(taskEvents);
18183
+ await recoverCurrentTurnOutputs();
18184
+ await projectFocusCountdown?.flush();
18185
+ await focusPersistence;
18186
+ await chatContextEvents?.flush();
18187
+ if (focusPersistenceError) throw focusPersistenceError;
18188
+ const mateName2 = category ? MATE_NAMES[category] ?? null : null;
18189
+ return {
18190
+ status: "completed",
18191
+ chatId,
18192
+ messageId: assistantMessageId,
18193
+ assistant,
18194
+ category,
18195
+ modelName,
18196
+ mateName: mateName2,
18197
+ followUpSuggestions,
18198
+ taskProposals,
18199
+ taskUpdateProposals,
18200
+ taskEvents,
18201
+ pendingTaskUpdateJobs,
18202
+ subChatEvents,
18203
+ appSettingsMemoryRequests,
18204
+ tokenUsage,
18205
+ promptBudget
18206
+ };
18207
+ }
18208
+ 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") {
18209
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
18210
+ }
18211
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
18212
+ Buffer.from(chatKeyBytes).toString("base64url"),
17999
18213
  chatId,
18000
- turnId: savedTurnId,
18001
- jobId: recoveryJobId,
18002
- assistantMessageId: assistantId,
18003
18214
  keyVersion
18215
+ );
18216
+ let envelope;
18217
+ try {
18218
+ envelope = JSON.parse(claim.sealed_payload);
18219
+ } catch {
18220
+ throw new Error("Recovery job contained an invalid sealed envelope.");
18004
18221
  }
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
18222
+ const plaintext = await openChatCompletionRecoveryEnvelope(
18223
+ envelope,
18224
+ {
18225
+ recoveryPrivateKey: recoveryKeypair.privateKey,
18226
+ ownerId: ownerId2,
18227
+ chatId,
18228
+ turnId: savedTurnId,
18229
+ jobId: recoveryJobId,
18230
+ assistantMessageId: assistantId,
18231
+ keyVersion
18232
+ }
18233
+ );
18234
+ let recovered;
18235
+ try {
18236
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
18237
+ } catch {
18238
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
18059
18239
  }
18060
- };
18061
- const persistTerminalRecovery = async (expectedMessagesV) => {
18062
- const persistedPromise = ws.waitForMessage(
18063
- "recovery_job_persisted",
18064
- (payload) => payload.job_id === recoveryJobId,
18065
- 2e4
18240
+ const expectedRecoveryFields = [
18241
+ "assistant_message_id",
18242
+ "category",
18243
+ "chat_id",
18244
+ "content",
18245
+ "job_id",
18246
+ "key_version",
18247
+ "model_name",
18248
+ "turn_id"
18249
+ ];
18250
+ if (Object.keys(recovered).filter((field3) => ![
18251
+ "thinking_content",
18252
+ "thinking_signature",
18253
+ "has_thinking",
18254
+ "thinking_token_count",
18255
+ "thinking_duration_seconds"
18256
+ ].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") {
18257
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
18258
+ }
18259
+ assistant = recovered.content;
18260
+ category = recovered.category;
18261
+ modelName = recovered.model_name;
18262
+ const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
18263
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
18264
+ assistant,
18265
+ chatKeyBytes
18066
18266
  );
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
- }
18267
+ const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
18268
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
18269
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
18270
+ const terminalPersistPayload = {
18271
+ protocol_version: 1,
18272
+ job_id: recoveryJobId,
18273
+ lease_token: leaseToken,
18274
+ lease_generation: leaseGeneration,
18275
+ encrypted_assistant_message: {
18276
+ client_message_id: assistantId,
18277
+ chat_id: chatId,
18278
+ encrypted_content: encryptedAssistantContent,
18279
+ ...await this.encryptThinkingFields(recovered, chatKeyBytes),
18280
+ encrypted_sender_name: encryptedSenderName,
18281
+ encrypted_category: encryptedCategory,
18282
+ encrypted_model_name: encryptedModelName,
18283
+ role: "assistant",
18284
+ user_message_id: messageId2,
18285
+ created_at: completedAt,
18286
+ updated_at: completedAt
18098
18287
  }
18099
- if (!terminalPersisted) {
18100
- throw new Error(`${error.message} (${error.code})`);
18288
+ };
18289
+ const persistTerminalRecovery = async (expectedMessagesV) => {
18290
+ const persistedPromise = ws.waitForMessage(
18291
+ "recovery_job_persisted",
18292
+ (payload) => payload.job_id === recoveryJobId,
18293
+ 2e4
18294
+ );
18295
+ await ws.sendAsync("recovery_job_persist", {
18296
+ ...terminalPersistPayload,
18297
+ expected_messages_v: expectedMessagesV
18298
+ });
18299
+ await persistedPromise;
18300
+ };
18301
+ let terminalPersisted = false;
18302
+ try {
18303
+ await persistTerminalRecovery(terminalExpectedMessagesV);
18304
+ terminalPersisted = true;
18305
+ } catch (error) {
18306
+ if (error instanceof WebSocketProtocolError) {
18307
+ if (error.code === "version_conflict") {
18308
+ const latestCache = await this.ensureSynced(
18309
+ true,
18310
+ [chatId],
18311
+ { teamId },
18312
+ false,
18313
+ void 0,
18314
+ void 0,
18315
+ { skipTypedRecoveryOutputReplay: true }
18316
+ );
18317
+ const latestChat = latestCache.chats.find(
18318
+ (chat) => String(chat.details.id ?? "") === chatId
18319
+ );
18320
+ const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
18321
+ if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
18322
+ await persistTerminalRecovery(latestMessagesV);
18323
+ terminalExpectedMessagesV = latestMessagesV;
18324
+ terminalPersisted = true;
18325
+ }
18326
+ }
18327
+ if (!terminalPersisted) {
18328
+ throw new Error(`${error.message} (${error.code})`);
18329
+ }
18330
+ } else {
18331
+ throw error;
18101
18332
  }
18102
- } else {
18103
- throw error;
18104
18333
  }
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({
18334
+ if (!terminalPersisted) {
18335
+ throw new Error("Encrypted completion recovery did not persist terminal state.");
18336
+ }
18337
+ assistantMessageId = assistantId;
18338
+ await this.persistStreamedEmbeds({
18123
18339
  ws,
18124
- jobs: pendingTaskUpdateJobs,
18125
- events: taskEvents,
18126
- teamId,
18127
- activeChatId: chatId,
18128
- activeChatKeyBytes: chatKeyBytes,
18129
- fallbackUserMessageId: messageId2,
18130
- requireActiveTurnEvent: true
18340
+ embeds: resp.embeds,
18341
+ chatId,
18342
+ chatKeyBytes,
18343
+ fallbackMessageId: assistantId,
18344
+ ownerId: ownerId2,
18345
+ teamId
18131
18346
  });
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 {
18347
+ if (initialHeaderWork) persistOptionalHeader = () => initialHeaderWork;
18348
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18349
+ if (taskUpdateJobsEnabled) {
18350
+ const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
18351
+ ws,
18352
+ jobs: pendingTaskUpdateJobs,
18353
+ events: taskEvents,
18354
+ teamId,
18355
+ activeChatId: chatId,
18356
+ activeChatKeyBytes: chatKeyBytes,
18357
+ fallbackUserMessageId: messageId2,
18358
+ requireActiveTurnEvent: true
18359
+ });
18360
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
18361
+ }
18362
+ await persistTaskEventSystemMessages(taskEvents);
18363
+ await recoverCurrentTurnOutputs();
18364
+ clearSyncCache(teamId);
18365
+ if (optionalMetadata) {
18366
+ metadataHandoffReady = true;
18367
+ metadataHandoffSuggestion = resp.suggestedFollowupMessage;
18156
18368
  }
18157
18369
  }
18158
- await chatContextEvents?.flush();
18159
18370
  } finally {
18160
- projectFocusCountdown?.();
18161
- chatContextEvents?.stop();
18162
- if (!metadataHandoffReady) ws.close();
18371
+ try {
18372
+ await projectFocusCountdown?.flush();
18373
+ await focusPersistence;
18374
+ if (projectAuthoringAvailable && assistant && chatContextEvents) {
18375
+ try {
18376
+ const history = boundedAuthoringHistory([
18377
+ ...messageHistoryForRequest ?? [],
18378
+ { role: "user", content: finalMessage },
18379
+ { role: "assistant", content: assistant }
18380
+ ]);
18381
+ const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
18382
+ for (const event of recommendations) chatContextEvents.persist(event);
18383
+ } catch {
18384
+ }
18385
+ }
18386
+ await chatContextEvents?.flush();
18387
+ } finally {
18388
+ projectFocusCountdown?.();
18389
+ chatContextEvents?.stop();
18390
+ if (!metadataHandoffReady) ws.close();
18391
+ }
18392
+ }
18393
+ if (focusPersistenceError) throw focusPersistenceError;
18394
+ if (metadataHandoffReady && optionalMetadata) {
18395
+ metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
18396
+ if (!metadataTransportOwned) ws.close();
18163
18397
  }
18164
18398
  }
18165
- if (focusPersistenceError) throw focusPersistenceError;
18166
- if (metadataHandoffReady && optionalMetadata) {
18167
- metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
18168
- if (!metadataTransportOwned) ws.close();
18399
+ const mateName = category ? MATE_NAMES[category] ?? null : null;
18400
+ if (params.interactiveHuman && shouldWaitForAi && !params.incognito) this.rearmSendSocketWhenClosed(ws, teamId);
18401
+ return {
18402
+ status: "completed",
18403
+ chatId,
18404
+ messageId: assistantMessageId,
18405
+ userMessageId: messageId2,
18406
+ assistant,
18407
+ category,
18408
+ modelName,
18409
+ mateName,
18410
+ followUpSuggestions,
18411
+ suggestedFollowupMessage,
18412
+ taskProposals,
18413
+ taskUpdateProposals,
18414
+ taskEvents,
18415
+ pendingTaskUpdateJobs,
18416
+ subChatEvents,
18417
+ appSettingsMemoryRequests,
18418
+ tokenUsage,
18419
+ promptBudget
18420
+ };
18421
+ } finally {
18422
+ stopProjectCatalogResponder();
18423
+ removeContextAckWatcher?.();
18424
+ earlyContextAbort.abort();
18425
+ if (!metadataTransportOwned) {
18426
+ initialHeaderStopped = true;
18427
+ optionalMetadata?.stop();
18428
+ ws.close();
18169
18429
  }
18430
+ if (earlyContext) await earlyContext;
18170
18431
  }
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;
18432
+ } catch (error) {
18433
+ memorySocket?.ws.close();
18434
+ throw error;
18201
18435
  }
18202
- } catch (error) {
18203
- memorySocket?.ws.close();
18204
- throw error;
18436
+ } finally {
18437
+ historyAbort.abort();
18205
18438
  }
18206
18439
  } finally {
18207
- historyAbort.abort();
18440
+ this.foregroundSendCount--;
18441
+ this.maybeRearmSendSocket();
18208
18442
  }
18209
18443
  }
18210
18444
  async persistEncryptedSystemMessage(ws, systemMessage, targetChatId) {
@@ -18439,6 +18673,30 @@ var OpenMatesClient = class _OpenMatesClient {
18439
18673
  return handledJobIds;
18440
18674
  }
18441
18675
  async persistStreamedEmbeds(params) {
18676
+ const assertOwner = this.captureRecoveryOwner();
18677
+ const ws = {
18678
+ waitForMessage: (...args) => {
18679
+ assertOwner();
18680
+ const result = params.ws.waitForMessage(...args).then((value) => {
18681
+ assertOwner();
18682
+ return value;
18683
+ });
18684
+ void result.catch(() => {
18685
+ });
18686
+ return result;
18687
+ },
18688
+ sendAsync: async (...args) => {
18689
+ assertOwner();
18690
+ await params.ws.sendAsync(...args);
18691
+ assertOwner();
18692
+ }
18693
+ };
18694
+ const http = { get: async (path2, headers) => {
18695
+ assertOwner();
18696
+ const result = await this.http.get(path2, headers);
18697
+ assertOwner();
18698
+ return result;
18699
+ } };
18442
18700
  const finalized = new Map(
18443
18701
  params.embeds.filter((embed) => {
18444
18702
  const status = embed.status ?? "finished";
@@ -18449,7 +18707,7 @@ var OpenMatesClient = class _OpenMatesClient {
18449
18707
  for (const parent of finalized.values()) {
18450
18708
  for (const childId of parent.embed_ids ?? []) {
18451
18709
  if (finalized.has(childId)) continue;
18452
- const childResponse = params.ws.waitForMessage(
18710
+ const childResponse = ws.waitForMessage(
18453
18711
  "send_embed_data",
18454
18712
  (payload) => {
18455
18713
  const frame = payload;
@@ -18457,7 +18715,8 @@ var OpenMatesClient = class _OpenMatesClient {
18457
18715
  return nested.embed_id === childId;
18458
18716
  }
18459
18717
  );
18460
- await params.ws.sendAsync("request_embed", { embed_id: childId });
18718
+ assertOwner();
18719
+ await ws.sendAsync("request_embed", { embed_id: childId });
18461
18720
  const envelope = (await childResponse).payload;
18462
18721
  const child = envelope.payload ?? envelope;
18463
18722
  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 +18730,6 @@ var OpenMatesClient = class _OpenMatesClient {
18471
18730
  }
18472
18731
  const masterKey = this.getMasterKeyBytes();
18473
18732
  const parentKeys = /* @__PURE__ */ new Map();
18474
- const processed = /* @__PURE__ */ new Set();
18475
18733
  const cachedEmbeds = loadSyncCache();
18476
18734
  const resolveCachedParentKey = async (parentId) => {
18477
18735
  if (!cachedEmbeds) return void 0;
@@ -18489,6 +18747,7 @@ var OpenMatesClient = class _OpenMatesClient {
18489
18747
  ) ?? void 0;
18490
18748
  };
18491
18749
  const persistOne = async (embed, embedKey, isChild) => {
18750
+ assertOwner();
18492
18751
  const now = Math.floor(Date.now() / 1e3);
18493
18752
  const createdAt = normalizeUnixSeconds(embed.createdAt, now);
18494
18753
  const updatedAt = normalizeUnixSeconds(embed.updatedAt, now);
@@ -18528,14 +18787,15 @@ var OpenMatesClient = class _OpenMatesClient {
18528
18787
  }
18529
18788
  ] : [];
18530
18789
  const storeRequestId = randomUUID9();
18531
- const storeConfirmed = params.ws.waitForMessage(
18790
+ const storeConfirmed = ws.waitForMessage(
18532
18791
  "store_embed_confirmed",
18533
18792
  (payload) => matchesRecoveryEmbedReceipt(payload, storeRequestId, embed.embed_id),
18534
18793
  3e4
18535
18794
  );
18536
18795
  void storeConfirmed.catch(() => {
18537
18796
  });
18538
- await params.ws.sendAsync("store_embed", {
18797
+ assertOwner();
18798
+ await ws.sendAsync("store_embed", {
18539
18799
  request_id: storeRequestId,
18540
18800
  embed_id: embed.embed_id,
18541
18801
  encrypted_type: encryptedType,
@@ -18566,10 +18826,11 @@ var OpenMatesClient = class _OpenMatesClient {
18566
18826
  let storedHead = true;
18567
18827
  try {
18568
18828
  await storeConfirmed;
18829
+ assertOwner();
18569
18830
  } catch (error) {
18570
18831
  if (!(error instanceof WebSocketProtocolError) || error.code !== "embed_write_denied") throw error;
18571
18832
  const query = params.teamId ? `?team_id=${encodeURIComponent(params.teamId)}` : "";
18572
- const canonical = await this.http.get(
18833
+ const canonical = await http.get(
18573
18834
  `/v1/embeds/chats/${encodeURIComponent(params.chatId)}/embeds/${encodeURIComponent(embed.embed_id)}${query}`,
18574
18835
  this.getCliRequestHeaders()
18575
18836
  );
@@ -18634,14 +18895,15 @@ var OpenMatesClient = class _OpenMatesClient {
18634
18895
  const expectedEncryptedType = head.encrypted_type;
18635
18896
  const expectedDigest = computeSHA256(expectedCiphertext);
18636
18897
  const requestId = randomUUID9();
18637
- const confirmed = params.ws.waitForMessage(
18898
+ const confirmed = ws.waitForMessage(
18638
18899
  "store_embed_confirmed",
18639
18900
  (payload) => matchesRecoveryEmbedReceipt(payload, requestId, embed.embed_id),
18640
18901
  3e4
18641
18902
  );
18642
18903
  void confirmed.catch(() => {
18643
18904
  });
18644
- await params.ws.sendAsync("store_embed", {
18905
+ assertOwner();
18906
+ await ws.sendAsync("store_embed", {
18645
18907
  request_id: requestId,
18646
18908
  embed_id: embed.embed_id,
18647
18909
  chat_id: params.chatId,
@@ -18661,7 +18923,7 @@ var OpenMatesClient = class _OpenMatesClient {
18661
18923
  if (receipt.canonical_source !== "head" || receipt.canonical_digest !== expectedDigest) {
18662
18924
  throw new Error(`Embed ${embed.embed_id} metadata enrichment receipt does not match canonical head.`);
18663
18925
  }
18664
- const enriched = await this.http.get(
18926
+ const enriched = await http.get(
18665
18927
  `/v1/embeds/chats/${encodeURIComponent(params.chatId)}/embeds/${encodeURIComponent(embed.embed_id)}${query}`,
18666
18928
  this.getCliRequestHeaders()
18667
18929
  );
@@ -18674,7 +18936,7 @@ var OpenMatesClient = class _OpenMatesClient {
18674
18936
  }
18675
18937
  if (storedHead && keys2.length > 0) {
18676
18938
  const keysRequestId = randomUUID9();
18677
- const keysConfirmed = params.ws.waitForMessage(
18939
+ const keysConfirmed = ws.waitForMessage(
18678
18940
  "store_embed_keys_confirmed",
18679
18941
  (payload) => {
18680
18942
  const p = payload;
@@ -18682,8 +18944,10 @@ var OpenMatesClient = class _OpenMatesClient {
18682
18944
  },
18683
18945
  3e4
18684
18946
  );
18685
- await params.ws.sendAsync("store_embed_keys", { request_id: keysRequestId, keys: keys2 });
18947
+ assertOwner();
18948
+ await ws.sendAsync("store_embed_keys", { request_id: keysRequestId, keys: keys2 });
18686
18949
  const keysReceipt = (await keysConfirmed).payload;
18950
+ assertOwner();
18687
18951
  if (keysReceipt.failed_count !== 0 || keysReceipt.created_count !== keys2.length) {
18688
18952
  throw new Error(`Embed ${embed.embed_id} key wrappers were not durably stored.`);
18689
18953
  }
@@ -18698,7 +18962,7 @@ var OpenMatesClient = class _OpenMatesClient {
18698
18962
  if (row3.version_number === 1) {
18699
18963
  const query = new URLSearchParams({ capability: "bounded-v1", chat_id: params.chatId });
18700
18964
  if (params.teamId) query.set("team_id", params.teamId);
18701
- const existing = await this.http.get(
18965
+ const existing = await http.get(
18702
18966
  `/v1/embeds/${encodeURIComponent(embed.embed_id)}/versions/1?${query}`,
18703
18967
  this.getCliRequestHeaders()
18704
18968
  );
@@ -18721,7 +18985,7 @@ var OpenMatesClient = class _OpenMatesClient {
18721
18985
  const encryptedPatch = typeof row3.patch === "string" ? await encryptWithAesGcmCombined(row3.patch, embedKey) : void 0;
18722
18986
  if (!encryptedSnapshot && !encryptedPatch) continue;
18723
18987
  const diffRequestId = randomUUID9();
18724
- const diffConfirmed = params.ws.waitForMessage(
18988
+ const diffConfirmed = ws.waitForMessage(
18725
18989
  "store_embed_diff_confirmed",
18726
18990
  (payload) => {
18727
18991
  const p = payload;
@@ -18731,7 +18995,8 @@ var OpenMatesClient = class _OpenMatesClient {
18731
18995
  );
18732
18996
  void diffConfirmed.catch(() => {
18733
18997
  });
18734
- await params.ws.sendAsync("store_embed_diff", {
18998
+ assertOwner();
18999
+ await ws.sendAsync("store_embed_diff", {
18735
19000
  request_id: diffRequestId,
18736
19001
  embed_id: row3.embed_id,
18737
19002
  version_number: row3.version_number,
@@ -18741,6 +19006,7 @@ var OpenMatesClient = class _OpenMatesClient {
18741
19006
  created_at: normalizeUnixSeconds(row3.created_at, now)
18742
19007
  });
18743
19008
  const diffReceipt = (await diffConfirmed).payload;
19009
+ assertOwner();
18744
19010
  const expectedDigest = computeSHA256(JSON.stringify([encryptedSnapshot ?? null, encryptedPatch ?? null]));
18745
19011
  if (diffReceipt.canonical_source !== "version_row" || diffReceipt.canonical_digest !== expectedDigest) {
18746
19012
  throw new Error(`Embed ${embed.embed_id} v${row3.version_number} durable receipt did not match the stored source.`);
@@ -18748,34 +19014,19 @@ var OpenMatesClient = class _OpenMatesClient {
18748
19014
  }
18749
19015
  }
18750
19016
  };
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;
19017
+ await runRecoveryBatch([...finalized.values()], async (embed) => {
19018
+ assertOwner();
18770
19019
  const parentId = embed.parent_embed_id || null;
18771
19020
  const parentKey = parentId ? parentKeys.get(parentId) ?? await resolveCachedParentKey(parentId) : void 0;
19021
+ assertOwner();
18772
19022
  if (parentId && !parentKey) {
18773
19023
  throw new Error(`Cannot persist child embed ${embed.embed_id}: parent embed key ${parentId} is unavailable.`);
18774
19024
  }
18775
19025
  const embedKey = parentKey ?? await deriveEmbedKeyFromChatKey(params.chatKeyBytes, embed.embed_id);
19026
+ assertOwner();
18776
19027
  await persistOne(embed, embedKey, Boolean(parentId));
18777
- processed.add(embed.embed_id);
18778
- }
19028
+ assertOwner();
19029
+ }, (embed, dependency) => embed.parent_embed_id === dependency.embed_id);
18779
19030
  }
18780
19031
  async persistPostProcessingMetadata(params) {
18781
19032
  const encryptedFollowUps = params.followUpSuggestions.length > 0 ? await encryptWithAesGcmCombined(
@@ -23103,6 +23354,7 @@ Required: ${schema.required.join(", ")}`
23103
23354
  interactiveViewerAllowed = false;
23104
23355
  interactiveViewerGeneration = 0;
23105
23356
  beginInteractiveViewerSession() {
23357
+ this.cancelSendSocketRearm();
23106
23358
  this.interactiveAdmissionEnabled = true;
23107
23359
  this.interactiveAdmissionOwner = null;
23108
23360
  this.interactiveAdmissionAuth = null;
@@ -23110,6 +23362,7 @@ Required: ${schema.required.join(", ")}`
23110
23362
  this.preparedSendSocketAttemptOwner = null;
23111
23363
  }
23112
23364
  endInteractiveViewerSession() {
23365
+ this.cancelSendSocketRearm();
23113
23366
  this.interactiveAdmissionEnabled = false;
23114
23367
  this.interactiveAdmissionOwner = null;
23115
23368
  this.interactiveAdmissionAuth = null;
@@ -42590,6 +42843,20 @@ async function verifyCaddyCoreRoutes(apiUrl, origin) {
42590
42843
  "Access-Control-Request-Headers": "content-type"
42591
42844
  } });
42592
42845
  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");
42846
+ let accountKeyPreflight;
42847
+ try {
42848
+ accountKeyPreflight = await request("/v1/account/chat-encryption-key", { method: "OPTIONS", headers: {
42849
+ Origin: origin,
42850
+ "Access-Control-Request-Method": "GET",
42851
+ "Access-Control-Request-Headers": "content-type"
42852
+ } });
42853
+ } catch {
42854
+ throw new Error("caddy_account_chat_key_preflight_failed");
42855
+ }
42856
+ const accountKeyMethods = accountKeyPreflight.headers.get("access-control-allow-methods")?.split(",").map((value) => value.trim().toUpperCase()) ?? [];
42857
+ if (accountKeyPreflight.status !== 200 || accountKeyPreflight.headers.get("access-control-allow-origin") !== origin || accountKeyPreflight.headers.get("access-control-allow-credentials") !== "true" || !accountKeyMethods.includes("GET")) {
42858
+ throw new Error("caddy_account_chat_key_preflight_failed");
42859
+ }
42593
42860
  const availability = await request("/v1/features/availability");
42594
42861
  if (!availability.ok) throw new Error("caddy_availability_route_failed");
42595
42862
  const features = await availability.json();