openmates 0.27.0-alpha.78 → 0.27.0-alpha.79

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.
@@ -6372,10 +6372,10 @@ function approvedCurrentPlan(plan) {
6372
6372
  function linkedOpenTask(task, chatId, planId) {
6373
6373
  return Boolean(task && task.primary_chat_id === chatId && task.plan_id === planId && OPEN_LINKED_TASK_STATES.has(task.status) && Number.isSafeInteger(task.version) && Number(task.version) >= 1);
6374
6374
  }
6375
- async function collectAcceptedCliPlanContext(client2, chatId, options, ownedTaskRecords = []) {
6375
+ async function collectAcceptedCliPlanContext(client2, chatId, options, ownedTaskRecords = [], plansRead) {
6376
6376
  const candidates = /* @__PURE__ */ new Map();
6377
6377
  try {
6378
- const plans = await client2.listUserPlans({ ...options, chatId, activeOnly: false });
6378
+ const plans = await (plansRead ?? client2.listUserPlans({ ...options, chatId, activeOnly: false }));
6379
6379
  for (const plan of plans.filter((plan2) => plan2.primary_chat_id === chatId && approvedCurrentPlan(plan2)).sort((a, b) => Number(b.updated_at ?? 0) - Number(a.updated_at ?? 0) || a.plan_id.localeCompare(b.plan_id))) candidates.set(plan.plan_id, []);
6380
6380
  } catch {
6381
6381
  }
@@ -6421,11 +6421,36 @@ async function collectAcceptedCliPlanContext(client2, chatId, options, ownedTask
6421
6421
  }
6422
6422
  return null;
6423
6423
  }
6424
- async function collectRelatedCliTaskContext(client2, chatId, options, records, nowSeconds3 = Date.now() / 1e3) {
6424
+ function taskContextOwner(client2) {
6425
+ if (typeof client2.getSession !== "function") return () => {
6426
+ };
6427
+ const identity3 = () => {
6428
+ const session = client2.getSession();
6429
+ return createHash9("sha256").update(JSON.stringify([
6430
+ session.apiUrl,
6431
+ session.hashedEmail,
6432
+ session.sessionId,
6433
+ session.createdAt,
6434
+ session.masterKeyExportedB64,
6435
+ session.activeTeamId
6436
+ ])).digest("hex");
6437
+ };
6438
+ const owner2 = identity3();
6439
+ return () => {
6440
+ try {
6441
+ if (identity3() === owner2) return;
6442
+ } catch {
6443
+ }
6444
+ throw new Error("Task context workspace changed");
6445
+ };
6446
+ }
6447
+ async function collectRelatedCliTaskContext(client2, chatId, options, records, nowSeconds3 = Date.now() / 1e3, requiredIds = /* @__PURE__ */ new Set()) {
6448
+ const assertTaskOwner = taskContextOwner(client2);
6425
6449
  const owned = records.filter((record10) => typeof record10.task_id === "string" && record10.task_id.length > 0 && record10.task_id.length <= 128 && Number.isSafeInteger(record10.version) && Number(record10.version) >= 1);
6426
6450
  const linked = owned.filter((record10) => record10.primary_chat_id === chatId && OPEN_LINKED_TASK_STATES.has(record10.status)).sort((a, b) => a.task_id.localeCompare(b.task_id));
6427
6451
  const dependencies = /* @__PURE__ */ new Set();
6428
- if (typeof client2.getTaskDependencies === "function") await Promise.all(linked.slice(0, 4).map(async (task) => {
6452
+ const dependencySeeds = [...new Map([...linked, ...owned.filter((task) => requiredIds.has(task.task_id))].map((task) => [task.task_id, task])).values()].slice(0, 4);
6453
+ if (typeof client2.getTaskDependencies === "function") await Promise.all(dependencySeeds.map(async (task) => {
6429
6454
  try {
6430
6455
  const result = await client2.getTaskDependencies(task.task_id, options);
6431
6456
  const ref = `task:${task.task_id}`;
@@ -6436,10 +6461,23 @@ async function collectRelatedCliTaskContext(client2, chatId, options, records, n
6436
6461
  } catch {
6437
6462
  }
6438
6463
  }));
6464
+ assertTaskOwner();
6465
+ const presentIds = new Set(owned.map((task) => task.task_id));
6466
+ if (typeof client2.getUserTask === "function") {
6467
+ await Promise.all([...dependencies].filter((id) => !presentIds.has(id)).sort().slice(0, 24).map(async (id) => {
6468
+ try {
6469
+ const row3 = await client2.getUserTask(id, options);
6470
+ assertTaskOwner();
6471
+ if (row3?.task_id === id && Number.isSafeInteger(row3.version) && Number(row3.version) >= 1) owned.push(row3);
6472
+ } catch {
6473
+ }
6474
+ }));
6475
+ }
6476
+ assertTaskOwner();
6439
6477
  const changedAt = (record10) => Number(record10.updated_at ?? record10.created_at ?? 0);
6440
6478
  const priority = (record10) => {
6441
6479
  if (record10.primary_chat_id === chatId && OPEN_LINKED_TASK_STATES.has(record10.status)) return 0;
6442
- if (dependencies.has(record10.task_id)) return 1;
6480
+ if (dependencies.has(record10.task_id) || requiredIds.has(record10.task_id)) return 1;
6443
6481
  if (record10.status === "in_progress") return 2;
6444
6482
  if (["blocked", "done"].includes(record10.status) && changedAt(record10) >= nowSeconds3 - 1800) return 3;
6445
6483
  return 4;
@@ -6462,9 +6500,12 @@ async function collectRelatedCliTaskContext(client2, chatId, options, records, n
6462
6500
  } catch {
6463
6501
  }
6464
6502
  }
6503
+ assertTaskOwner();
6465
6504
  return candidates;
6466
6505
  }
6467
6506
  async function prepareCliJevContext(client2, chatId, options, supplied = {}, text8, discovery = {}) {
6507
+ const assertTaskOwner = taskContextOwner(client2);
6508
+ const plansRead = discovery.sdkMintedChatId ? Promise.resolve([]) : typeof client2.listUserPlans === "function" ? client2.listUserPlans({ ...options, chatId, activeOnly: false }).catch(() => []) : Promise.resolve([]);
6468
6509
  let candidates = [];
6469
6510
  let active = {};
6470
6511
  let tasks = [];
@@ -6485,18 +6526,39 @@ async function prepareCliJevContext(client2, chatId, options, supplied = {}, tex
6485
6526
  discovery.onProjectRoutingError?.(error);
6486
6527
  }),
6487
6528
  (async () => {
6488
- const records = await client2.listUserTasks({ ...options, limit: 500 });
6489
- ownedTaskRecords = records;
6490
- tasks = await collectRelatedCliTaskContext(client2, chatId, options, records);
6529
+ const taskClient = client2;
6530
+ const [directory, linked] = await Promise.all([
6531
+ taskClient.listUserTaskDiscoveryPage?.(options).catch(() => []) ?? Promise.resolve([]),
6532
+ discovery.sdkMintedChatId ? Promise.resolve([]) : client2.listUserTasks({ ...options, chatId, limit: 100 }).catch(() => [])
6533
+ ]);
6534
+ assertTaskOwner();
6535
+ const records = /* @__PURE__ */ new Map();
6536
+ for (const record10 of [...directory.slice(0, 100), ...linked]) records.set(record10.task_id, record10);
6537
+ const requiredIds = new Set([
6538
+ ...(supplied.related_task_candidates ?? []).map((row3) => row3.task_id)
6539
+ ].filter((id) => typeof id === "string" && id.length > 0 && id.length <= 128).slice(0, 24));
6540
+ if (typeof client2.getUserTask === "function") await Promise.all([...requiredIds].filter((id) => !records.has(id)).map(async (id) => {
6541
+ try {
6542
+ const record10 = await client2.getUserTask(id, options);
6543
+ assertTaskOwner();
6544
+ if (record10?.task_id === id) records.set(id, record10);
6545
+ } catch {
6546
+ }
6547
+ }));
6548
+ assertTaskOwner();
6549
+ ownedTaskRecords = [...records.values()];
6550
+ tasks = await collectRelatedCliTaskContext(client2, chatId, options, ownedTaskRecords, Date.now() / 1e3, requiredIds);
6491
6551
  })().catch(() => {
6492
6552
  })
6493
6553
  ]);
6494
- const acceptedPlan = discovery.sdkMintedChatId ? null : await collectAcceptedCliPlanContext(client2, chatId, options, ownedTaskRecords).catch(() => null);
6554
+ assertTaskOwner();
6555
+ const acceptedPlan = discovery.sdkMintedChatId ? null : await collectAcceptedCliPlanContext(client2, chatId, options, ownedTaskRecords, plansRead).catch(() => null);
6556
+ assertTaskOwner();
6495
6557
  const catalog = await catalogReady;
6496
6558
  discovery.onRouting?.({
6497
6559
  accepted_plan_context: acceptedPlan,
6498
6560
  project_focus_candidates: (discovery.verifiedProjectCandidates ?? supplied.project_focus_candidates ?? candidates).slice(0, 40),
6499
- related_task_candidates: (supplied.related_task_candidates ?? tasks).slice(0, 24),
6561
+ related_task_candidates: tasks.slice(0, 24),
6500
6562
  project_focus_catalog: catalog.slice(0, 20)
6501
6563
  });
6502
6564
  await activeReady;
@@ -6518,10 +6580,11 @@ async function prepareCliJevContext(client2, chatId, options, supplied = {}, tex
6518
6580
  documentIds.add(identity3);
6519
6581
  customDocuments.push(rule);
6520
6582
  }
6583
+ assertTaskOwner();
6521
6584
  return {
6522
6585
  accepted_plan_context: acceptedPlan,
6523
6586
  project_focus_candidates: (discovery.verifiedProjectCandidates ?? supplied.project_focus_candidates ?? candidates).slice(0, 40),
6524
- related_task_candidates: (supplied.related_task_candidates ?? tasks).slice(0, 24),
6587
+ related_task_candidates: tasks.slice(0, 24),
6525
6588
  custom_memory_documents: customDocuments,
6526
6589
  project_focus_catalog: current2 ? (supplied.project_focus_catalog ?? loaded.project_focus_catalog ?? []).slice(0, 20) : [],
6527
6590
  project_focus_documents: current2 ? (supplied.project_focus_documents ?? loaded.project_focus_documents ?? []).slice(0, 20) : [],
@@ -11254,6 +11317,7 @@ var OpenMatesClient = class _OpenMatesClient {
11254
11317
  ownTeamDisplayNames = /* @__PURE__ */ new Map();
11255
11318
  pendingWhoAmIAuthHandoff = null;
11256
11319
  personalMemoryAuthHandoff = null;
11320
+ scopedChatHistory = /* @__PURE__ */ new WeakMap();
11257
11321
  personalMemoryReadInFlight = null;
11258
11322
  interactiveAdmissionEnabled = false;
11259
11323
  interactiveAdmissionOwner = null;
@@ -14541,8 +14605,11 @@ var OpenMatesClient = class _OpenMatesClient {
14541
14605
  const accountCurrent = this.captureTeamAccountOwner().isCurrent;
14542
14606
  const teamId = this.resolveTeamContext(options);
14543
14607
  const masterKey = Buffer.from(this.getMasterKeyBytes());
14544
- const localCache = options.preferCache ? loadSyncCache(teamId) : null;
14545
- let cache = localCache ?? (options.preferCache ? {
14608
+ if (options.freshChatOnly && !CANONICAL_UUID_PATTERN.test(query)) {
14609
+ throw new Error("Fresh scoped history requires a full chat UUID.");
14610
+ }
14611
+ const localCache = options.preferCache || options.freshChatOnly ? loadSyncCache(teamId) : null;
14612
+ let cache = localCache ?? (options.preferCache || options.freshChatOnly ? {
14546
14613
  syncedAt: 0,
14547
14614
  totalChatCount: 0,
14548
14615
  loadedChatCount: 0,
@@ -14560,7 +14627,28 @@ var OpenMatesClient = class _OpenMatesClient {
14560
14627
  const wrappingKey = localTeamKey ? base64ToBytes(localTeamKey) : await this.getChatWrappingKey(teamId, masterKey);
14561
14628
  let found;
14562
14629
  let fetchedMetadata = false;
14563
- try {
14630
+ if (options.freshChatOnly) {
14631
+ const response = await this.http.post(
14632
+ this.appendTeamQuery("/v1/chats/metadata/batch", { teamId }),
14633
+ { chat_ids: [query] },
14634
+ this.getCliRequestHeaders()
14635
+ );
14636
+ current2();
14637
+ options.signal?.throwIfAborted();
14638
+ if (!response.ok) throw new Error(`Chat metadata failed with HTTP ${response.status}`);
14639
+ const details = response.data.chats.find((chat) => String(chat.id ?? "") === query);
14640
+ if (!details) throw new Error(`Chat '${query}' not found.`);
14641
+ found = { details, messages: [] };
14642
+ fetchedMetadata = true;
14643
+ cache = {
14644
+ ...cache,
14645
+ chats: [...cache.chats.filter((chat) => String(chat.details.id ?? "") !== query), found],
14646
+ chatKeyWrappers: [
14647
+ ...cache.chatKeyWrappers ?? [],
14648
+ ...Array.isArray(details.chat_key_wrappers) ? details.chat_key_wrappers : []
14649
+ ]
14650
+ };
14651
+ } else try {
14564
14652
  found = await this.resolveCachedChatForQuery(query, cache, wrappingKey, teamId);
14565
14653
  } catch (error) {
14566
14654
  if (!options.preferCache || !(error instanceof Error) || !/^Chat '.*' not found\./.test(error.message)) throw error;
@@ -14582,7 +14670,7 @@ var OpenMatesClient = class _OpenMatesClient {
14582
14670
  ] };
14583
14671
  }
14584
14672
  const foundChatId = String(found.details.id ?? "");
14585
- if (!options.preferCache && foundChatId && getClientMessagesVersionForSync(found) === 0) {
14673
+ if (!options.preferCache && !options.freshChatOnly && foundChatId && getClientMessagesVersionForSync(found) === 0) {
14586
14674
  cache = await this.ensureSynced(true, [foundChatId], options, true);
14587
14675
  found = cache.chats.find((c) => String(c.details.id ?? "") === foundChatId) ?? found;
14588
14676
  }
@@ -14668,7 +14756,11 @@ var OpenMatesClient = class _OpenMatesClient {
14668
14756
  const messages = coalesceMemoryRequestMessages(decoded);
14669
14757
  current2();
14670
14758
  if (options.preferCache && rawMessages !== found.messages) persistMessages(rawMessages);
14671
- return { chat: chatItem, messages, ...historyIncomplete ? { historyIncomplete: true } : {} };
14759
+ const result = { chat: chatItem, messages, ...historyIncomplete ? { historyIncomplete: true } : {} };
14760
+ if (options.freshChatOnly) {
14761
+ this.scopedChatHistory.set(result, { ...cache, chats: cache.chats.map((chat) => chat === found ? { ...found, messages: rawMessages } : chat) });
14762
+ }
14763
+ return result;
14672
14764
  }
14673
14765
  /** Warm the latest recent windows in the background; only ciphertext reaches disk. */
14674
14766
  async cacheRecentChatMessages(options = {}) {
@@ -16176,15 +16268,10 @@ var OpenMatesClient = class _OpenMatesClient {
16176
16268
  async replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId) {
16177
16269
  const isActive = (output) => output.root_chat_id === chatId || output.target_chat_id === chatId;
16178
16270
  const active = pages.map((page) => page.filter(isActive));
16179
- const unrelated = pages.map((page) => page.filter((output) => !isActive(output)));
16180
16271
  if (active.some((page) => page.length > 0)) {
16181
16272
  await this.replayRecoveryOutputPages(ws, ownerId2, active, cache, teamId);
16182
16273
  clearSyncCache(teamId);
16183
16274
  }
16184
- if (unrelated.some((page) => page.length > 0)) {
16185
- await this.replayRecoveryOutputPages(ws, ownerId2, unrelated, cache, teamId, () => {
16186
- });
16187
- }
16188
16275
  }
16189
16276
  async sendMessage(params) {
16190
16277
  const preWsStartedAt = performance.now();
@@ -16224,1579 +16311,1506 @@ var OpenMatesClient = class _OpenMatesClient {
16224
16311
  } else {
16225
16312
  chatId = params.chatId;
16226
16313
  }
16227
- let finalMessage = params.message;
16228
- if (hasRememberMessageReference(finalMessage)) {
16229
- const rememberedHistory = (params.messageHistory ?? []).map((message) => ({ id: message.message_id, content: message.content })).filter((message) => Boolean(message.id) && typeof message.content === "string");
16230
- finalMessage = rewriteRememberMessageReferences(finalMessage, rememberedHistory);
16231
- }
16232
- if (params.chatId && !params.incognito && hasRememberMessageReference(finalMessage)) {
16233
- const { messages } = await this.getChatMessages(chatId, { personal: !teamId, teamId });
16234
- finalMessage = rewriteRememberMessageReferences(finalMessage, messages);
16235
- }
16236
- const shouldWaitForAi = shouldWaitForTeamAi(finalMessage, teamId);
16237
- const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
16238
- const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
16239
- const sendSocketOptions = {
16240
- taskUpdateJobs: taskUpdateJobsEnabled,
16241
- projectFileJobs: !params.incognito,
16242
- remoteCommandJobs: !params.incognito,
16243
- interactiveHuman: params.interactiveHuman === true && this.interactiveViewerAllowed && process.stdin.isTTY === true && process.stdout.isTTY === true
16314
+ const historyAbort = new AbortController();
16315
+ let savedPrivacyHistoryRead = null;
16316
+ let historySnapshotOwner = null;
16317
+ const readSavedPrivacyHistory = () => {
16318
+ if (!savedPrivacyHistoryRead) {
16319
+ savedPrivacyHistoryRead = withHttpAbortSignal(historyAbort.signal, () => this.getChatMessages(
16320
+ chatId,
16321
+ { personal: !teamId, teamId, freshChatOnly: true, signal: historyAbort.signal }
16322
+ )).then((history) => {
16323
+ historySnapshotOwner = this.preparedChatOwner(this.requireSession(), teamId);
16324
+ return history;
16325
+ });
16326
+ void savedPrivacyHistoryRead.catch(() => {
16327
+ });
16328
+ }
16329
+ return savedPrivacyHistoryRead;
16244
16330
  };
16245
- const independentKeyOwner = this.preparedChatOwner(this.requireSession(), teamId);
16246
- const independentNewChatKey = !params.incognito && !params.chatId ? (async () => {
16247
- if (ownsPreparedId && this.preparedChatKey?.chatId === chatId) {
16248
- const prepared = await this.preparedChatKey.value;
16249
- if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(prepared.wrappingKey)).digest("hex");
16250
- return prepared;
16331
+ const joinSavedPrivacyHistory = async () => {
16332
+ const history = await readSavedPrivacyHistory();
16333
+ if (!historySnapshotOwner || !this.preparedChatOwnerCurrent(historySnapshotOwner, teamId)) {
16334
+ throw new Error("Chat workspace changed during history preparation.");
16251
16335
  }
16252
- const wrappingKey = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
16253
- if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(wrappingKey)).digest("hex");
16254
- const key = new Uint8Array(randomBytes4(32));
16255
- return { key, wrappingKey, encryptedKey: await encryptBytesWithAesGcm(key, wrappingKey) };
16256
- })() : null;
16257
- void independentNewChatKey?.catch(() => {
16258
- });
16259
- const independentRecoveryKeypair = independentNewChatKey?.then((prepared) => deriveChatCompletionRecoveryKeypair(
16260
- Buffer.from(prepared.key).toString("base64url"),
16261
- chatId,
16262
- 1
16263
- ));
16264
- void independentRecoveryKeypair?.catch(() => {
16265
- });
16266
- const privacyTurnId = params.incognito ? null : randomUUID9();
16267
- let conditionalPrivacyReceipt = null;
16268
- let compiledPrivacy;
16269
- let memorySocket = null;
16270
- let socketSnapshotAttempted = false;
16271
- let socketSnapshotResultRows = null;
16272
- let socketProjectCandidateRows = null;
16273
- let verifiedProjectCandidates = null;
16274
- let deferredProjectDirectory = false;
16275
- let usePersonalRoutingDirectory = false;
16276
- let projectCandidatesReadyFromSendStartMs = null;
16277
- let socketMemoryReceipt = null;
16278
- const socketMemoryOptionsKey = () => JSON.stringify([
16279
- this.sendSocketOptionsKey(sendSocketOptions),
16280
- params.projectId ?? null,
16281
- params.teamId ?? null,
16282
- params.personal === true,
16283
- params.incognito === true,
16284
- params.autoApproveMemories === true,
16285
- params.piiDetection !== false,
16286
- params.learningMode?.enabled ?? null,
16287
- params.learningMode?.ageGroup ?? null
16288
- ]);
16289
- const memoryStartedAt = performance.now();
16290
- let availableMemories = params.memorySnapshot ?? [];
16291
- let memoryCountsLoaded = false;
16292
- let memoryMetadataKeys = [];
16293
- if (!params.incognito) {
16294
- try {
16295
- if (!teamId) {
16296
- try {
16297
- socketSnapshotAttempted = true;
16298
- memorySocket = (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient(sendSocketOptions);
16299
- const ownerKey = JSON.stringify([
16300
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16301
- authCookieFingerprint(this.http.getCookieMap())
16302
- ]);
16303
- const conditionalSupported = await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v2");
16304
- const combinedSupported = conditionalSupported || await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v1");
16305
- const supported = combinedSupported || await memorySocket.ws.waitForServerCapability("personal_memory_snapshot_v1");
16306
- if (supported) {
16307
- 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");
16308
- deferredProjectDirectory = deferProjectDirectory;
16309
- usePersonalRoutingDirectory = deferProjectDirectory && memorySocket.ws.supportsServerCapability("personal_project_routing_directory_v1");
16310
- traceSendTiming("snapshot_request");
16311
- const privacyOwnerKey = this.preparedPrivacyOwnerKey(null);
16312
- const preferencesKey = JSON.stringify(await readPrivacyPreferences());
16313
- const preparedBundle = conditionalSupported ? this.preparedPrivacy.get(privacyOwnerKey, preferencesKey) : null;
16314
- const conditional = conditionalSupported ? await memorySocket.ws.requestPersonalSendContextSnapshotV2({
16315
- request_id: randomUUID9(),
16316
- purpose: "send",
16317
- turn_id: privacyTurnId,
16318
- ...preparedBundle ? { known_generation: preparedBundle.generation } : {}
16319
- }) : null;
16320
- const combined = !conditional && combinedSupported && !deferProjectDirectory ? await memorySocket.ws.requestPersonalSendContextSnapshot(randomUUID9()) : null;
16321
- const snapshot2 = conditional ? conditional.personal_memories ?? null : combined?.personal_memories ?? await memorySocket.ws.requestPersonalMemorySnapshot(randomUUID9());
16322
- traceSendTiming("snapshot_ready");
16323
- if (conditional || combined) {
16324
- socketProjectCandidateRows = (conditional ?? combined).project_candidates;
16325
- verifiedProjectCandidates = await decodePersonalProjectCandidates(this, (conditional ?? combined).project_candidates);
16326
- projectCandidatesReadyFromSendStartMs = Math.round(performance.now() - preWsStartedAt);
16327
- }
16328
- if (conditional?.unchanged) {
16329
- if (!preparedBundle || conditional.privacy_generation !== preparedBundle.generation) throw new Error("Unchanged privacy snapshot has no matching prepared bundle.");
16330
- availableMemories = preparedBundle.memories;
16331
- compiledPrivacy = preparedBundle.privacy;
16332
- socketSnapshotResultRows = [];
16333
- } else {
16334
- if (!snapshot2) throw new Error("Full Personal privacy snapshot is required.");
16335
- socketSnapshotResultRows = snapshot2.memories;
16336
- availableMemories = await this.listMemoriesUnshared({ personal: true }, snapshot2);
16336
+ return history;
16337
+ };
16338
+ try {
16339
+ let finalMessage = params.message;
16340
+ if (hasRememberMessageReference(finalMessage)) {
16341
+ const rememberedHistory = (params.messageHistory ?? []).map((message) => ({ id: message.message_id, content: message.content })).filter((message) => Boolean(message.id) && typeof message.content === "string");
16342
+ finalMessage = rewriteRememberMessageReferences(finalMessage, rememberedHistory);
16343
+ }
16344
+ if (params.chatId && !params.incognito && hasRememberMessageReference(finalMessage)) {
16345
+ const { messages } = await joinSavedPrivacyHistory();
16346
+ finalMessage = rewriteRememberMessageReferences(finalMessage, messages);
16347
+ }
16348
+ const shouldWaitForAi = shouldWaitForTeamAi(finalMessage, teamId);
16349
+ const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
16350
+ const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
16351
+ const sendSocketOptions = {
16352
+ taskUpdateJobs: taskUpdateJobsEnabled,
16353
+ projectFileJobs: !params.incognito,
16354
+ remoteCommandJobs: !params.incognito,
16355
+ interactiveHuman: params.interactiveHuman === true && this.interactiveViewerAllowed && process.stdin.isTTY === true && process.stdout.isTTY === true
16356
+ };
16357
+ const independentKeyOwner = this.preparedChatOwner(this.requireSession(), teamId);
16358
+ const independentNewChatKey = !params.incognito && !params.chatId ? (async () => {
16359
+ if (ownsPreparedId && this.preparedChatKey?.chatId === chatId) {
16360
+ const prepared = await this.preparedChatKey.value;
16361
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(prepared.wrappingKey)).digest("hex");
16362
+ return prepared;
16363
+ }
16364
+ const wrappingKey = await this.getChatWrappingKey(teamId, this.getMasterKeyBytes());
16365
+ if (teamId) independentKeyOwner.teamKeyFingerprint = createHash14("sha256").update(bytesToBase64(wrappingKey)).digest("hex");
16366
+ const key = new Uint8Array(randomBytes4(32));
16367
+ return { key, wrappingKey, encryptedKey: await encryptBytesWithAesGcm(key, wrappingKey) };
16368
+ })() : null;
16369
+ void independentNewChatKey?.catch(() => {
16370
+ });
16371
+ const independentRecoveryKeypair = independentNewChatKey?.then((prepared) => deriveChatCompletionRecoveryKeypair(
16372
+ Buffer.from(prepared.key).toString("base64url"),
16373
+ chatId,
16374
+ 1
16375
+ ));
16376
+ void independentRecoveryKeypair?.catch(() => {
16377
+ });
16378
+ const privacyTurnId = params.incognito ? null : randomUUID9();
16379
+ let conditionalPrivacyReceipt = null;
16380
+ let compiledPrivacy;
16381
+ let memorySocket = null;
16382
+ let socketSnapshotAttempted = false;
16383
+ let socketSnapshotResultRows = null;
16384
+ let socketProjectCandidateRows = null;
16385
+ let verifiedProjectCandidates = null;
16386
+ let deferredProjectDirectory = false;
16387
+ let usePersonalRoutingDirectory = false;
16388
+ let projectCandidatesReadyFromSendStartMs = null;
16389
+ let socketMemoryReceipt = null;
16390
+ const socketMemoryOptionsKey = () => JSON.stringify([
16391
+ this.sendSocketOptionsKey(sendSocketOptions),
16392
+ params.projectId ?? null,
16393
+ params.teamId ?? null,
16394
+ params.personal === true,
16395
+ params.incognito === true,
16396
+ params.autoApproveMemories === true,
16397
+ params.piiDetection !== false,
16398
+ params.learningMode?.enabled ?? null,
16399
+ params.learningMode?.ageGroup ?? null
16400
+ ]);
16401
+ if (!params.incognito && params.chatId) readSavedPrivacyHistory();
16402
+ const memoryStartedAt = performance.now();
16403
+ let availableMemories = params.memorySnapshot ?? [];
16404
+ let memoryCountsLoaded = false;
16405
+ let memoryMetadataKeys = [];
16406
+ if (!params.incognito) {
16407
+ try {
16408
+ if (!teamId) {
16409
+ try {
16410
+ socketSnapshotAttempted = true;
16411
+ memorySocket = (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient(sendSocketOptions);
16412
+ const ownerKey = JSON.stringify([
16413
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16414
+ authCookieFingerprint(this.http.getCookieMap())
16415
+ ]);
16416
+ const conditionalSupported = await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v2");
16417
+ const combinedSupported = conditionalSupported || await memorySocket.ws.waitForServerCapability("personal_send_context_snapshot_v1");
16418
+ const supported = combinedSupported || await memorySocket.ws.waitForServerCapability("personal_memory_snapshot_v1");
16419
+ if (supported) {
16420
+ 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");
16421
+ deferredProjectDirectory = deferProjectDirectory;
16422
+ usePersonalRoutingDirectory = deferProjectDirectory && memorySocket.ws.supportsServerCapability("personal_project_routing_directory_v1");
16423
+ traceSendTiming("snapshot_request");
16424
+ const privacyOwnerKey = this.preparedPrivacyOwnerKey(null);
16425
+ const preferencesKey = JSON.stringify(await readPrivacyPreferences());
16426
+ const preparedBundle = conditionalSupported ? this.preparedPrivacy.get(privacyOwnerKey, preferencesKey) : null;
16427
+ const conditional = conditionalSupported ? await memorySocket.ws.requestPersonalSendContextSnapshotV2({
16428
+ request_id: randomUUID9(),
16429
+ purpose: "send",
16430
+ turn_id: privacyTurnId,
16431
+ ...preparedBundle ? { known_generation: preparedBundle.generation } : {}
16432
+ }) : null;
16433
+ const combined = !conditional && combinedSupported && !deferProjectDirectory ? await memorySocket.ws.requestPersonalSendContextSnapshot(randomUUID9()) : null;
16434
+ const snapshot2 = conditional ? conditional.personal_memories ?? null : combined?.personal_memories ?? await memorySocket.ws.requestPersonalMemorySnapshot(randomUUID9());
16435
+ traceSendTiming("snapshot_ready");
16436
+ if (conditional || combined) {
16437
+ socketProjectCandidateRows = (conditional ?? combined).project_candidates;
16438
+ verifiedProjectCandidates = await decodePersonalProjectCandidates(this, (conditional ?? combined).project_candidates);
16439
+ projectCandidatesReadyFromSendStartMs = Math.round(performance.now() - preWsStartedAt);
16440
+ }
16441
+ if (conditional?.unchanged) {
16442
+ if (!preparedBundle || conditional.privacy_generation !== preparedBundle.generation) throw new Error("Unchanged privacy snapshot has no matching prepared bundle.");
16443
+ availableMemories = preparedBundle.memories;
16444
+ compiledPrivacy = preparedBundle.privacy;
16445
+ socketSnapshotResultRows = [];
16446
+ } else {
16447
+ if (!snapshot2) throw new Error("Full Personal privacy snapshot is required.");
16448
+ socketSnapshotResultRows = snapshot2.memories;
16449
+ availableMemories = await this.listMemoriesUnshared({ personal: true }, snapshot2);
16450
+ if (conditional) {
16451
+ this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16452
+ compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16453
+ }
16454
+ }
16337
16455
  if (conditional) {
16338
- this.preparedPrivacy.set(privacyOwnerKey, preferencesKey, conditional.privacy_generation, availableMemories);
16339
- compiledPrivacy = this.preparedPrivacy.get(privacyOwnerKey, preferencesKey)?.privacy;
16456
+ if (!conditional.receipt_id || conditional.turn_id !== privacyTurnId) throw new Error("Conditional privacy snapshot has no valid send receipt.");
16457
+ conditionalPrivacyReceipt = {
16458
+ receiptId: conditional.receipt_id,
16459
+ generation: conditional.privacy_generation,
16460
+ preferencesKey,
16461
+ epoch: this.preparedPrivacyEpoch
16462
+ };
16340
16463
  }
16341
- }
16342
- if (conditional) {
16343
- if (!conditional.receipt_id || conditional.turn_id !== privacyTurnId) throw new Error("Conditional privacy snapshot has no valid send receipt.");
16344
- conditionalPrivacyReceipt = {
16345
- receiptId: conditional.receipt_id,
16346
- generation: conditional.privacy_generation,
16347
- preferencesKey,
16348
- epoch: this.preparedPrivacyEpoch
16464
+ traceSendTiming("memory_decrypt_ready");
16465
+ if (!memorySocket.ws.isOpen() || JSON.stringify([
16466
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16467
+ authCookieFingerprint(this.http.getCookieMap())
16468
+ ]) !== ownerKey) {
16469
+ throw new Error("Personal Memory snapshot owner or socket changed during preparation.");
16470
+ }
16471
+ socketMemoryReceipt = {
16472
+ ws: memorySocket.ws,
16473
+ snapshot: availableMemories,
16474
+ resultRows: socketSnapshotResultRows,
16475
+ projectRows: socketProjectCandidateRows,
16476
+ candidates: verifiedProjectCandidates,
16477
+ ownerKey,
16478
+ optionsKey: socketMemoryOptionsKey(),
16479
+ used: false
16349
16480
  };
16481
+ } else {
16482
+ memorySocket.ws.close();
16483
+ memorySocket = null;
16484
+ socketSnapshotAttempted = false;
16350
16485
  }
16351
- traceSendTiming("memory_decrypt_ready");
16352
- if (!memorySocket.ws.isOpen() || JSON.stringify([
16353
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16354
- authCookieFingerprint(this.http.getCookieMap())
16355
- ]) !== ownerKey) {
16356
- throw new Error("Personal Memory snapshot owner or socket changed during preparation.");
16357
- }
16358
- socketMemoryReceipt = {
16359
- ws: memorySocket.ws,
16360
- snapshot: availableMemories,
16361
- resultRows: socketSnapshotResultRows,
16362
- projectRows: socketProjectCandidateRows,
16363
- candidates: verifiedProjectCandidates,
16364
- ownerKey,
16365
- optionsKey: socketMemoryOptionsKey(),
16366
- used: false
16367
- };
16368
- } else {
16369
- memorySocket.ws.close();
16486
+ } catch (error) {
16487
+ memorySocket?.ws.close();
16370
16488
  memorySocket = null;
16371
- socketSnapshotAttempted = false;
16489
+ throw error;
16372
16490
  }
16373
- } catch (error) {
16374
- memorySocket?.ws.close();
16375
- memorySocket = null;
16376
- throw error;
16377
16491
  }
16378
- }
16379
- if (!socketMemoryReceipt) availableMemories = teamId && params.memorySnapshot ? params.memorySnapshot : await this.listMemoriesUnshared({ teamId }, void 0, !teamId);
16380
- memoryCountsLoaded = true;
16381
- memoryMetadataKeys = [
16382
- ...new Set(
16383
- availableMemories.filter((memory) => memory.app_id && memory.item_type).map((memory) => `${memory.app_id}-${memory.item_type}`)
16384
- )
16385
- ];
16386
- } catch (error) {
16387
- if (socketSnapshotAttempted || !teamId) throw error;
16388
- if (params.autoApproveMemories) {
16389
- const message = error instanceof Error ? error.message : String(error);
16390
- throw new Error(`Failed to load memories for auto-approval: ${message}`);
16492
+ if (!socketMemoryReceipt) availableMemories = teamId && params.memorySnapshot ? params.memorySnapshot : await this.listMemoriesUnshared({ teamId }, void 0, !teamId);
16493
+ memoryCountsLoaded = true;
16494
+ memoryMetadataKeys = [
16495
+ ...new Set(
16496
+ availableMemories.filter((memory) => memory.app_id && memory.item_type).map((memory) => `${memory.app_id}-${memory.item_type}`)
16497
+ )
16498
+ ];
16499
+ } catch (error) {
16500
+ if (socketSnapshotAttempted || !teamId) throw error;
16501
+ if (params.autoApproveMemories) {
16502
+ const message = error instanceof Error ? error.message : String(error);
16503
+ throw new Error(`Failed to load memories for auto-approval: ${message}`);
16504
+ }
16391
16505
  }
16392
16506
  }
16393
- }
16394
- try {
16395
- const snapshotAuthHandoff = this.personalMemoryAuthHandoff?.snapshot.deref() === availableMemories ? this.personalMemoryAuthHandoff.auth : null;
16396
- this.personalMemoryAuthHandoff = null;
16397
- this.pendingWhoAmIAuthHandoff = null;
16398
- const personalSendAuthHandoff = !teamId && !params.incognito ? snapshotAuthHandoff : null;
16399
- const memoryPrepMs = Math.round(performance.now() - memoryStartedAt);
16400
- const privacyStartedAt = performance.now();
16401
- traceSendTiming("privacy_begin");
16402
- const savedPrivacyHistory = !params.incognito && params.chatId ? await this.getChatMessages(chatId, teamId ? { teamId } : { personal: true }) : null;
16403
- let piiMappings = [...savedPrivacyHistory?.messages.flatMap((message) => (message.piiMappings ?? []).map((mapping) => ({ ...mapping, type: mapping.type ?? "OTHER" }))) ?? [], ...params.piiMappings ?? []];
16404
16507
  try {
16405
- if (params.piiDetection !== false) {
16406
- const prepared = await prepareCliMessagePrivacy(finalMessage, availableMemories, piiMappings, { kind: "message" }, params.onPrivacyProgress, compiledPrivacy);
16407
- finalMessage = prepared.message;
16408
- piiMappings = prepared.mappings;
16409
- for (const embed of params.preparedEmbeds ?? []) {
16410
- if (!["code-code", "docs-doc", "sheets-sheet"].includes(embed.type)) continue;
16411
- const decoded = toonDecode(embed.content);
16412
- for (const field3 of ["code", "html", "table", "text"]) if (typeof decoded[field3] === "string") {
16413
- const value = await prepareCliMessagePrivacy(decoded[field3], availableMemories, piiMappings, { kind: "document" }, params.onPrivacyProgress, compiledPrivacy);
16414
- decoded[field3] = value.message;
16415
- piiMappings = value.mappings;
16416
- }
16417
- embed.content = toonEncodeContent(decoded);
16418
- const content = decoded.code ?? decoded.table ?? decoded.html ?? decoded.text;
16419
- if (typeof content === "string") {
16420
- embed.contentHash = createHash14("sha256").update(content).digest("hex");
16421
- embed.textLengthChars = content.length;
16508
+ const snapshotAuthHandoff = this.personalMemoryAuthHandoff?.snapshot.deref() === availableMemories ? this.personalMemoryAuthHandoff.auth : null;
16509
+ this.personalMemoryAuthHandoff = null;
16510
+ this.pendingWhoAmIAuthHandoff = null;
16511
+ const personalSendAuthHandoff = !teamId && !params.incognito ? snapshotAuthHandoff : null;
16512
+ const memoryPrepMs = Math.round(performance.now() - memoryStartedAt);
16513
+ const privacyStartedAt = performance.now();
16514
+ traceSendTiming("privacy_begin");
16515
+ const savedPrivacyHistory = !params.incognito && params.chatId ? await joinSavedPrivacyHistory() : null;
16516
+ let piiMappings = [...savedPrivacyHistory?.messages.flatMap((message) => (message.piiMappings ?? []).map((mapping) => ({ ...mapping, type: mapping.type ?? "OTHER" }))) ?? [], ...params.piiMappings ?? []];
16517
+ try {
16518
+ if (params.piiDetection !== false) {
16519
+ const prepared = await prepareCliMessagePrivacy(finalMessage, availableMemories, piiMappings, { kind: "message" }, params.onPrivacyProgress, compiledPrivacy);
16520
+ finalMessage = prepared.message;
16521
+ piiMappings = prepared.mappings;
16522
+ for (const embed of params.preparedEmbeds ?? []) {
16523
+ if (!["code-code", "docs-doc", "sheets-sheet"].includes(embed.type)) continue;
16524
+ const decoded = toonDecode(embed.content);
16525
+ for (const field3 of ["code", "html", "table", "text"]) if (typeof decoded[field3] === "string") {
16526
+ const value = await prepareCliMessagePrivacy(decoded[field3], availableMemories, piiMappings, { kind: "document" }, params.onPrivacyProgress, compiledPrivacy);
16527
+ decoded[field3] = value.message;
16528
+ piiMappings = value.mappings;
16529
+ }
16530
+ embed.content = toonEncodeContent(decoded);
16531
+ const content = decoded.code ?? decoded.table ?? decoded.html ?? decoded.text;
16532
+ if (typeof content === "string") {
16533
+ embed.contentHash = createHash14("sha256").update(content).digest("hex");
16534
+ embed.textLengthChars = content.length;
16535
+ }
16422
16536
  }
16423
16537
  }
16538
+ params.onPrivacyPrepared?.(finalMessage);
16539
+ } catch (error) {
16540
+ memorySocket?.ws.close();
16541
+ throw error;
16424
16542
  }
16425
- params.onPrivacyPrepared?.(finalMessage);
16426
- } catch (error) {
16427
- memorySocket?.ws.close();
16428
- throw error;
16429
- }
16430
- const privacyPrepMs = Math.round(performance.now() - privacyStartedAt);
16431
- traceSendTiming("privacy_ready");
16432
- const preWsMs = Math.round(performance.now() - preWsStartedAt);
16433
- const contextSession = this.requireSession();
16434
- const {
16435
- apiUrl: contextApiUrl,
16436
- hashedEmail: contextHashedEmail,
16437
- sessionId: contextSessionId,
16438
- createdAt: contextSessionCreatedAt,
16439
- masterKeyExportedB64: contextMasterKey,
16440
- activeTeamId: contextActiveTeamId
16441
- } = contextSession;
16442
- const contextScope = { teamId, personal: !teamId };
16443
- const suppliedContext = deferredProjectDirectory ? { ...params.jevContext, project_focus_candidates: void 0 } : params.jevContext;
16444
- const contextStartedAt = Date.now();
16445
- let publishRouting;
16446
- const routingContext = new Promise((resolve20) => {
16447
- publishRouting = resolve20;
16448
- });
16449
- let publishProjectRouting;
16450
- let rejectProjectRouting;
16451
- const projectRoutingContext = new Promise((resolve20, reject) => {
16452
- publishProjectRouting = resolve20;
16453
- rejectProjectRouting = reject;
16454
- });
16455
- void projectRoutingContext.catch(() => {
16456
- });
16457
- const publishProjectRoutingTimed = (context) => {
16458
- projectCandidatesReadyFromSendStartMs ??= Math.round(performance.now() - preWsStartedAt);
16459
- publishProjectRouting(deferredProjectDirectory ? { ...context, project_focus_candidates: context.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : context);
16460
- };
16461
- let recoveryMs = 0;
16462
- const contextOwnerIsCurrent = () => {
16463
- const current2 = this.session;
16464
- return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
16465
- };
16466
- const earlyContextAbort = new AbortController();
16467
- const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16468
- sdkMintedChatId,
16469
- onRouting: publishRouting,
16470
- verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16471
- usePersonalRoutingDirectory,
16472
- onProjectRouting: publishProjectRoutingTimed,
16473
- onProjectRoutingError: rejectProjectRouting
16474
- })).then(
16475
- (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
16476
- (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
16477
- ) : null;
16478
- const authSocketStartedAt = Date.now();
16479
- let opened;
16480
- try {
16481
- opened = memorySocket ?? (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient({ ...sendSocketOptions, personalSendAuthHandoff });
16482
- } catch (error) {
16483
- earlyContextAbort.abort();
16484
- if (earlyContext) await earlyContext;
16485
- throw error;
16486
- }
16487
- const { ws, ownerId: ownerId2 } = opened;
16488
- const consumeSocketMemoryReceipt = async () => {
16489
- if (!socketMemoryReceipt) return;
16490
- const receipt = socketMemoryReceipt;
16491
- if (receipt.used || receipt.ws !== ws || receipt.snapshot !== availableMemories || receipt.resultRows !== socketSnapshotResultRows || receipt.projectRows !== socketProjectCandidateRows || receipt.candidates !== verifiedProjectCandidates || !ws.isOpen() || receipt.optionsKey !== socketMemoryOptionsKey()) {
16492
- throw new Error("Personal Memory snapshot socket receipt is invalid.");
16493
- }
16494
- await this.withFreshStoredSession(async () => {
16495
- if (!ws.isOpen() || JSON.stringify([
16496
- this.sendSocketOwnerKey(this.requireSession(), teamId),
16497
- authCookieFingerprint(this.http.getCookieMap())
16498
- ]) !== receipt.ownerKey) {
16499
- throw new Error("Personal Memory snapshot owner changed before send.");
16500
- }
16543
+ const privacyPrepMs = Math.round(performance.now() - privacyStartedAt);
16544
+ traceSendTiming("privacy_ready");
16545
+ const preWsMs = Math.round(performance.now() - preWsStartedAt);
16546
+ const contextSession = this.requireSession();
16547
+ const {
16548
+ apiUrl: contextApiUrl,
16549
+ hashedEmail: contextHashedEmail,
16550
+ sessionId: contextSessionId,
16551
+ createdAt: contextSessionCreatedAt,
16552
+ masterKeyExportedB64: contextMasterKey,
16553
+ activeTeamId: contextActiveTeamId
16554
+ } = contextSession;
16555
+ const contextScope = { teamId, personal: !teamId };
16556
+ const suppliedContext = deferredProjectDirectory ? { ...params.jevContext, project_focus_candidates: void 0 } : params.jevContext;
16557
+ const contextStartedAt = Date.now();
16558
+ let publishRouting;
16559
+ const routingContext = new Promise((resolve20) => {
16560
+ publishRouting = resolve20;
16561
+ });
16562
+ let publishProjectRouting;
16563
+ let rejectProjectRouting;
16564
+ const projectRoutingContext = new Promise((resolve20, reject) => {
16565
+ publishProjectRouting = resolve20;
16566
+ rejectProjectRouting = reject;
16501
16567
  });
16502
- if (conditionalPrivacyReceipt && (conditionalPrivacyReceipt.epoch !== this.preparedPrivacyEpoch || conditionalPrivacyReceipt.preferencesKey !== JSON.stringify(await readPrivacyPreferences()))) {
16503
- throw new Error("Privacy preferences changed before send; prepare the turn again.");
16568
+ void projectRoutingContext.catch(() => {
16569
+ });
16570
+ const publishProjectRoutingTimed = (context) => {
16571
+ projectCandidatesReadyFromSendStartMs ??= Math.round(performance.now() - preWsStartedAt);
16572
+ publishProjectRouting(deferredProjectDirectory ? { ...context, project_focus_candidates: context.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : context);
16573
+ };
16574
+ let recoveryMs = 0;
16575
+ const contextOwnerIsCurrent = () => {
16576
+ const current2 = this.session;
16577
+ return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
16578
+ };
16579
+ const earlyContextAbort = new AbortController();
16580
+ const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16581
+ sdkMintedChatId,
16582
+ onRouting: publishRouting,
16583
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16584
+ usePersonalRoutingDirectory,
16585
+ onProjectRouting: publishProjectRoutingTimed,
16586
+ onProjectRoutingError: rejectProjectRouting
16587
+ })).then(
16588
+ (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
16589
+ (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
16590
+ ) : null;
16591
+ const authSocketStartedAt = Date.now();
16592
+ let opened;
16593
+ try {
16594
+ opened = memorySocket ?? (sendSocketOptions.interactiveHuman ? await this.takePreparedSendSocket(sendSocketOptions, teamId) : null) ?? await this.openWsClient({ ...sendSocketOptions, personalSendAuthHandoff });
16595
+ } catch (error) {
16596
+ earlyContextAbort.abort();
16597
+ if (earlyContext) await earlyContext;
16598
+ throw error;
16504
16599
  }
16505
- receipt.used = true;
16506
- };
16507
- let metadataTransportOwned = false;
16508
- let metadataHandoffReady = false;
16509
- let metadataHandoffSuggestion = null;
16510
- let initialHeaderWork = null;
16511
- let initialHeaderNotice = null;
16512
- let initialAdmissionDurable = false;
16513
- let initialHeaderStopped = false;
16514
- let initialTitleScheduled;
16515
- let initialIconScheduled;
16516
- let persistOptionalHeader;
16517
- let optionalMetadata = null;
16518
- const socketReadyForSendAt = performance.now();
16519
- let capabilitiesWaitMs = 0;
16520
- let keyWaitMs = 0;
16521
- let recoveryKeypairMs = 0;
16522
- let encryptPayloadMs = 0;
16523
- if (!params.incognito && !ownerId2) {
16524
- earlyContextAbort.abort();
16525
- ws.close();
16526
- if (earlyContext) await earlyContext;
16527
- throw new Error("Authenticated user identity is required for saved chat recovery.");
16528
- }
16529
- const authSocketMs = Date.now() - authSocketStartedAt;
16530
- const mayDeferRecovery = !params.incognito && sdkMintedChatId && !teamId && !params.projectId && shouldWaitForAi;
16531
- let recoveryDiscovery = null;
16532
- let recoveryDiscoveryError = null;
16533
- let contextAckRejection = null;
16534
- let removeContextAckWatcher = null;
16535
- try {
16536
- if (!params.incognito) {
16537
- try {
16538
- ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
16539
- } catch {
16600
+ const { ws, ownerId: ownerId2 } = opened;
16601
+ const consumeSocketMemoryReceipt = async () => {
16602
+ if (!socketMemoryReceipt) return;
16603
+ const receipt = socketMemoryReceipt;
16604
+ if (receipt.used || receipt.ws !== ws || receipt.snapshot !== availableMemories || receipt.resultRows !== socketSnapshotResultRows || receipt.projectRows !== socketProjectCandidateRows || receipt.candidates !== verifiedProjectCandidates || !ws.isOpen() || receipt.optionsKey !== socketMemoryOptionsKey()) {
16605
+ throw new Error("Personal Memory snapshot socket receipt is invalid.");
16540
16606
  }
16541
- const recoveryStartedAt = Date.now();
16542
- recoveryDiscovery = ws.waitForRecoveryOutputDiscovery().then(
16543
- () => {
16544
- recoveryMs = Date.now() - recoveryStartedAt;
16545
- },
16546
- (error) => {
16547
- recoveryDiscoveryError = error;
16548
- throw error;
16607
+ await this.withFreshStoredSession(async () => {
16608
+ if (!ws.isOpen() || JSON.stringify([
16609
+ this.sendSocketOwnerKey(this.requireSession(), teamId),
16610
+ authCookieFingerprint(this.http.getCookieMap())
16611
+ ]) !== receipt.ownerKey) {
16612
+ throw new Error("Personal Memory snapshot owner changed before send.");
16549
16613
  }
16550
- );
16551
- void recoveryDiscovery.catch(() => {
16552
16614
  });
16553
- if (mayDeferRecovery) {
16554
- const capabilitiesWaitStartedAt = performance.now();
16555
- await Promise.race([ws.waitForServerCapabilities(), recoveryDiscovery]);
16556
- capabilitiesWaitMs = Math.round(performance.now() - capabilitiesWaitStartedAt);
16557
- } else await recoveryDiscovery;
16558
- }
16559
- const messageId2 = randomUUID9();
16560
- const wallCreatedAt = Math.floor(Date.now() / 1e3);
16561
- const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
16562
- const isNewChat = !params.chatId;
16563
- let messageHistoryForRequest = params.messageHistory;
16564
- if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
16565
- const { messages } = savedPrivacyHistory;
16566
- messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
16567
- }
16568
- ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
16569
- const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
16570
- params.connectedAccountDirectory
16571
- );
16572
- const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
16573
- chatId,
16574
- messageId: messageId2,
16575
- refs: params.connectedAccountTokenRefInputs,
16576
- teamId
16577
- }) : [];
16578
- assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
16579
- let chatKeyBytes = null;
16580
- let activeFocusId = null;
16581
- let encryptedChatKey = null;
16582
- let chatSlugLookupKey = null;
16583
- let focusPhaseState = {};
16584
- let baselineMessagesV = 0;
16585
- let notificationTitle = null;
16586
- let terminalExpectedMessagesV = 1;
16587
- let savedTurnId = null;
16588
- if (!params.incognito) {
16589
- traceSendTiming("key_begin");
16590
- const masterKey = this.getMasterKeyBytes();
16591
- const preparedKey = isNewChat && ownsPreparedId && this.preparedChatKey?.chatId === chatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId) ? this.preparedChatKey : null;
16592
- if (preparedKey) this.preparedChatKey = null;
16593
- const keyWaitStartedAt = performance.now();
16594
- const prepared = independentNewChatKey ? await independentNewChatKey : preparedKey ? await preparedKey.value : null;
16595
- if (prepared && !this.preparedChatOwnerCurrent(independentKeyOwner, teamId)) throw new Error("Chat key preparation owner changed before send.");
16596
- const wrappingKey = prepared?.wrappingKey ?? await this.getChatWrappingKey(teamId, masterKey);
16597
- keyWaitMs = Math.round(performance.now() - keyWaitStartedAt);
16598
- chatSlugLookupKey = wrappingKey;
16599
- if (isNewChat && prepared) {
16600
- chatKeyBytes = prepared.key;
16601
- encryptedChatKey = prepared.encryptedKey;
16602
- } else if (isNewChat) {
16603
- chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
16604
- (await import("crypto")).webcrypto.getRandomValues(
16605
- new Uint8Array(32)
16606
- )
16607
- );
16608
- encryptedChatKey = await encryptBytesWithAesGcm(
16609
- chatKeyBytes,
16610
- wrappingKey
16611
- );
16612
- } else {
16613
- const cache = loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
16614
- const chat = cache.chats.find(
16615
- (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
16615
+ if (conditionalPrivacyReceipt && (conditionalPrivacyReceipt.epoch !== this.preparedPrivacyEpoch || conditionalPrivacyReceipt.preferencesKey !== JSON.stringify(await readPrivacyPreferences()))) {
16616
+ throw new Error("Privacy preferences changed before send; prepare the turn again.");
16617
+ }
16618
+ receipt.used = true;
16619
+ };
16620
+ let metadataTransportOwned = false;
16621
+ let metadataHandoffReady = false;
16622
+ let metadataHandoffSuggestion = null;
16623
+ let initialHeaderWork = null;
16624
+ let initialHeaderNotice = null;
16625
+ let initialAdmissionDurable = false;
16626
+ let initialHeaderStopped = false;
16627
+ let initialTitleScheduled;
16628
+ let initialIconScheduled;
16629
+ let persistOptionalHeader;
16630
+ let optionalMetadata = null;
16631
+ const socketReadyForSendAt = performance.now();
16632
+ let capabilitiesWaitMs = 0;
16633
+ let keyWaitMs = 0;
16634
+ let recoveryKeypairMs = 0;
16635
+ let encryptPayloadMs = 0;
16636
+ if (!params.incognito && !ownerId2) {
16637
+ earlyContextAbort.abort();
16638
+ ws.close();
16639
+ if (earlyContext) await earlyContext;
16640
+ throw new Error("Authenticated user identity is required for saved chat recovery.");
16641
+ }
16642
+ const authSocketMs = Date.now() - authSocketStartedAt;
16643
+ const mayDeferRecovery = !params.incognito && sdkMintedChatId && !teamId && !params.projectId && shouldWaitForAi;
16644
+ let recoveryDiscovery = null;
16645
+ let recoveryDiscoveryError = null;
16646
+ let contextAckRejection = null;
16647
+ let removeContextAckWatcher = null;
16648
+ try {
16649
+ if (!params.incognito) {
16650
+ try {
16651
+ ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
16652
+ } catch {
16653
+ }
16654
+ const recoveryStartedAt = Date.now();
16655
+ recoveryDiscovery = ws.waitForRecoveryOutputDiscovery().then(
16656
+ () => {
16657
+ recoveryMs = Date.now() - recoveryStartedAt;
16658
+ },
16659
+ (error) => {
16660
+ recoveryDiscoveryError = error;
16661
+ throw error;
16662
+ }
16616
16663
  );
16617
- if (chat) {
16618
- baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
16619
- const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
16620
- if (encKey) {
16621
- chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
16622
- encryptedChatKey = encKey;
16623
- if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
16624
- if (typeof chat.details.encrypted_title === "string") {
16625
- try {
16626
- notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
16627
- } catch {
16664
+ void recoveryDiscovery.catch(() => {
16665
+ });
16666
+ if (mayDeferRecovery) {
16667
+ const capabilitiesWaitStartedAt = performance.now();
16668
+ await Promise.race([ws.waitForServerCapabilities(), recoveryDiscovery]);
16669
+ capabilitiesWaitMs = Math.round(performance.now() - capabilitiesWaitStartedAt);
16670
+ } else await recoveryDiscovery;
16671
+ }
16672
+ const messageId2 = randomUUID9();
16673
+ const wallCreatedAt = Math.floor(Date.now() / 1e3);
16674
+ const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
16675
+ const isNewChat = !params.chatId;
16676
+ let messageHistoryForRequest = params.messageHistory;
16677
+ if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
16678
+ const { messages } = savedPrivacyHistory;
16679
+ messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
16680
+ }
16681
+ ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
16682
+ const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
16683
+ params.connectedAccountDirectory
16684
+ );
16685
+ const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
16686
+ chatId,
16687
+ messageId: messageId2,
16688
+ refs: params.connectedAccountTokenRefInputs,
16689
+ teamId
16690
+ }) : [];
16691
+ assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
16692
+ let chatKeyBytes = null;
16693
+ let activeFocusId = null;
16694
+ let encryptedChatKey = null;
16695
+ let chatSlugLookupKey = null;
16696
+ let focusPhaseState = {};
16697
+ let baselineMessagesV = 0;
16698
+ let notificationTitle = null;
16699
+ let terminalExpectedMessagesV = 1;
16700
+ let savedTurnId = null;
16701
+ if (!params.incognito) {
16702
+ traceSendTiming("key_begin");
16703
+ const masterKey = this.getMasterKeyBytes();
16704
+ const preparedKey = isNewChat && ownsPreparedId && this.preparedChatKey?.chatId === chatId && this.preparedChatOwnerCurrent(this.preparedChatKey.owner, teamId) ? this.preparedChatKey : null;
16705
+ if (preparedKey) this.preparedChatKey = null;
16706
+ const keyWaitStartedAt = performance.now();
16707
+ const prepared = independentNewChatKey ? await independentNewChatKey : preparedKey ? await preparedKey.value : null;
16708
+ if (prepared && !this.preparedChatOwnerCurrent(independentKeyOwner, teamId)) throw new Error("Chat key preparation owner changed before send.");
16709
+ const wrappingKey = prepared?.wrappingKey ?? await this.getChatWrappingKey(teamId, masterKey);
16710
+ keyWaitMs = Math.round(performance.now() - keyWaitStartedAt);
16711
+ chatSlugLookupKey = wrappingKey;
16712
+ if (isNewChat && prepared) {
16713
+ chatKeyBytes = prepared.key;
16714
+ encryptedChatKey = prepared.encryptedKey;
16715
+ } else if (isNewChat) {
16716
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
16717
+ (await import("crypto")).webcrypto.getRandomValues(
16718
+ new Uint8Array(32)
16719
+ )
16720
+ );
16721
+ encryptedChatKey = await encryptBytesWithAesGcm(
16722
+ chatKeyBytes,
16723
+ wrappingKey
16724
+ );
16725
+ } else {
16726
+ const cache = (savedPrivacyHistory && this.scopedChatHistory.get(savedPrivacyHistory)) ?? loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
16727
+ const chat = cache.chats.find(
16728
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
16729
+ );
16730
+ if (chat) {
16731
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
16732
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
16733
+ if (encKey) {
16734
+ chatKeyBytes = await this.resolveChatKey(cache, chat, wrappingKey, teamId);
16735
+ encryptedChatKey = encKey;
16736
+ if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
16737
+ if (typeof chat.details.encrypted_title === "string") {
16738
+ try {
16739
+ notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
16740
+ } catch {
16741
+ }
16628
16742
  }
16629
- }
16630
- const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
16631
- if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
16632
- const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
16633
- if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
16634
- focusPhaseState = JSON.parse(phaseText);
16635
- }
16636
- const encryptedFocusId = chat.details.encrypted_active_focus_id;
16637
- if (typeof encryptedFocusId === "string" && encryptedFocusId) {
16638
- activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
16639
- if (!activeFocusId) {
16640
- throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
16743
+ const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
16744
+ if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
16745
+ const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
16746
+ if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
16747
+ focusPhaseState = JSON.parse(phaseText);
16748
+ }
16749
+ const encryptedFocusId = chat.details.encrypted_active_focus_id;
16750
+ if (typeof encryptedFocusId === "string" && encryptedFocusId) {
16751
+ activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
16752
+ if (!activeFocusId) {
16753
+ throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
16754
+ }
16641
16755
  }
16642
16756
  }
16643
16757
  }
16758
+ if (!chatKeyBytes || !encryptedChatKey) {
16759
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
16760
+ }
16644
16761
  }
16645
- if (!chatKeyBytes || !encryptedChatKey) {
16646
- throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
16647
- }
16648
- }
16649
- traceSendTiming("key_ready");
16650
- }
16651
- let focusPersistence = Promise.resolve();
16652
- let focusPersistenceError = null;
16653
- const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
16654
- ws,
16655
- chatId,
16656
- chatKey: chatKeyBytes,
16657
- existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
16658
- previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
16659
- onApplied: params.onChatContextApplied
16660
- }) : null;
16661
- let projectAuthoringAvailable = null;
16662
- if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
16663
- if (!value || typeof value !== "object") return;
16664
- const frame = value;
16665
- if (frame.chat_id === chatId && frame.user_message_id === messageId2 && typeof frame.project_id === "string")
16666
- projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId2 };
16667
- });
16668
- const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
16669
- client: this,
16670
- ws,
16671
- chatId,
16672
- teamId,
16673
- onPending: params.onProjectFocusPending,
16674
- onError: (error) => {
16675
- focusPersistenceError = error;
16762
+ traceSendTiming("key_ready");
16676
16763
  }
16677
- }) : null;
16678
- ws.onMessageType("focus_mode_activated", (event) => {
16679
- if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
16680
- const focusId = event.focus_id;
16681
- if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
16682
- const key = chatKeyBytes;
16683
- focusPersistence = focusPersistence.then(async () => {
16684
- const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
16685
- await ws.sendAsync("update_encrypted_active_focus_id", {
16686
- chat_id: chatId,
16687
- encrypted_active_focus_id: encryptedFocusId
16688
- });
16689
- }).catch((error) => {
16690
- focusPersistenceError = error;
16764
+ let focusPersistence = Promise.resolve();
16765
+ let focusPersistenceError = null;
16766
+ const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
16767
+ ws,
16768
+ chatId,
16769
+ chatKey: chatKeyBytes,
16770
+ existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
16771
+ previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
16772
+ onApplied: params.onChatContextApplied
16773
+ }) : null;
16774
+ let projectAuthoringAvailable = null;
16775
+ if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
16776
+ if (!value || typeof value !== "object") return;
16777
+ const frame = value;
16778
+ if (frame.chat_id === chatId && frame.user_message_id === messageId2 && typeof frame.project_id === "string")
16779
+ projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId2 };
16691
16780
  });
16692
- });
16693
- ws.onMessageType("focus_phases_updated", (event) => {
16694
- if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
16695
- const key = chatKeyBytes;
16696
- const states2 = event.states;
16697
- focusPersistence = focusPersistence.then(async () => {
16698
- for (const [focusId, phase] of Object.entries(states2)) {
16699
- 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) {
16700
- throw new Error("Invalid focus phase state received");
16701
- }
16702
- if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
16781
+ const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
16782
+ client: this,
16783
+ ws,
16784
+ chatId,
16785
+ teamId,
16786
+ onPending: params.onProjectFocusPending,
16787
+ onError: (error) => {
16788
+ focusPersistenceError = error;
16703
16789
  }
16704
- focusPhaseState = states2;
16705
- const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
16706
- const persisted = ws.waitForMessage(
16707
- "encrypted_metadata_stored",
16708
- (payload) => payload.chat_id === chatId,
16709
- 2e4
16710
- );
16711
- await ws.sendAsync("encrypted_chat_metadata", {
16712
- chat_id: chatId,
16713
- ...teamId ? { team_id: teamId } : {},
16714
- encrypted_focus_phase_state: encryptedState
16715
- });
16716
- await persisted;
16717
- const eventIds = /* @__PURE__ */ new Set();
16718
- for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
16719
- if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
16720
- eventIds.add(transition.event_id);
16721
- const content = JSON.stringify(transition);
16722
- await ws.sendAsync("chat_system_message_added", {
16790
+ }) : null;
16791
+ ws.onMessageType("focus_mode_activated", (event) => {
16792
+ if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
16793
+ const focusId = event.focus_id;
16794
+ if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
16795
+ const key = chatKeyBytes;
16796
+ focusPersistence = focusPersistence.then(async () => {
16797
+ const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
16798
+ await ws.sendAsync("update_encrypted_active_focus_id", {
16723
16799
  chat_id: chatId,
16724
- message: {
16725
- message_id: transition.event_id,
16726
- role: "system",
16727
- status: "synced",
16728
- encrypted_content: await encryptWithAesGcmCombined(content, key),
16729
- created_at: transition.created_at
16730
- }
16800
+ encrypted_active_focus_id: encryptedFocusId
16731
16801
  });
16732
- }
16733
- clearSyncCache(teamId);
16734
- }).catch((error) => {
16735
- focusPersistenceError = error;
16802
+ }).catch((error) => {
16803
+ focusPersistenceError = error;
16804
+ });
16736
16805
  });
16737
- });
16738
- const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
16739
- if (!params.incognito && chatKeyBytes) {
16740
- registerCliProjectFileExecutor({
16741
- client: this,
16742
- ws,
16743
- chatId,
16744
- chatKey: chatKeyBytes,
16745
- piiMappings,
16746
- memories: availableMemories,
16747
- requestApproval: params.onProjectWriteApproval,
16748
- requestReadApproval: params.onProjectReadApproval,
16749
- onHostedVersionCommitted: params.onHostedVersionCommitted
16806
+ ws.onMessageType("focus_phases_updated", (event) => {
16807
+ if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
16808
+ const key = chatKeyBytes;
16809
+ const states2 = event.states;
16810
+ focusPersistence = focusPersistence.then(async () => {
16811
+ for (const [focusId, phase] of Object.entries(states2)) {
16812
+ 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) {
16813
+ throw new Error("Invalid focus phase state received");
16814
+ }
16815
+ if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
16816
+ }
16817
+ focusPhaseState = states2;
16818
+ const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
16819
+ const persisted = ws.waitForMessage(
16820
+ "encrypted_metadata_stored",
16821
+ (payload) => payload.chat_id === chatId,
16822
+ 2e4
16823
+ );
16824
+ await ws.sendAsync("encrypted_chat_metadata", {
16825
+ chat_id: chatId,
16826
+ ...teamId ? { team_id: teamId } : {},
16827
+ encrypted_focus_phase_state: encryptedState
16828
+ });
16829
+ await persisted;
16830
+ const eventIds = /* @__PURE__ */ new Set();
16831
+ for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
16832
+ if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
16833
+ eventIds.add(transition.event_id);
16834
+ const content = JSON.stringify(transition);
16835
+ await ws.sendAsync("chat_system_message_added", {
16836
+ chat_id: chatId,
16837
+ message: {
16838
+ message_id: transition.event_id,
16839
+ role: "system",
16840
+ status: "synced",
16841
+ encrypted_content: await encryptWithAesGcmCombined(content, key),
16842
+ created_at: transition.created_at
16843
+ }
16844
+ });
16845
+ }
16846
+ clearSyncCache(teamId);
16847
+ }).catch((error) => {
16848
+ focusPersistenceError = error;
16849
+ });
16750
16850
  });
16751
- clientCapabilities.push("project_file_jobs");
16752
- registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
16753
- clientCapabilities.push("remote_command_jobs");
16754
- }
16755
- const messagePayload = {
16756
- chat_id: chatId,
16757
- ...teamId ? { team_id: teamId } : {},
16758
- client_capabilities: clientCapabilities,
16759
- // Only decrypted current metadata restores focus; history is never authority.
16760
- active_focus_id: activeFocusId,
16761
- focus_phase_state: focusPhaseState,
16762
- is_incognito: Boolean(params.incognito),
16763
- message: {
16764
- message_id: messageId2,
16765
- chat_id: chatId,
16766
- role: "user",
16767
- sender_name: "User",
16768
- status: "sent",
16769
- content: finalMessage,
16770
- created_at: createdAt,
16771
- chat_has_title: Boolean(params.chatId)
16772
- }
16773
- };
16774
- const useCombinedAdmission = ws.supportsServerCapability?.("chat_turn_admit_v1") === true;
16775
- const useDeferredContext = useCombinedAdmission && !params.chatId && !params.projectId && !teamId && ws.supportsServerCapability?.("chat_turn_context_v1") === true;
16776
- const useSelectionDetailsV2 = useDeferredContext && sdkMintedChatId && ws.supportsServerCapability?.("chat_turn_context_selection_details_v2") === true;
16777
- const contextRequestId = !params.incognito && shouldWaitForAi && useDeferredContext ? randomUUID9() : null;
16778
- let pendingContext = null;
16779
- if (!params.incognito && shouldWaitForAi) {
16780
- if (contextRequestId) {
16781
- messagePayload.client_context_pending = true;
16782
- messagePayload.client_context_request_id = contextRequestId;
16851
+ const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
16852
+ if (!params.incognito && chatKeyBytes) {
16853
+ registerCliProjectFileExecutor({
16854
+ client: this,
16855
+ ws,
16856
+ chatId,
16857
+ chatKey: chatKeyBytes,
16858
+ piiMappings,
16859
+ memories: availableMemories,
16860
+ requestApproval: params.onProjectWriteApproval,
16861
+ requestReadApproval: params.onProjectReadApproval,
16862
+ onHostedVersionCommitted: params.onHostedVersionCommitted
16863
+ });
16864
+ clientCapabilities.push("project_file_jobs");
16865
+ registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
16866
+ clientCapabilities.push("remote_command_jobs");
16783
16867
  }
16784
- pendingContext = (async () => {
16785
- const prepared = earlyContext ? await earlyContext : { ok: true, value: await withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16786
- sdkMintedChatId,
16787
- onRouting: publishRouting,
16788
- verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16789
- usePersonalRoutingDirectory,
16790
- onProjectRouting: publishProjectRoutingTimed,
16791
- onProjectRoutingError: rejectProjectRouting
16792
- })), elapsedMs: Date.now() - contextStartedAt };
16793
- if (!prepared.ok) throw prepared.error;
16794
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
16795
- if (process.env.OPENMATES_DEBUG === "1") {
16796
- 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"}`);
16868
+ const messagePayload = {
16869
+ chat_id: chatId,
16870
+ ...teamId ? { team_id: teamId } : {},
16871
+ client_capabilities: clientCapabilities,
16872
+ // Only decrypted current metadata restores focus; history is never authority.
16873
+ active_focus_id: activeFocusId,
16874
+ focus_phase_state: focusPhaseState,
16875
+ is_incognito: Boolean(params.incognito),
16876
+ message: {
16877
+ message_id: messageId2,
16878
+ chat_id: chatId,
16879
+ role: "user",
16880
+ sender_name: "User",
16881
+ status: "sent",
16882
+ content: finalMessage,
16883
+ created_at: createdAt,
16884
+ chat_has_title: Boolean(params.chatId)
16797
16885
  }
16798
- return deferredProjectDirectory ? { ...prepared.value, project_focus_candidates: prepared.value.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : prepared.value;
16799
- })();
16800
- void pendingContext.catch(() => {
16801
- });
16802
- }
16803
- if (params.testMockMarker !== void 0) {
16804
- const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
16805
- messagePayload.test_mock_marker = capacityReplayMarker(
16806
- params.testMockMarker,
16807
- this.apiUrl,
16808
- process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
16809
- );
16810
- }
16811
- if (isNewChat && encryptedChatKey) {
16812
- messagePayload.encrypted_chat_key = encryptedChatKey;
16813
- }
16814
- if (memoryMetadataKeys.length > 0) {
16815
- messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
16816
- }
16817
- if (connectedAccountDirectory) {
16818
- messagePayload.connected_account_directory = connectedAccountDirectory;
16819
- }
16820
- if (connectedAccountTokenRefs.length > 0) {
16821
- messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
16822
- }
16823
- if (params.benchmarkMetadata) {
16824
- messagePayload.benchmark_metadata = params.benchmarkMetadata;
16825
- }
16826
- if (params.learningMode) {
16827
- messagePayload.learning_mode = {
16828
- enabled: params.learningMode.enabled,
16829
- age_group: params.learningMode.ageGroup ?? null
16830
16886
  };
16831
- }
16832
- if (params.incognito) {
16833
- const providedHistory = (params.messageHistory ?? []).map(
16834
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
16835
- );
16836
- messagePayload.message_history = [...providedHistory, {
16837
- message_id: messageId2,
16838
- chat_id: chatId,
16839
- role: "user",
16840
- sender_name: "User",
16841
- content: finalMessage,
16842
- created_at: createdAt
16843
- }];
16844
- } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
16845
- messagePayload.message_history = messageHistoryForRequest.map(
16846
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
16847
- );
16848
- }
16849
- if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
16850
- messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
16851
- embed_id: embed.embedId,
16852
- type: embed.type,
16853
- content: embed.content,
16854
- status: embed.status,
16855
- text_preview: embed.textPreview
16856
- }));
16857
- }
16858
- const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
16859
- if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
16860
- if (!ownerId2) {
16861
- throw new Error("Authenticated user identity is required to encrypt embeds.");
16887
+ const useCombinedAdmission = ws.supportsServerCapability?.("chat_turn_admit_v1") === true;
16888
+ const useDeferredContext = useCombinedAdmission && !params.chatId && !params.projectId && !teamId && ws.supportsServerCapability?.("chat_turn_context_v1") === true;
16889
+ const useSelectionDetailsV2 = useDeferredContext && sdkMintedChatId && ws.supportsServerCapability?.("chat_turn_context_selection_details_v2") === true;
16890
+ const contextRequestId = !params.incognito && shouldWaitForAi && useDeferredContext ? randomUUID9() : null;
16891
+ let pendingContext = null;
16892
+ if (!params.incognito && shouldWaitForAi) {
16893
+ if (contextRequestId) {
16894
+ messagePayload.client_context_pending = true;
16895
+ messagePayload.client_context_request_id = contextRequestId;
16896
+ }
16897
+ pendingContext = (async () => {
16898
+ const prepared = earlyContext ? await earlyContext : { ok: true, value: await withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, suppliedContext, finalMessage, {
16899
+ sdkMintedChatId,
16900
+ onRouting: publishRouting,
16901
+ verifiedProjectCandidates: verifiedProjectCandidates ?? void 0,
16902
+ usePersonalRoutingDirectory,
16903
+ onProjectRouting: publishProjectRoutingTimed,
16904
+ onProjectRoutingError: rejectProjectRouting
16905
+ })), elapsedMs: Date.now() - contextStartedAt };
16906
+ if (!prepared.ok) throw prepared.error;
16907
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
16908
+ if (process.env.OPENMATES_DEBUG === "1") {
16909
+ 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"}`);
16910
+ }
16911
+ return deferredProjectDirectory ? { ...prepared.value, project_focus_candidates: prepared.value.project_focus_candidates?.filter((candidate) => candidate.auto_selection === true) } : prepared.value;
16912
+ })();
16913
+ void pendingContext.catch(() => {
16914
+ });
16915
+ }
16916
+ if (params.testMockMarker !== void 0) {
16917
+ const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
16918
+ messagePayload.test_mock_marker = capacityReplayMarker(
16919
+ params.testMockMarker,
16920
+ this.apiUrl,
16921
+ process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
16922
+ );
16862
16923
  }
16863
- if (!chatKeyBytes) {
16864
- throw new Error("Saved chat key is required to encrypt every prepared embed.");
16924
+ if (isNewChat && encryptedChatKey) {
16925
+ messagePayload.encrypted_chat_key = encryptedChatKey;
16865
16926
  }
16866
- if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
16867
- throw new Error("Prepared message embed IDs must be unique.");
16927
+ if (memoryMetadataKeys.length > 0) {
16928
+ messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
16868
16929
  }
16869
- const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
16870
- if (preparedIds.some((id) => !id || id.length > 128)) {
16871
- throw new Error("Prepared message embed ID is invalid.");
16930
+ if (connectedAccountDirectory) {
16931
+ messagePayload.connected_account_directory = connectedAccountDirectory;
16872
16932
  }
16873
- const canonicalReady = /* @__PURE__ */ new Set();
16874
- for (let offset = 0; offset < preparedIds.length; offset += 20) {
16875
- const selectedIds = preparedIds.slice(offset, offset + 20);
16876
- if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
16877
- throw new Error("Message embed availability request exceeds its byte limit.");
16878
- }
16879
- const path2 = this.appendTeamQuery(
16880
- `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
16881
- { teamId }
16882
- );
16883
- const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
16884
- if (!availability.ok) {
16885
- throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
16886
- }
16887
- const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
16888
- for (const id of page.ready) canonicalReady.add(id);
16933
+ if (connectedAccountTokenRefs.length > 0) {
16934
+ messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
16889
16935
  }
16890
- const masterKey = this.getMasterKeyBytes();
16891
- for (const embed of params.preparedEmbeds) {
16892
- if (canonicalReady.has(embed.embedId)) continue;
16893
- const encrypted = await encryptEmbed(
16894
- embed,
16895
- masterKey,
16896
- chatKeyBytes,
16897
- chatId,
16898
- messageId2,
16899
- ownerId2
16936
+ if (params.benchmarkMetadata) {
16937
+ messagePayload.benchmark_metadata = params.benchmarkMetadata;
16938
+ }
16939
+ if (params.learningMode) {
16940
+ messagePayload.learning_mode = {
16941
+ enabled: params.learningMode.enabled,
16942
+ age_group: params.learningMode.ageGroup ?? null
16943
+ };
16944
+ }
16945
+ if (params.incognito) {
16946
+ const providedHistory = (params.messageHistory ?? []).map(
16947
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
16948
+ );
16949
+ messagePayload.message_history = [...providedHistory, {
16950
+ message_id: messageId2,
16951
+ chat_id: chatId,
16952
+ role: "user",
16953
+ sender_name: "User",
16954
+ content: finalMessage,
16955
+ created_at: createdAt
16956
+ }];
16957
+ } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
16958
+ messagePayload.message_history = messageHistoryForRequest.map(
16959
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
16900
16960
  );
16901
- if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
16902
- throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
16903
- }
16904
- encryptedEmbeds.push(encrypted);
16905
16961
  }
16906
- }
16907
- if (!params.incognito && encryptedEmbeds.length > 0) {
16908
- if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
16909
- assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId: messageId2, ownerId: ownerId2 });
16910
- }
16911
- if (encryptedEmbeds.length > 0) {
16912
- messagePayload.encrypted_embeds = encryptedEmbeds;
16913
- }
16914
- let dispatchHandlersReady = () => {
16915
- };
16916
- const handlersReady = new Promise((resolve20) => {
16917
- dispatchHandlersReady = resolve20;
16918
- });
16919
- let admissionPayload = null;
16920
- let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId2, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
16921
- if (!params.incognito && chatKeyBytes && encryptedChatKey) {
16922
- traceSendTiming("preflight_begin");
16923
- const protocolVersion = 1;
16924
- const chatKeyVersion = 1;
16925
- const turnId = privacyTurnId;
16926
- savedTurnId = turnId;
16927
- terminalExpectedMessagesV = baselineMessagesV + 1;
16928
- const recoveryKeypairStartedAt = performance.now();
16929
- const recoveryKeypair = independentRecoveryKeypair ? await independentRecoveryKeypair : await deriveChatCompletionRecoveryKeypair(
16930
- Buffer.from(chatKeyBytes).toString("base64url"),
16931
- chatId,
16932
- chatKeyVersion
16933
- );
16934
- recoveryKeypairMs = Math.round(performance.now() - recoveryKeypairStartedAt);
16935
- const encryptPayloadStartedAt = performance.now();
16936
- const [encryptedContent, encryptedSenderName, encryptedPiiMappings] = await Promise.all([
16937
- encryptWithAesGcmCombined(finalMessage, chatKeyBytes),
16938
- encryptWithAesGcmCombined(teamSenderName, chatKeyBytes),
16939
- piiMappings.length ? encryptWithAesGcmCombined(JSON.stringify(piiMappings), chatKeyBytes) : null
16940
- ]);
16941
- const encryptedUserMessage = {
16942
- client_message_id: messageId2,
16943
- chat_id: chatId,
16944
- encrypted_content: encryptedContent,
16945
- role: "user",
16946
- created_at: createdAt,
16947
- updated_at: createdAt
16948
- };
16949
- encryptedUserMessage.encrypted_sender_name = encryptedSenderName;
16950
- if (encryptedPiiMappings) encryptedUserMessage.encrypted_pii_mappings = encryptedPiiMappings;
16951
- Object.assign(messagePayload, {
16952
- turn_id: turnId,
16953
- recovery_public_key: recoveryKeypair.publicKey,
16954
- chat_key_version: chatKeyVersion
16955
- });
16956
- let preflightInferenceRequest = messagePayload;
16957
- if (teamId) {
16958
- const teamInferenceHistory = [
16959
- ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
16960
- role: historyMessage.role,
16961
- content: historyMessage.content,
16962
- category: historyMessage.category ?? null,
16963
- sender_name: historyMessage.sender_name ?? historyMessage.role,
16964
- created_at: historyMessage.created_at
16965
- })),
16966
- {
16967
- role: "user",
16968
- content: finalMessage,
16969
- category: null,
16970
- sender_name: teamSenderName,
16971
- created_at: createdAt
16962
+ if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
16963
+ messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
16964
+ embed_id: embed.embedId,
16965
+ type: embed.type,
16966
+ content: embed.content,
16967
+ status: embed.status,
16968
+ text_preview: embed.textPreview
16969
+ }));
16970
+ }
16971
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
16972
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
16973
+ if (!ownerId2) {
16974
+ throw new Error("Authenticated user identity is required to encrypt embeds.");
16975
+ }
16976
+ if (!chatKeyBytes) {
16977
+ throw new Error("Saved chat key is required to encrypt every prepared embed.");
16978
+ }
16979
+ if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
16980
+ throw new Error("Prepared message embed IDs must be unique.");
16981
+ }
16982
+ const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
16983
+ if (preparedIds.some((id) => !id || id.length > 128)) {
16984
+ throw new Error("Prepared message embed ID is invalid.");
16985
+ }
16986
+ const canonicalReady = /* @__PURE__ */ new Set();
16987
+ for (let offset = 0; offset < preparedIds.length; offset += 20) {
16988
+ const selectedIds = preparedIds.slice(offset, offset + 20);
16989
+ if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
16990
+ throw new Error("Message embed availability request exceeds its byte limit.");
16972
16991
  }
16973
- ];
16974
- const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId2 };
16975
- delete messagePayload.message_history;
16976
- messagePayload.message = teamMessageEnvelope;
16977
- if (shouldWaitForAi) {
16978
- messagePayload.team_ai_invocation = { history: teamInferenceHistory };
16979
- preflightInferenceRequest = {
16980
- ...messagePayload,
16981
- message_history: teamInferenceHistory
16982
- };
16992
+ const path2 = this.appendTeamQuery(
16993
+ `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
16994
+ { teamId }
16995
+ );
16996
+ const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
16997
+ if (!availability.ok) {
16998
+ throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
16999
+ }
17000
+ const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
17001
+ for (const id of page.ready) canonicalReady.add(id);
17002
+ }
17003
+ const masterKey = this.getMasterKeyBytes();
17004
+ for (const embed of params.preparedEmbeds) {
17005
+ if (canonicalReady.has(embed.embedId)) continue;
17006
+ const encrypted = await encryptEmbed(
17007
+ embed,
17008
+ masterKey,
17009
+ chatKeyBytes,
17010
+ chatId,
17011
+ messageId2,
17012
+ ownerId2
17013
+ );
17014
+ if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
17015
+ throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
17016
+ }
17017
+ encryptedEmbeds.push(encrypted);
16983
17018
  }
16984
17019
  }
16985
- const preflightPayload = {
16986
- protocol_version: protocolVersion,
16987
- chat_id: chatId,
16988
- ...teamId ? { team_id: teamId } : {},
16989
- turn_id: turnId,
16990
- message_id: messageId2,
16991
- chat_key_version: chatKeyVersion,
16992
- encrypted_chat_key: encryptedChatKey,
16993
- recovery_public_key: recoveryKeypair.publicKey,
16994
- expected_messages_v: baselineMessagesV,
16995
- encrypted_user_message: encryptedUserMessage,
16996
- inference_request: preflightInferenceRequest,
16997
- ...conditionalPrivacyReceipt ? {
16998
- privacy_receipt_id: conditionalPrivacyReceipt.receiptId,
16999
- privacy_generation: conditionalPrivacyReceipt.generation
17000
- } : {}
17020
+ if (!params.incognito && encryptedEmbeds.length > 0) {
17021
+ if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
17022
+ assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId: messageId2, ownerId: ownerId2 });
17023
+ }
17024
+ if (encryptedEmbeds.length > 0) {
17025
+ messagePayload.encrypted_embeds = encryptedEmbeds;
17026
+ }
17027
+ let dispatchHandlersReady = () => {
17001
17028
  };
17002
- if (isNewChat) {
17003
- if (!chatSlugLookupKey) {
17004
- throw new Error("Encrypted chat slug lookup key was not initialized.");
17005
- }
17006
- const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
17007
- const slugMetadata = await buildEncryptedObjectSlugMetadata({
17008
- value: selectNewChatSlugValue({
17009
- message: finalMessage,
17010
- chatId,
17011
- explicitSlug: params.slug
17012
- }),
17013
- encryptionKey: chatKeyBytes,
17014
- lookupKey: chatSlugLookupKey
17015
- });
17016
- preflightPayload.encrypted_chat_metadata = {
17017
- encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
17018
- encrypted_slug: slugMetadata.encrypted_slug,
17019
- slug_lookup_hash: slugMetadata.slug_lookup_hash,
17020
- encrypted_chat_key: encryptedChatKey,
17029
+ const handlersReady = new Promise((resolve20) => {
17030
+ dispatchHandlersReady = resolve20;
17031
+ });
17032
+ let admissionPayload = null;
17033
+ let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId2, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
17034
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
17035
+ traceSendTiming("preflight_begin");
17036
+ const protocolVersion = 1;
17037
+ const chatKeyVersion = 1;
17038
+ const turnId = privacyTurnId;
17039
+ savedTurnId = turnId;
17040
+ terminalExpectedMessagesV = baselineMessagesV + 1;
17041
+ const recoveryKeypairStartedAt = performance.now();
17042
+ const recoveryKeypair = independentRecoveryKeypair ? await independentRecoveryKeypair : await deriveChatCompletionRecoveryKeypair(
17043
+ Buffer.from(chatKeyBytes).toString("base64url"),
17044
+ chatId,
17045
+ chatKeyVersion
17046
+ );
17047
+ recoveryKeypairMs = Math.round(performance.now() - recoveryKeypairStartedAt);
17048
+ const encryptPayloadStartedAt = performance.now();
17049
+ const [encryptedContent, encryptedSenderName, encryptedPiiMappings] = await Promise.all([
17050
+ encryptWithAesGcmCombined(finalMessage, chatKeyBytes),
17051
+ encryptWithAesGcmCombined(teamSenderName, chatKeyBytes),
17052
+ piiMappings.length ? encryptWithAesGcmCombined(JSON.stringify(piiMappings), chatKeyBytes) : null
17053
+ ]);
17054
+ const encryptedUserMessage = {
17055
+ client_message_id: messageId2,
17056
+ chat_id: chatId,
17057
+ encrypted_content: encryptedContent,
17058
+ role: "user",
17021
17059
  created_at: createdAt,
17022
17060
  updated_at: createdAt
17023
17061
  };
17024
- }
17025
- encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17026
- try {
17062
+ encryptedUserMessage.encrypted_sender_name = encryptedSenderName;
17063
+ if (encryptedPiiMappings) encryptedUserMessage.encrypted_pii_mappings = encryptedPiiMappings;
17064
+ Object.assign(messagePayload, {
17065
+ turn_id: turnId,
17066
+ recovery_public_key: recoveryKeypair.publicKey,
17067
+ chat_key_version: chatKeyVersion
17068
+ });
17069
+ let preflightInferenceRequest = messagePayload;
17027
17070
  if (teamId) {
17028
- await stageCliTeamNotificationPreview(ws, {
17029
- teamId,
17030
- chatId,
17031
- messageId: messageId2,
17032
- content: finalMessage,
17033
- title: notificationTitle
17071
+ const teamInferenceHistory = [
17072
+ ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
17073
+ role: historyMessage.role,
17074
+ content: historyMessage.content,
17075
+ category: historyMessage.category ?? null,
17076
+ sender_name: historyMessage.sender_name ?? historyMessage.role,
17077
+ created_at: historyMessage.created_at
17078
+ })),
17079
+ {
17080
+ role: "user",
17081
+ content: finalMessage,
17082
+ category: null,
17083
+ sender_name: teamSenderName,
17084
+ created_at: createdAt
17085
+ }
17086
+ ];
17087
+ const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId2 };
17088
+ delete messagePayload.message_history;
17089
+ messagePayload.message = teamMessageEnvelope;
17090
+ if (shouldWaitForAi) {
17091
+ messagePayload.team_ai_invocation = { history: teamInferenceHistory };
17092
+ preflightInferenceRequest = {
17093
+ ...messagePayload,
17094
+ message_history: teamInferenceHistory
17095
+ };
17096
+ }
17097
+ }
17098
+ const preflightPayload = {
17099
+ protocol_version: protocolVersion,
17100
+ chat_id: chatId,
17101
+ ...teamId ? { team_id: teamId } : {},
17102
+ turn_id: turnId,
17103
+ message_id: messageId2,
17104
+ chat_key_version: chatKeyVersion,
17105
+ encrypted_chat_key: encryptedChatKey,
17106
+ recovery_public_key: recoveryKeypair.publicKey,
17107
+ expected_messages_v: baselineMessagesV,
17108
+ encrypted_user_message: encryptedUserMessage,
17109
+ inference_request: preflightInferenceRequest,
17110
+ ...conditionalPrivacyReceipt ? {
17111
+ privacy_receipt_id: conditionalPrivacyReceipt.receiptId,
17112
+ privacy_generation: conditionalPrivacyReceipt.generation
17113
+ } : {}
17114
+ };
17115
+ if (isNewChat) {
17116
+ if (!chatSlugLookupKey) {
17117
+ throw new Error("Encrypted chat slug lookup key was not initialized.");
17118
+ }
17119
+ const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
17120
+ const slugMetadata = await buildEncryptedObjectSlugMetadata({
17121
+ value: selectNewChatSlugValue({
17122
+ message: finalMessage,
17123
+ chatId,
17124
+ explicitSlug: params.slug
17125
+ }),
17126
+ encryptionKey: chatKeyBytes,
17127
+ lookupKey: chatSlugLookupKey
17034
17128
  });
17129
+ preflightPayload.encrypted_chat_metadata = {
17130
+ encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
17131
+ encrypted_slug: slugMetadata.encrypted_slug,
17132
+ slug_lookup_hash: slugMetadata.slug_lookup_hash,
17133
+ encrypted_chat_key: encryptedChatKey,
17134
+ created_at: createdAt,
17135
+ updated_at: createdAt
17136
+ };
17035
17137
  }
17036
- if (params.projectId) {
17037
- await prepareCliProjectFocusForPreflight({
17138
+ encryptPayloadMs = Math.round(performance.now() - encryptPayloadStartedAt);
17139
+ try {
17140
+ if (teamId) {
17141
+ await stageCliTeamNotificationPreview(ws, {
17142
+ teamId,
17143
+ chatId,
17144
+ messageId: messageId2,
17145
+ content: finalMessage,
17146
+ title: notificationTitle
17147
+ });
17148
+ }
17149
+ if (params.projectId) {
17150
+ await prepareCliProjectFocusForPreflight({
17151
+ ws,
17152
+ chatId,
17153
+ teamId,
17154
+ isNewChat,
17155
+ encryptedChatKey,
17156
+ createdAt,
17157
+ activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17158
+ });
17159
+ const focusedContext = await prepareCliJevContext(
17160
+ this,
17161
+ chatId,
17162
+ { teamId, personal: !teamId },
17163
+ params.jevContext,
17164
+ finalMessage
17165
+ );
17166
+ pendingContext = Promise.resolve(focusedContext);
17167
+ }
17168
+ admissionPayload = preflightPayload;
17169
+ traceSendTiming("preflight_ready");
17170
+ } catch (error) {
17171
+ ws.close();
17172
+ throw error;
17173
+ }
17174
+ }
17175
+ const initialHeaderOwner = this.preparedChatOwner(this.requireSession(), teamId);
17176
+ const startInitialHeader = () => {
17177
+ if (!initialAdmissionDurable || !initialHeaderNotice || !chatKeyBytes || !isNewChat || params.incognito || !savedTurnId) return;
17178
+ const notice = { ...initialHeaderNotice }, key = chatKeyBytes, turn = savedTurnId;
17179
+ const titleChanged = !!notice.chatTitle && notice.chatTitle !== initialTitleScheduled;
17180
+ const iconChanged = !!notice.chatIcon && notice.chatIcon !== initialIconScheduled;
17181
+ if (!titleChanged && !iconChanged) return;
17182
+ initialTitleScheduled = notice.chatTitle ?? initialTitleScheduled;
17183
+ initialIconScheduled = notice.chatIcon ?? initialIconScheduled;
17184
+ const current2 = () => !initialHeaderStopped && ws.isOpen() && this.preparedChatOwnerCurrent(initialHeaderOwner, teamId) && this.activeSubmittedChatTurns.get(chatId) === turn;
17185
+ initialHeaderWork = (initialHeaderWork ?? Promise.resolve()).then(async () => {
17186
+ if (!current2()) return;
17187
+ if (titleChanged) await this.persistPostProcessingMetadata({
17038
17188
  ws,
17039
17189
  chatId,
17040
17190
  teamId,
17041
- isNewChat,
17191
+ chatKeyBytes: key,
17042
17192
  encryptedChatKey,
17043
- createdAt,
17044
- activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
17193
+ followUpSuggestions: [],
17194
+ suggestedFollowupMessage: null,
17195
+ newChatSuggestions: [],
17196
+ chatSummary: null,
17197
+ chatTags: [],
17198
+ updatedChatTitle: notice.chatTitle ?? null,
17199
+ sourceTitleVersion: notice.sourceTitleVersion ?? null,
17200
+ sourceMetadataVersion: notice.sourceMetadataVersion ?? null,
17201
+ unversionedGeneratedTitle: notice.sourceTitleVersion === void 0 && notice.sourceMetadataVersion === void 0,
17202
+ ownerCurrent: current2
17045
17203
  });
17046
- const focusedContext = await prepareCliJevContext(
17047
- this,
17048
- chatId,
17049
- { teamId, personal: !teamId },
17050
- params.jevContext,
17051
- finalMessage
17052
- );
17053
- pendingContext = Promise.resolve(focusedContext);
17204
+ if (!current2()) return;
17205
+ if (iconChanged || notice.category) {
17206
+ const [encryptedIcon, encryptedCategory] = await Promise.all([
17207
+ notice.chatIcon ? encryptWithAesGcmCombined(notice.chatIcon, key) : null,
17208
+ notice.category ? encryptWithAesGcmCombined(notice.category, key) : null
17209
+ ]);
17210
+ if (!current2()) return;
17211
+ const stored = ws.waitForMessage("encrypted_metadata_stored", (value) => value.chat_id === chatId, 2e4);
17212
+ void stored.catch(() => {
17213
+ });
17214
+ await ws.sendAsync("encrypted_chat_metadata", {
17215
+ chat_id: chatId,
17216
+ ...teamId ? { team_id: teamId } : {},
17217
+ encrypted_chat_key: encryptedChatKey,
17218
+ ...encryptedIcon ? { encrypted_icon: encryptedIcon } : {},
17219
+ ...encryptedCategory ? { encrypted_chat_category: encryptedCategory } : {}
17220
+ });
17221
+ await stored;
17222
+ }
17223
+ if (current2()) clearSyncCache(teamId);
17224
+ }).catch(() => {
17225
+ });
17226
+ };
17227
+ const onTurnStream = (event) => {
17228
+ 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)) {
17229
+ initialHeaderNotice = { ...initialHeaderNotice, ...event };
17230
+ startInitialHeader();
17054
17231
  }
17055
- admissionPayload = preflightPayload;
17056
- traceSendTiming("preflight_ready");
17057
- } catch (error) {
17058
- ws.close();
17059
- throw error;
17060
- }
17061
- }
17062
- const initialHeaderOwner = this.preparedChatOwner(this.requireSession(), teamId);
17063
- const startInitialHeader = () => {
17064
- if (!initialAdmissionDurable || !initialHeaderNotice || !chatKeyBytes || !isNewChat || params.incognito || !savedTurnId) return;
17065
- const notice = { ...initialHeaderNotice }, key = chatKeyBytes, turn = savedTurnId;
17066
- const titleChanged = !!notice.chatTitle && notice.chatTitle !== initialTitleScheduled;
17067
- const iconChanged = !!notice.chatIcon && notice.chatIcon !== initialIconScheduled;
17068
- if (!titleChanged && !iconChanged) return;
17069
- initialTitleScheduled = notice.chatTitle ?? initialTitleScheduled;
17070
- initialIconScheduled = notice.chatIcon ?? initialIconScheduled;
17071
- const current2 = () => !initialHeaderStopped && ws.isOpen() && this.preparedChatOwnerCurrent(initialHeaderOwner, teamId) && this.activeSubmittedChatTurns.get(chatId) === turn;
17072
- initialHeaderWork = (initialHeaderWork ?? Promise.resolve()).then(async () => {
17073
- if (!current2()) return;
17074
- if (titleChanged) await this.persistPostProcessingMetadata({
17232
+ params.onStream?.(event);
17233
+ };
17234
+ if (!params.incognito && chatKeyBytes && savedTurnId) {
17235
+ const key = chatKeyBytes;
17236
+ const ownedTurnId = savedTurnId;
17237
+ this.activeSubmittedChatTurns.set(chatId, ownedTurnId);
17238
+ const owner2 = this.preparedChatOwner(this.requireSession(), teamId);
17239
+ const stopOwned = () => optionalMetadata?.stop();
17240
+ optionalMetadata = observeOwnedTurnMetadata({
17075
17241
  ws,
17076
17242
  chatId,
17077
- teamId,
17078
- chatKeyBytes: key,
17079
- encryptedChatKey,
17080
- followUpSuggestions: [],
17081
- suggestedFollowupMessage: null,
17082
- newChatSuggestions: [],
17083
- chatSummary: null,
17084
- chatTags: [],
17085
- updatedChatTitle: notice.chatTitle ?? null,
17086
- sourceTitleVersion: notice.sourceTitleVersion ?? null,
17087
- sourceMetadataVersion: notice.sourceMetadataVersion ?? null,
17088
- unversionedGeneratedTitle: notice.sourceTitleVersion === void 0 && notice.sourceMetadataVersion === void 0,
17089
- ownerCurrent: current2
17243
+ turnId: savedTurnId,
17244
+ userMessageId: messageId2,
17245
+ closeAfterMetadata: true,
17246
+ ownerCurrent: () => this.preparedChatOwnerCurrent(owner2, teamId),
17247
+ persist: async (metadata) => {
17248
+ await this.persistPostProcessingMetadata({ ws, teamId, chatKeyBytes: key, encryptedChatKey, ...metadata });
17249
+ clearSyncCache(teamId);
17250
+ },
17251
+ onMetadata: params.onMetadata,
17252
+ onReleased: () => {
17253
+ this.ownedMetadataSubscriptions.delete(stopOwned);
17254
+ if (this.activeSubmittedChatTurns.get(chatId) === ownedTurnId) this.activeSubmittedChatTurns.delete(chatId);
17255
+ if (metadataTransportOwned) ws.close();
17256
+ }
17090
17257
  });
17091
- if (!current2()) return;
17092
- if (iconChanged || notice.category) {
17093
- const [encryptedIcon, encryptedCategory] = await Promise.all([
17094
- notice.chatIcon ? encryptWithAesGcmCombined(notice.chatIcon, key) : null,
17095
- notice.category ? encryptWithAesGcmCombined(notice.category, key) : null
17096
- ]);
17097
- if (!current2()) return;
17098
- const stored = ws.waitForMessage("encrypted_metadata_stored", (value) => value.chat_id === chatId, 2e4);
17099
- void stored.catch(() => {
17100
- });
17101
- await ws.sendAsync("encrypted_chat_metadata", {
17102
- chat_id: chatId,
17103
- ...teamId ? { team_id: teamId } : {},
17104
- encrypted_chat_key: encryptedChatKey,
17105
- ...encryptedIcon ? { encrypted_icon: encryptedIcon } : {},
17106
- ...encryptedCategory ? { encrypted_chat_category: encryptedCategory } : {}
17107
- });
17108
- await stored;
17109
- }
17110
- if (current2()) clearSyncCache(teamId);
17111
- }).catch(() => {
17112
- });
17113
- };
17114
- const onTurnStream = (event) => {
17115
- 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)) {
17116
- initialHeaderNotice = { ...initialHeaderNotice, ...event };
17117
- startInitialHeader();
17258
+ while (this.ownedMetadataSubscriptions.size >= 8) this.ownedMetadataSubscriptions.values().next().value?.();
17259
+ this.ownedMetadataSubscriptions.add(stopOwned);
17118
17260
  }
17119
- params.onStream?.(event);
17120
- };
17121
- if (!params.incognito && chatKeyBytes && savedTurnId) {
17122
- const key = chatKeyBytes;
17123
- const ownedTurnId = savedTurnId;
17124
- this.activeSubmittedChatTurns.set(chatId, ownedTurnId);
17125
- const owner2 = this.preparedChatOwner(this.requireSession(), teamId);
17126
- const stopOwned = () => optionalMetadata?.stop();
17127
- optionalMetadata = observeOwnedTurnMetadata({
17128
- ws,
17129
- chatId,
17130
- turnId: savedTurnId,
17131
- userMessageId: messageId2,
17132
- closeAfterMetadata: true,
17133
- ownerCurrent: () => this.preparedChatOwnerCurrent(owner2, teamId),
17134
- persist: async (metadata) => {
17135
- await this.persistPostProcessingMetadata({ ws, teamId, chatKeyBytes: key, encryptedChatKey, ...metadata });
17136
- clearSyncCache(teamId);
17137
- },
17138
- onMetadata: params.onMetadata,
17139
- onReleased: () => {
17140
- this.ownedMetadataSubscriptions.delete(stopOwned);
17141
- if (this.activeSubmittedChatTurns.get(chatId) === ownedTurnId) this.activeSubmittedChatTurns.delete(chatId);
17142
- if (metadataTransportOwned) ws.close();
17143
- }
17261
+ if (!params.incognito && shouldWaitForAi) {
17262
+ precollectedResponse = ws.collectAiResponse(messageId2, chatId, {
17263
+ waitForPostProcessing: false,
17264
+ onStream: onTurnStream,
17265
+ onSubChatEvent: async (event) => {
17266
+ await handlersReady;
17267
+ await handleSubChatEvent(event);
17268
+ },
17269
+ onAppSettingsMemoriesRequest: async (event) => {
17270
+ await handlersReady;
17271
+ await handleAppSettingsMemoriesRequest(event);
17272
+ },
17273
+ timeoutMs: params.responseTimeoutMs,
17274
+ recoveryTurnId: savedTurnId,
17275
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17276
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17277
+ });
17278
+ }
17279
+ const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
17280
+ const event = payload;
17281
+ return event.chat_id === chatId && event.user_message_id === messageId2;
17282
+ }, params.responseTimeoutMs ?? 9e4) : null;
17283
+ if (queuedAdmission) void queuedAdmission.catch(() => {
17144
17284
  });
17145
- while (this.ownedMetadataSubscriptions.size >= 8) this.ownedMetadataSubscriptions.values().next().value?.();
17146
- this.ownedMetadataSubscriptions.add(stopOwned);
17147
- }
17148
- if (!params.incognito && shouldWaitForAi) {
17149
- precollectedResponse = ws.collectAiResponse(messageId2, chatId, {
17150
- waitForPostProcessing: false,
17151
- onStream: onTurnStream,
17152
- onSubChatEvent: async (event) => {
17153
- await handlersReady;
17154
- await handleSubChatEvent(event);
17155
- },
17156
- onAppSettingsMemoriesRequest: async (event) => {
17157
- await handlersReady;
17158
- await handleAppSettingsMemoriesRequest(event);
17159
- },
17160
- timeoutMs: params.responseTimeoutMs,
17161
- recoveryTurnId: savedTurnId,
17162
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17163
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17285
+ const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
17286
+ const event = payload;
17287
+ return event.chat_id === chatId && (event.message_id === messageId2 || event.message_id === void 0);
17288
+ }, params.responseTimeoutMs ?? 9e4) : null;
17289
+ if (pausedHandoff) void pausedHandoff.catch(() => {
17164
17290
  });
17165
- }
17166
- const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
17167
- const event = payload;
17168
- return event.chat_id === chatId && event.user_message_id === messageId2;
17169
- }, params.responseTimeoutMs ?? 9e4) : null;
17170
- if (queuedAdmission) void queuedAdmission.catch(() => {
17171
- });
17172
- const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
17173
- const event = payload;
17174
- return event.chat_id === chatId && (event.message_id === messageId2 || event.message_id === void 0);
17175
- }, params.responseTimeoutMs ?? 9e4) : null;
17176
- if (pausedHandoff) void pausedHandoff.catch(() => {
17177
- });
17178
- const confirmed = ws.waitForMessage(
17179
- "chat_message_confirmed",
17180
- (payload) => {
17181
- const eventPayload = payload;
17182
- return eventPayload.chat_id === chatId && eventPayload.message_id === messageId2;
17183
- },
17184
- params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
17185
- );
17186
- let confirmedPayload;
17187
- let admissionContinuationPhase = "before_dispatch";
17188
- let admissionAckObserved = false;
17189
- let routingFrameSent = false;
17190
- let detailsFrameSent = false;
17191
- const admissionContinuationStartedAt = performance.now();
17192
- try {
17193
- if (admissionPayload) {
17194
- if (!useDeferredContext && recoveryDiscovery) await recoveryDiscovery;
17195
- if (recoveryDiscoveryError) throw recoveryDiscoveryError;
17196
- if (!contextRequestId && pendingContext) {
17197
- const context = await pendingContext;
17198
- Object.assign(messagePayload, context);
17199
- Object.assign(admissionPayload.inference_request, context);
17200
- }
17201
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17202
- const admissionAck = ws.waitForMessage("chat_turn_preflight_ack", (payload) => payload.turn_id === savedTurnId);
17203
- void admissionAck.catch(() => {
17204
- });
17205
- const frameDispatchAt = performance.now();
17206
- admissionContinuationPhase = "await_ack";
17207
- await consumeSocketMemoryReceipt();
17208
- await ws.sendAsync(useCombinedAdmission ? "chat_turn_admit" : "chat_turn_preflight", admissionPayload);
17209
- traceSendTiming("admission_frame");
17210
- if (process.env.OPENMATES_DEBUG === "1") {
17211
- 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)}`);
17212
- }
17213
- const ackPayload = (await admissionAck).payload;
17214
- traceSendTiming("admission_ack");
17215
- admissionAckObserved = true;
17216
- if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
17217
- throw new Error("Encrypted chat admission acknowledgement omitted preflight_id.");
17218
- }
17219
- if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
17220
- terminalExpectedMessagesV = ackPayload.committed_messages_v;
17221
- }
17222
- initialAdmissionDurable = true;
17223
- startInitialHeader();
17224
- if (!useCombinedAdmission) {
17225
- Object.assign(messagePayload, { protocol_version: admissionPayload.protocol_version, preflight_id: ackPayload.preflight_id });
17226
- await ws.sendAsync("chat_message_added", messagePayload);
17227
- }
17228
- if (pendingContext && contextRequestId) {
17229
- admissionContinuationPhase = "await_routing";
17230
- const identity3 = {
17231
- chat_id: chatId,
17232
- turn_id: savedTurnId,
17233
- message_id: messageId2,
17234
- request_id: contextRequestId,
17235
- ...teamId ? { team_id: teamId } : {}
17236
- };
17237
- let failureNotice = null;
17238
- const notifyContextFailure = (reason) => {
17239
- if (failureNotice) return failureNotice;
17240
- if (!ws.supportsServerCapability("chat_turn_context_failure_v1") || !ws.isOpen() || !contextOwnerIsCurrent() || !savedTurnId) return Promise.resolve();
17241
- failureNotice = (async () => {
17242
- const failureAck = ws.waitForMessage("chat_turn_context_failed_ack", (payload) => {
17243
- const event = payload;
17244
- return event.chat_id === chatId && event.turn_id === savedTurnId && event.message_id === messageId2 && event.request_id === contextRequestId;
17245
- }, 5e3);
17246
- void failureAck.catch(() => {
17247
- });
17248
- await ws.sendAsync("chat_turn_context_failed", { ...identity3, reason });
17249
- const status = (await failureAck).payload;
17250
- if (status.status !== "accepted" && status.status !== "duplicate") {
17251
- throw new Error("Chat context failure receipt was rejected.");
17252
- }
17253
- })();
17254
- return failureNotice;
17255
- };
17256
- let routing;
17257
- try {
17258
- routing = useSelectionDetailsV2 ? await projectRoutingContext : deferredProjectDirectory ? (await Promise.all([pendingContext, projectRoutingContext]))[0] : params.projectId ? await pendingContext : await Promise.race([routingContext, pendingContext]);
17259
- } catch (error) {
17260
- try {
17261
- await notifyContextFailure(useSelectionDetailsV2 ? "project_discovery_failed" : "context_preparation_failed");
17262
- } catch {
17263
- }
17264
- throw error;
17291
+ const confirmed = ws.waitForMessage(
17292
+ "chat_message_confirmed",
17293
+ (payload) => {
17294
+ const eventPayload = payload;
17295
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId2;
17296
+ },
17297
+ params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
17298
+ );
17299
+ let confirmedPayload;
17300
+ let admissionContinuationPhase = "before_dispatch";
17301
+ let admissionAckObserved = false;
17302
+ let routingFrameSent = false;
17303
+ let detailsFrameSent = false;
17304
+ const admissionContinuationStartedAt = performance.now();
17305
+ try {
17306
+ if (admissionPayload) {
17307
+ if (!useDeferredContext && recoveryDiscovery) await recoveryDiscovery;
17308
+ if (recoveryDiscoveryError) throw recoveryDiscoveryError;
17309
+ if (!contextRequestId && pendingContext) {
17310
+ const context = await pendingContext;
17311
+ Object.assign(messagePayload, context);
17312
+ Object.assign(admissionPayload.inference_request, context);
17265
17313
  }
17266
17314
  if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17267
- let rejectContextAck;
17268
- contextAckRejection = new Promise((_resolve, reject) => {
17269
- rejectContextAck = reject;
17270
- });
17271
- void contextAckRejection.catch(() => {
17315
+ const admissionAck = ws.waitForMessage("chat_turn_preflight_ack", (payload) => payload.turn_id === savedTurnId);
17316
+ void admissionAck.catch(() => {
17272
17317
  });
17273
- let rejectedPhaseSeen = false;
17274
- const attemptedContextPhases = /* @__PURE__ */ new Set();
17275
- removeContextAckWatcher = ws.onMessageType("chat_turn_context_ack", (event) => {
17276
- 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;
17277
- if (event.status === "accepted" || event.status === "duplicate") {
17278
- traceSendTiming(event.phase === "routing" ? "routing_ack" : "details_ack");
17279
- return;
17280
- }
17281
- if (event.status !== "rejected") return;
17282
- rejectedPhaseSeen = true;
17283
- if (!contextOwnerIsCurrent()) {
17284
- rejectContextAck(new Error("Chat workspace changed while preparing context."));
17285
- return;
17318
+ const frameDispatchAt = performance.now();
17319
+ admissionContinuationPhase = "await_ack";
17320
+ await consumeSocketMemoryReceipt();
17321
+ await ws.sendAsync(useCombinedAdmission ? "chat_turn_admit" : "chat_turn_preflight", admissionPayload);
17322
+ traceSendTiming("admission_frame");
17323
+ if (process.env.OPENMATES_DEBUG === "1") {
17324
+ 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)}`);
17325
+ }
17326
+ const ackPayload = (await admissionAck).payload;
17327
+ traceSendTiming("admission_ack");
17328
+ admissionAckObserved = true;
17329
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
17330
+ throw new Error("Encrypted chat admission acknowledgement omitted preflight_id.");
17331
+ }
17332
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
17333
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
17334
+ }
17335
+ initialAdmissionDurable = true;
17336
+ startInitialHeader();
17337
+ if (!useCombinedAdmission) {
17338
+ Object.assign(messagePayload, { protocol_version: admissionPayload.protocol_version, preflight_id: ackPayload.preflight_id });
17339
+ await ws.sendAsync("chat_message_added", messagePayload);
17340
+ }
17341
+ if (pendingContext && contextRequestId) {
17342
+ admissionContinuationPhase = "await_routing";
17343
+ const identity3 = {
17344
+ chat_id: chatId,
17345
+ turn_id: savedTurnId,
17346
+ message_id: messageId2,
17347
+ request_id: contextRequestId,
17348
+ ...teamId ? { team_id: teamId } : {}
17349
+ };
17350
+ let failureNotice = null;
17351
+ const notifyContextFailure = (reason) => {
17352
+ if (failureNotice) return failureNotice;
17353
+ if (!ws.supportsServerCapability("chat_turn_context_failure_v1") || !ws.isOpen() || !contextOwnerIsCurrent() || !savedTurnId) return Promise.resolve();
17354
+ failureNotice = (async () => {
17355
+ const failureAck = ws.waitForMessage("chat_turn_context_failed_ack", (payload) => {
17356
+ const event = payload;
17357
+ return event.chat_id === chatId && event.turn_id === savedTurnId && event.message_id === messageId2 && event.request_id === contextRequestId;
17358
+ }, 5e3);
17359
+ void failureAck.catch(() => {
17360
+ });
17361
+ await ws.sendAsync("chat_turn_context_failed", { ...identity3, reason });
17362
+ const status = (await failureAck).payload;
17363
+ if (status.status !== "accepted" && status.status !== "duplicate") {
17364
+ throw new Error("Chat context failure receipt was rejected.");
17365
+ }
17366
+ })();
17367
+ return failureNotice;
17368
+ };
17369
+ let routing;
17370
+ try {
17371
+ routing = useSelectionDetailsV2 ? await projectRoutingContext : deferredProjectDirectory ? (await Promise.all([pendingContext, projectRoutingContext]))[0] : params.projectId ? await pendingContext : await Promise.race([routingContext, pendingContext]);
17372
+ } catch (error) {
17373
+ try {
17374
+ await notifyContextFailure(useSelectionDetailsV2 ? "project_discovery_failed" : "context_preparation_failed");
17375
+ } catch {
17376
+ }
17377
+ throw error;
17286
17378
  }
17287
- void notifyContextFailure("context_preparation_failed").catch(() => {
17288
- }).then(() => {
17289
- rejectContextAck(new Error("Chat context delivery was rejected."));
17379
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17380
+ let rejectContextAck;
17381
+ contextAckRejection = new Promise((_resolve, reject) => {
17382
+ rejectContextAck = reject;
17290
17383
  });
17291
- });
17292
- const { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } = routing;
17293
- traceSendTiming("routing_prepared");
17294
- attemptedContextPhases.add("routing");
17295
- 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 } });
17296
- routingFrameSent = true;
17297
- traceSendTiming("routing_frame");
17298
- admissionContinuationPhase = "await_details";
17299
- let context;
17300
- try {
17301
- context = await Promise.race([pendingContext, contextAckRejection]);
17302
- } catch (error) {
17384
+ void contextAckRejection.catch(() => {
17385
+ });
17386
+ let rejectedPhaseSeen = false;
17387
+ const attemptedContextPhases = /* @__PURE__ */ new Set();
17388
+ removeContextAckWatcher = ws.onMessageType("chat_turn_context_ack", (event) => {
17389
+ 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;
17390
+ if (event.status === "accepted" || event.status === "duplicate") {
17391
+ traceSendTiming(event.phase === "routing" ? "routing_ack" : "details_ack");
17392
+ return;
17393
+ }
17394
+ if (event.status !== "rejected") return;
17395
+ rejectedPhaseSeen = true;
17396
+ if (!contextOwnerIsCurrent()) {
17397
+ rejectContextAck(new Error("Chat workspace changed while preparing context."));
17398
+ return;
17399
+ }
17400
+ void notifyContextFailure("context_preparation_failed").catch(() => {
17401
+ }).then(() => {
17402
+ rejectContextAck(new Error("Chat context delivery was rejected."));
17403
+ });
17404
+ });
17405
+ const { project_focus_candidates, related_task_candidates, project_focus_catalog, accepted_plan_context } = routing;
17406
+ traceSendTiming("routing_prepared");
17407
+ attemptedContextPhases.add("routing");
17408
+ 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 } });
17409
+ routingFrameSent = true;
17410
+ traceSendTiming("routing_frame");
17411
+ admissionContinuationPhase = "await_details";
17412
+ let context;
17303
17413
  try {
17304
- await notifyContextFailure("context_preparation_failed");
17305
- } catch {
17414
+ context = await Promise.race([pendingContext, contextAckRejection]);
17415
+ } catch (error) {
17416
+ try {
17417
+ await notifyContextFailure("context_preparation_failed");
17418
+ } catch {
17419
+ }
17420
+ throw error;
17306
17421
  }
17307
- throw error;
17422
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17423
+ traceSendTiming("details_prepared");
17424
+ attemptedContextPhases.add("details");
17425
+ await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "details", context: {
17426
+ custom_memory_documents: context.custom_memory_documents ?? [],
17427
+ project_focus_documents: context.project_focus_documents ?? [],
17428
+ project_context_documents: context.project_context_documents ?? [],
17429
+ ...useSelectionDetailsV2 ? {
17430
+ related_task_candidates: context.related_task_candidates ?? [],
17431
+ project_focus_catalog: context.project_focus_catalog ?? [],
17432
+ accepted_plan_context: context.accepted_plan_context ?? null,
17433
+ ...connectedAccountDirectory ? { connected_account_directory: connectedAccountDirectory } : {}
17434
+ } : {}
17435
+ } });
17436
+ detailsFrameSent = true;
17437
+ traceSendTiming("details_frame");
17308
17438
  }
17309
- if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
17310
- traceSendTiming("details_prepared");
17311
- attemptedContextPhases.add("details");
17312
- await ws.sendAsync("chat_turn_context_ready", { ...identity3, phase: "details", context: {
17313
- custom_memory_documents: context.custom_memory_documents ?? [],
17314
- project_focus_documents: context.project_focus_documents ?? [],
17315
- project_context_documents: context.project_context_documents ?? [],
17316
- ...useSelectionDetailsV2 ? {
17317
- related_task_candidates: context.related_task_candidates ?? [],
17318
- project_focus_catalog: context.project_focus_catalog ?? [],
17319
- accepted_plan_context: context.accepted_plan_context ?? null,
17320
- ...connectedAccountDirectory ? { connected_account_directory: connectedAccountDirectory } : {}
17321
- } : {}
17322
- } });
17323
- detailsFrameSent = true;
17324
- traceSendTiming("details_frame");
17439
+ admissionContinuationPhase = "await_confirmation";
17440
+ } else {
17441
+ await consumeSocketMemoryReceipt();
17442
+ await ws.sendAsync("chat_message_added", messagePayload);
17325
17443
  }
17326
- admissionContinuationPhase = "await_confirmation";
17327
- } else {
17328
- await consumeSocketMemoryReceipt();
17329
- await ws.sendAsync("chat_message_added", messagePayload);
17330
- }
17331
- confirmedPayload = (await (contextAckRejection ? Promise.race([confirmed, contextAckRejection]) : confirmed)).payload;
17332
- } catch (error) {
17333
- if (process.env.OPENMATES_DEBUG === "1" && admissionPayload) {
17334
- 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)}`);
17444
+ confirmedPayload = (await (contextAckRejection ? Promise.race([confirmed, contextAckRejection]) : confirmed)).payload;
17445
+ } catch (error) {
17446
+ if (process.env.OPENMATES_DEBUG === "1" && admissionPayload) {
17447
+ 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)}`);
17448
+ }
17449
+ dispatchHandlersReady();
17450
+ void confirmed.catch(() => {
17451
+ });
17452
+ if (precollectedResponse) void precollectedResponse.catch(() => {
17453
+ });
17454
+ ws.close();
17455
+ throw error;
17335
17456
  }
17336
- dispatchHandlersReady();
17337
- void confirmed.catch(() => {
17338
- });
17339
- if (precollectedResponse) void precollectedResponse.catch(() => {
17340
- });
17341
- ws.close();
17342
- throw error;
17343
- }
17344
- if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
17345
- terminalExpectedMessagesV = confirmedPayload.new_messages_v;
17346
- }
17347
- if (!params.incognito && (!teamId || shouldWaitForAi)) {
17348
- clearSyncCache(teamId);
17349
- }
17350
- let assistant = "";
17351
- let assistantMessageId = null;
17352
- let category = null;
17353
- let modelName = null;
17354
- let tokenUsage = null;
17355
- let promptBudget = null;
17356
- let followUpSuggestions = [];
17357
- let suggestedFollowupMessage = null;
17358
- let taskProposals = [];
17359
- let taskUpdateProposals = [];
17360
- let taskEvents = [];
17361
- let pendingTaskUpdateJobs = [];
17362
- let subChatEvents = [];
17363
- const appSettingsMemoryRequests = [];
17364
- const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
17365
- const isApprovalWithinServerLimits = (request) => {
17366
- const count = request.subChats.length;
17367
- if (count === 0) return false;
17368
- if (request.remainingSubChats !== null && count > request.remainingSubChats) {
17369
- return false;
17457
+ if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
17458
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v;
17370
17459
  }
17371
- if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17372
- return false;
17460
+ if (!params.incognito && (!teamId || shouldWaitForAi)) {
17461
+ clearSyncCache(teamId);
17373
17462
  }
17374
- return true;
17375
- };
17376
- const isAutoApprovalWithinServerLimits = (request) => {
17377
- if (!isApprovalWithinServerLimits(request)) return false;
17378
- return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
17379
- };
17380
- const handleSubChatEvent = async (event) => {
17381
- params.onSubChatEvent?.(event);
17382
- if (event.type === "spawn_sub_chats") {
17383
- if (params.incognito || !chatKeyBytes) return;
17384
- const rawSubChats = event.payload.sub_chats;
17385
- if (!Array.isArray(rawSubChats)) return;
17386
- const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
17387
- parentChatId: chatId,
17388
- parentChatKey: chatKeyBytes,
17389
- encryptedParentChatKey: encryptedChatKey,
17390
- subChats: rawSubChats
17391
- });
17392
- for (const metadataPayload of metadataPayloads) {
17393
- await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
17463
+ let assistant = "";
17464
+ let assistantMessageId = null;
17465
+ let category = null;
17466
+ let modelName = null;
17467
+ let tokenUsage = null;
17468
+ let promptBudget = null;
17469
+ let followUpSuggestions = [];
17470
+ let suggestedFollowupMessage = null;
17471
+ let taskProposals = [];
17472
+ let taskUpdateProposals = [];
17473
+ let taskEvents = [];
17474
+ let pendingTaskUpdateJobs = [];
17475
+ let subChatEvents = [];
17476
+ const appSettingsMemoryRequests = [];
17477
+ const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
17478
+ const isApprovalWithinServerLimits = (request) => {
17479
+ const count = request.subChats.length;
17480
+ if (count === 0) return false;
17481
+ if (request.remainingSubChats !== null && count > request.remainingSubChats) {
17482
+ return false;
17394
17483
  }
17395
- return;
17396
- }
17397
- if (event.type !== "sub_chat_confirmation_required") return;
17398
- const payload = event.payload;
17399
- const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
17400
- const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
17401
- const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
17402
- if (!taskId) return;
17403
- const request = {
17404
- chatId: confirmationChatId,
17405
- taskId,
17406
- subChats,
17407
- maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
17408
- maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
17409
- existingSubChats: numberOrNull(payload.existing_sub_chats),
17410
- remainingSubChats: numberOrNull(payload.remaining_sub_chats)
17411
- };
17412
- const withinLimits = isApprovalWithinServerLimits(request);
17413
- const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
17414
- await ws.sendAsync(
17415
- "sub_chat_confirmation",
17416
- buildSubChatConfirmationPayload({
17417
- chatId: request.chatId,
17418
- taskId: request.taskId,
17419
- approved,
17420
- approveCount: approved ? request.subChats.length : void 0
17421
- })
17422
- );
17423
- };
17424
- const persistSystemMessage = async (systemMessage) => {
17425
- if (!chatKeyBytes) return;
17426
- await ws.sendAsync("chat_system_message_added", {
17427
- chat_id: chatId,
17428
- message: {
17429
- message_id: systemMessage.message_id,
17430
- role: "system",
17431
- encrypted_content: await encryptWithAesGcmCombined(
17432
- systemMessage.content,
17433
- chatKeyBytes
17434
- ),
17435
- created_at: systemMessage.created_at,
17436
- user_message_id: systemMessage.user_message_id
17484
+ if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
17485
+ return false;
17437
17486
  }
17438
- });
17439
- await ws.waitForMessage(
17440
- "system_message_confirmed",
17441
- (payload) => {
17442
- const p = payload;
17443
- return p.message_id === systemMessage.message_id;
17444
- },
17445
- 2e4
17446
- );
17447
- };
17448
- const persistMemoryRequestSystemMessage = async (event) => {
17449
- const requestId = event.requestId ?? `${messageId2}-memory-request`;
17450
- await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
17451
- userMessageId: messageId2,
17452
- requestId,
17453
- requestedKeys: event.requestedKeys,
17454
- entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
17455
- createdAt: Math.floor(Date.now() / 1e3)
17456
- }));
17457
- };
17458
- const responseCategoriesFromMemories = (approvedMemories) => {
17459
- const counts = /* @__PURE__ */ new Map();
17460
- for (const memory of approvedMemories) {
17461
- const key = `${memory.app_id}-${memory.item_key}`;
17462
- const existing = counts.get(key);
17463
- if (existing) {
17464
- existing.entryCount += 1;
17465
- } else {
17466
- counts.set(key, {
17467
- appId: memory.app_id,
17468
- itemType: memory.item_key,
17469
- entryCount: 1
17487
+ return true;
17488
+ };
17489
+ const isAutoApprovalWithinServerLimits = (request) => {
17490
+ if (!isApprovalWithinServerLimits(request)) return false;
17491
+ return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
17492
+ };
17493
+ const handleSubChatEvent = async (event) => {
17494
+ params.onSubChatEvent?.(event);
17495
+ if (event.type === "spawn_sub_chats") {
17496
+ if (params.incognito || !chatKeyBytes) return;
17497
+ const rawSubChats = event.payload.sub_chats;
17498
+ if (!Array.isArray(rawSubChats)) return;
17499
+ const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
17500
+ parentChatId: chatId,
17501
+ parentChatKey: chatKeyBytes,
17502
+ encryptedParentChatKey: encryptedChatKey,
17503
+ subChats: rawSubChats
17470
17504
  });
17505
+ for (const metadataPayload of metadataPayloads) {
17506
+ await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
17507
+ }
17508
+ return;
17471
17509
  }
17472
- }
17473
- return [...counts.values()];
17474
- };
17475
- const handleAppSettingsMemoriesRequest = async (event) => {
17476
- await persistMemoryRequestSystemMessage(event);
17477
- if (params.autoApproveMemories) {
17478
- const requested = new Set(event.requestedKeys);
17479
- const unadvertisedKeys = event.requestedKeys.filter(
17480
- (key) => !memoryMetadataKeys.includes(key)
17481
- );
17482
- if (unadvertisedKeys.length > 0) {
17483
- throw new Error(
17484
- `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
17485
- );
17486
- }
17487
- const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
17488
- app_id: memory.app_id,
17489
- item_key: memory.item_type,
17490
- content: memory.data
17491
- }));
17492
- const approvedKeys = [
17493
- ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
17494
- ];
17495
- const categories = responseCategoriesFromMemories(approvedMemories);
17496
- appSettingsMemoryRequests.push({
17497
- requestId: event.requestId,
17498
- requestedKeys: event.requestedKeys,
17499
- approvedKeys,
17500
- entryCount: approvedMemories.length
17501
- });
17502
- await ws.sendAsync("app_settings_memories_confirmed", {
17503
- chat_id: event.chatId,
17504
- app_settings_memories: approvedMemories
17505
- });
17506
- await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17507
- userMessageId: messageId2,
17508
- messageId: `${messageId2}-memory-response`,
17509
- action: "included",
17510
- categories,
17511
- createdAt: Math.floor(Date.now() / 1e3)
17512
- }));
17513
- return;
17514
- }
17515
- appSettingsMemoryRequests.push({
17516
- requestId: event.requestId,
17517
- requestedKeys: event.requestedKeys,
17518
- approvedKeys: [],
17519
- entryCount: 0
17520
- });
17521
- throw new Error(
17522
- `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.`
17523
- );
17524
- };
17525
- const persistedTaskEventIds = /* @__PURE__ */ new Set();
17526
- const persistTaskEventSystemMessages = async (events) => {
17527
- if (!chatKeyBytes || events.length === 0) return;
17528
- for (const event of events) {
17529
- if (persistedTaskEventIds.has(event.event_id)) continue;
17530
- await this.persistEncryptedSystemMessage(
17531
- ws,
17532
- await buildTaskEventSystemMessage({
17533
- chatKey: chatKeyBytes,
17534
- userMessageId: messageId2,
17535
- event
17536
- }),
17537
- chatId
17510
+ if (event.type !== "sub_chat_confirmation_required") return;
17511
+ const payload = event.payload;
17512
+ const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
17513
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
17514
+ const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
17515
+ if (!taskId) return;
17516
+ const request = {
17517
+ chatId: confirmationChatId,
17518
+ taskId,
17519
+ subChats,
17520
+ maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
17521
+ maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
17522
+ existingSubChats: numberOrNull(payload.existing_sub_chats),
17523
+ remainingSubChats: numberOrNull(payload.remaining_sub_chats)
17524
+ };
17525
+ const withinLimits = isApprovalWithinServerLimits(request);
17526
+ const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
17527
+ await ws.sendAsync(
17528
+ "sub_chat_confirmation",
17529
+ buildSubChatConfirmationPayload({
17530
+ chatId: request.chatId,
17531
+ taskId: request.taskId,
17532
+ approved,
17533
+ approveCount: approved ? request.subChats.length : void 0
17534
+ })
17538
17535
  );
17539
- persistedTaskEventIds.add(event.event_id);
17540
- }
17541
- };
17542
- const streamOpts = {
17543
- waitForPostProcessing: false,
17544
- onStream: onTurnStream,
17545
- onSubChatEvent: handleSubChatEvent,
17546
- onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
17547
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17548
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17549
- };
17550
- dispatchHandlersReady();
17551
- if (!shouldWaitForAi) {
17552
- ws.close();
17553
- } else if (params.incognito) {
17554
- try {
17555
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17556
- ...streamOpts,
17557
- timeoutMs: params.responseTimeoutMs,
17558
- recoveryTurnId: savedTurnId
17559
- }));
17560
- assistantMessageId = resp.messageId;
17561
- assistant = resp.content;
17562
- category = resp.category;
17563
- modelName = resp.modelName;
17564
- tokenUsage = resp.tokenUsage;
17565
- promptBudget = resp.promptBudget;
17566
- taskEvents = resp.taskEvents;
17567
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17568
- subChatEvents = resp.subChatEvents;
17569
- if (resp.status === "waiting_for_user") {
17570
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17571
- return {
17572
- status: resp.status,
17573
- chatId,
17574
- messageId: assistantMessageId,
17575
- assistant,
17576
- category,
17577
- modelName,
17578
- mateName: category ? MATE_NAMES[category] ?? null : null,
17579
- followUpSuggestions,
17580
- taskProposals,
17581
- taskUpdateProposals,
17582
- taskEvents,
17583
- pendingTaskUpdateJobs,
17584
- subChatEvents,
17585
- appSettingsMemoryRequests,
17586
- tokenUsage,
17587
- promptBudget
17588
- };
17589
- }
17590
- } finally {
17591
- ws.close();
17592
- }
17593
- } else {
17594
- try {
17595
- const recoverCurrentTurnOutputs = async () => {
17596
- if (recoveryDiscovery) {
17597
- try {
17598
- await recoveryDiscovery;
17599
- } catch (error) {
17600
- if (!useDeferredContext) throw error;
17601
- return;
17602
- }
17536
+ };
17537
+ const persistSystemMessage = async (systemMessage) => {
17538
+ if (!chatKeyBytes) return;
17539
+ await ws.sendAsync("chat_system_message_added", {
17540
+ chat_id: chatId,
17541
+ message: {
17542
+ message_id: systemMessage.message_id,
17543
+ role: "system",
17544
+ encrypted_content: await encryptWithAesGcmCombined(
17545
+ systemMessage.content,
17546
+ chatKeyBytes
17547
+ ),
17548
+ created_at: systemMessage.created_at,
17549
+ user_message_id: systemMessage.user_message_id
17603
17550
  }
17604
- const pages = ws.drainAvailableRecoveryOutputPages();
17605
- if (pages.length === 0) return;
17606
- if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
17607
- if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
17608
- throw new Error("Saved chat recovery output requires the root chat key.");
17551
+ });
17552
+ await ws.waitForMessage(
17553
+ "system_message_confirmed",
17554
+ (payload) => {
17555
+ const p = payload;
17556
+ return p.message_id === systemMessage.message_id;
17557
+ },
17558
+ 2e4
17559
+ );
17560
+ };
17561
+ const persistMemoryRequestSystemMessage = async (event) => {
17562
+ const requestId = event.requestId ?? `${messageId2}-memory-request`;
17563
+ await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
17564
+ userMessageId: messageId2,
17565
+ requestId,
17566
+ requestedKeys: event.requestedKeys,
17567
+ entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
17568
+ createdAt: Math.floor(Date.now() / 1e3)
17569
+ }));
17570
+ };
17571
+ const responseCategoriesFromMemories = (approvedMemories) => {
17572
+ const counts = /* @__PURE__ */ new Map();
17573
+ for (const memory of approvedMemories) {
17574
+ const key = `${memory.app_id}-${memory.item_key}`;
17575
+ const existing = counts.get(key);
17576
+ if (existing) {
17577
+ existing.entryCount += 1;
17578
+ } else {
17579
+ counts.set(key, {
17580
+ appId: memory.app_id,
17581
+ itemType: memory.item_key,
17582
+ entryCount: 1
17583
+ });
17609
17584
  }
17610
- const cache = loadSyncCache(teamId) ?? {
17611
- syncedAt: Date.now(),
17612
- totalChatCount: 0,
17613
- loadedChatCount: 0,
17614
- chats: [],
17615
- embeds: [],
17616
- embedKeys: [],
17617
- chatKeyWrappers: []
17618
- };
17619
- if (encryptedChatKey) {
17620
- const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
17621
- if (root) root.details.encrypted_chat_key = encryptedChatKey;
17622
- else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
17585
+ }
17586
+ return [...counts.values()];
17587
+ };
17588
+ const handleAppSettingsMemoriesRequest = async (event) => {
17589
+ await persistMemoryRequestSystemMessage(event);
17590
+ if (params.autoApproveMemories) {
17591
+ const requested = new Set(event.requestedKeys);
17592
+ const unadvertisedKeys = event.requestedKeys.filter(
17593
+ (key) => !memoryMetadataKeys.includes(key)
17594
+ );
17595
+ if (unadvertisedKeys.length > 0) {
17596
+ throw new Error(
17597
+ `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
17598
+ );
17623
17599
  }
17624
- await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
17625
- };
17626
- const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17627
- ...streamOpts,
17628
- timeoutMs: params.responseTimeoutMs,
17629
- recoveryTurnId: savedTurnId
17630
- });
17631
- const outcome = await Promise.race([
17632
- responsePromise.then((response) => ({ kind: "response", response })),
17633
- ...contextAckRejection ? [contextAckRejection] : [],
17634
- ...queuedAdmission ? [queuedAdmission.then(
17635
- (frame) => ({ kind: "queued", frame }),
17636
- () => new Promise(() => {
17637
- })
17638
- )] : [],
17639
- ...pausedHandoff ? [pausedHandoff.then(
17640
- (frame) => ({ kind: "paused", frame }),
17641
- () => new Promise(() => {
17642
- })
17643
- )] : []
17644
- ]);
17645
- if (outcome.kind === "paused") {
17646
- void responsePromise.catch(() => {
17600
+ const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
17601
+ app_id: memory.app_id,
17602
+ item_key: memory.item_type,
17603
+ content: memory.data
17604
+ }));
17605
+ const approvedKeys = [
17606
+ ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
17607
+ ];
17608
+ const categories = responseCategoriesFromMemories(approvedMemories);
17609
+ appSettingsMemoryRequests.push({
17610
+ requestId: event.requestId,
17611
+ requestedKeys: event.requestedKeys,
17612
+ approvedKeys,
17613
+ entryCount: approvedMemories.length
17647
17614
  });
17648
- throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
17649
- }
17650
- if (outcome.kind === "queued") {
17651
- void responsePromise.catch(() => {
17615
+ await ws.sendAsync("app_settings_memories_confirmed", {
17616
+ chat_id: event.chatId,
17617
+ app_settings_memories: approvedMemories
17652
17618
  });
17653
- const frame = outcome.frame.payload;
17654
- return {
17655
- status: "queued",
17656
- chatId,
17657
- messageId: null,
17619
+ await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
17658
17620
  userMessageId: messageId2,
17659
- queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
17660
- queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
17661
- assistant: "",
17662
- category: null,
17663
- modelName: null,
17664
- mateName: null,
17665
- followUpSuggestions: [],
17666
- taskProposals: [],
17667
- taskUpdateProposals: [],
17668
- taskEvents: [],
17669
- pendingTaskUpdateJobs: [],
17670
- subChatEvents: [],
17671
- appSettingsMemoryRequests: [],
17672
- tokenUsage: null,
17673
- promptBudget: null
17674
- };
17621
+ messageId: `${messageId2}-memory-response`,
17622
+ action: "included",
17623
+ categories,
17624
+ createdAt: Math.floor(Date.now() / 1e3)
17625
+ }));
17626
+ return;
17675
17627
  }
17676
- const resp = outcome.response;
17677
- assistantMessageId = resp.messageId;
17678
- assistant = resp.content;
17679
- category = resp.category;
17680
- modelName = resp.modelName;
17681
- tokenUsage = resp.tokenUsage;
17682
- promptBudget = resp.promptBudget;
17683
- followUpSuggestions = resp.followUpSuggestions;
17684
- suggestedFollowupMessage = resp.suggestedFollowupMessage;
17685
- taskProposals = resp.taskProposals;
17686
- taskUpdateProposals = resp.taskUpdateProposals;
17687
- taskEvents = resp.taskEvents;
17688
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17689
- subChatEvents = resp.subChatEvents;
17690
- if (resp.status === "waiting_for_user") {
17691
- await persistTaskEventSystemMessages(taskEvents);
17692
- await recoverCurrentTurnOutputs();
17693
- return {
17694
- status: resp.status,
17695
- chatId,
17696
- messageId: assistantMessageId,
17697
- assistant,
17698
- category,
17699
- modelName,
17700
- mateName: category ? MATE_NAMES[category] ?? null : null,
17701
- followUpSuggestions,
17702
- taskProposals,
17703
- taskUpdateProposals,
17704
- taskEvents,
17705
- pendingTaskUpdateJobs,
17706
- subChatEvents,
17707
- appSettingsMemoryRequests,
17708
- tokenUsage,
17709
- promptBudget
17710
- };
17628
+ appSettingsMemoryRequests.push({
17629
+ requestId: event.requestId,
17630
+ requestedKeys: event.requestedKeys,
17631
+ approvedKeys: [],
17632
+ entryCount: 0
17633
+ });
17634
+ throw new Error(
17635
+ `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.`
17636
+ );
17637
+ };
17638
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
17639
+ const persistTaskEventSystemMessages = async (events) => {
17640
+ if (!chatKeyBytes || events.length === 0) return;
17641
+ for (const event of events) {
17642
+ if (persistedTaskEventIds.has(event.event_id)) continue;
17643
+ await this.persistEncryptedSystemMessage(
17644
+ ws,
17645
+ await buildTaskEventSystemMessage({
17646
+ chatKey: chatKeyBytes,
17647
+ userMessageId: messageId2,
17648
+ event
17649
+ }),
17650
+ chatId
17651
+ );
17652
+ persistedTaskEventIds.add(event.event_id);
17711
17653
  }
17712
- if (chatKeyBytes) {
17713
- if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
17714
- throw new Error("Saved chat completion did not include recoverable terminal identity.");
17654
+ };
17655
+ const streamOpts = {
17656
+ waitForPostProcessing: false,
17657
+ onStream: onTurnStream,
17658
+ onSubChatEvent: handleSubChatEvent,
17659
+ onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
17660
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
17661
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
17662
+ };
17663
+ dispatchHandlersReady();
17664
+ if (!shouldWaitForAi) {
17665
+ ws.close();
17666
+ } else if (params.incognito) {
17667
+ try {
17668
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17669
+ ...streamOpts,
17670
+ timeoutMs: params.responseTimeoutMs,
17671
+ recoveryTurnId: savedTurnId
17672
+ }));
17673
+ assistantMessageId = resp.messageId;
17674
+ assistant = resp.content;
17675
+ category = resp.category;
17676
+ modelName = resp.modelName;
17677
+ tokenUsage = resp.tokenUsage;
17678
+ promptBudget = resp.promptBudget;
17679
+ taskEvents = resp.taskEvents;
17680
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17681
+ subChatEvents = resp.subChatEvents;
17682
+ if (resp.status === "waiting_for_user") {
17683
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17684
+ return {
17685
+ status: resp.status,
17686
+ chatId,
17687
+ messageId: assistantMessageId,
17688
+ assistant,
17689
+ category,
17690
+ modelName,
17691
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17692
+ followUpSuggestions,
17693
+ taskProposals,
17694
+ taskUpdateProposals,
17695
+ taskEvents,
17696
+ pendingTaskUpdateJobs,
17697
+ subChatEvents,
17698
+ appSettingsMemoryRequests,
17699
+ tokenUsage,
17700
+ promptBudget
17701
+ };
17715
17702
  }
17716
- const recoveryJobId = resp.recoveryJobId;
17717
- let claimRetryDeadline = null;
17718
- let claim;
17719
- let claimRetryDelayMs = 500;
17720
- while (true) {
17721
- const requestId = randomUUID9();
17722
- let matchingLeaseConflict = false;
17723
- const stopWatchingErrors = ws.onMessageType("error", (payload) => {
17724
- if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
17725
- matchingLeaseConflict = true;
17726
- }
17727
- });
17728
- const claimPromise = ws.waitForMessage(
17729
- "recovery_job_claimed",
17730
- (payload) => {
17731
- const received = payload;
17732
- return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
17733
- },
17734
- claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
17735
- );
17736
- try {
17703
+ } finally {
17704
+ ws.close();
17705
+ }
17706
+ } else {
17707
+ try {
17708
+ const recoverCurrentTurnOutputs = async () => {
17709
+ if (recoveryDiscovery) {
17737
17710
  try {
17738
- await ws.sendAsync("recovery_job_claim", {
17739
- protocol_version: 1,
17740
- job_id: recoveryJobId,
17741
- request_id: requestId
17742
- });
17711
+ await recoveryDiscovery;
17743
17712
  } catch (error) {
17744
- void claimPromise.catch(() => {
17745
- });
17746
- throw error;
17713
+ if (!useDeferredContext) throw error;
17714
+ return;
17747
17715
  }
17748
- claim = (await claimPromise).payload;
17749
- break;
17750
- } catch (error) {
17751
- if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
17752
- claimRetryDeadline ??= Date.now() + Math.min(
17753
- 1e4,
17754
- Math.max(0, params.responseTimeoutMs ?? 9e4)
17755
- );
17756
- const remainingMs = claimRetryDeadline - Date.now();
17757
- if (remainingMs <= claimRetryDelayMs) throw error;
17758
- await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
17759
- claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
17760
- } finally {
17761
- stopWatchingErrors();
17762
17716
  }
17717
+ const pages = ws.drainAvailableRecoveryOutputPages();
17718
+ if (pages.length === 0) return;
17719
+ if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
17720
+ if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
17721
+ throw new Error("Saved chat recovery output requires the root chat key.");
17722
+ }
17723
+ const cache = loadSyncCache(teamId) ?? {
17724
+ syncedAt: Date.now(),
17725
+ totalChatCount: 0,
17726
+ loadedChatCount: 0,
17727
+ chats: [],
17728
+ embeds: [],
17729
+ embedKeys: [],
17730
+ chatKeyWrappers: []
17731
+ };
17732
+ if (encryptedChatKey) {
17733
+ const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
17734
+ if (root) root.details.encrypted_chat_key = encryptedChatKey;
17735
+ else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
17736
+ }
17737
+ await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
17738
+ };
17739
+ const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId2, chatId, {
17740
+ ...streamOpts,
17741
+ timeoutMs: params.responseTimeoutMs,
17742
+ recoveryTurnId: savedTurnId
17743
+ });
17744
+ const outcome = await Promise.race([
17745
+ responsePromise.then((response) => ({ kind: "response", response })),
17746
+ ...contextAckRejection ? [contextAckRejection] : [],
17747
+ ...queuedAdmission ? [queuedAdmission.then(
17748
+ (frame) => ({ kind: "queued", frame }),
17749
+ () => new Promise(() => {
17750
+ })
17751
+ )] : [],
17752
+ ...pausedHandoff ? [pausedHandoff.then(
17753
+ (frame) => ({ kind: "paused", frame }),
17754
+ () => new Promise(() => {
17755
+ })
17756
+ )] : []
17757
+ ]);
17758
+ if (outcome.kind === "paused") {
17759
+ void responsePromise.catch(() => {
17760
+ });
17761
+ throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
17763
17762
  }
17764
- const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
17765
- const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
17766
- const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
17767
- const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
17768
- const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
17769
- if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
17770
- assistant = resp.content;
17771
- category = resp.category;
17772
- modelName = resp.modelName;
17773
- assistantMessageId = assistantId;
17774
- await this.persistStreamedEmbeds({
17775
- ws,
17776
- embeds: resp.embeds,
17777
- chatId,
17778
- chatKeyBytes,
17779
- fallbackMessageId: assistantId,
17780
- ownerId: ownerId2,
17781
- teamId
17763
+ if (outcome.kind === "queued") {
17764
+ void responsePromise.catch(() => {
17782
17765
  });
17783
- clearSyncCache(teamId);
17784
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17766
+ const frame = outcome.frame.payload;
17767
+ return {
17768
+ status: "queued",
17769
+ chatId,
17770
+ messageId: null,
17771
+ userMessageId: messageId2,
17772
+ queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
17773
+ queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
17774
+ assistant: "",
17775
+ category: null,
17776
+ modelName: null,
17777
+ mateName: null,
17778
+ followUpSuggestions: [],
17779
+ taskProposals: [],
17780
+ taskUpdateProposals: [],
17781
+ taskEvents: [],
17782
+ pendingTaskUpdateJobs: [],
17783
+ subChatEvents: [],
17784
+ appSettingsMemoryRequests: [],
17785
+ tokenUsage: null,
17786
+ promptBudget: null
17787
+ };
17788
+ }
17789
+ const resp = outcome.response;
17790
+ assistantMessageId = resp.messageId;
17791
+ assistant = resp.content;
17792
+ category = resp.category;
17793
+ modelName = resp.modelName;
17794
+ tokenUsage = resp.tokenUsage;
17795
+ promptBudget = resp.promptBudget;
17796
+ followUpSuggestions = resp.followUpSuggestions;
17797
+ suggestedFollowupMessage = resp.suggestedFollowupMessage;
17798
+ taskProposals = resp.taskProposals;
17799
+ taskUpdateProposals = resp.taskUpdateProposals;
17800
+ taskEvents = resp.taskEvents;
17801
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
17802
+ subChatEvents = resp.subChatEvents;
17803
+ if (resp.status === "waiting_for_user") {
17785
17804
  await persistTaskEventSystemMessages(taskEvents);
17786
17805
  await recoverCurrentTurnOutputs();
17787
- await projectFocusCountdown?.flush();
17788
- await focusPersistence;
17789
- await chatContextEvents?.flush();
17790
- if (focusPersistenceError) throw focusPersistenceError;
17791
- const mateName2 = category ? MATE_NAMES[category] ?? null : null;
17792
17806
  return {
17793
- status: "completed",
17807
+ status: resp.status,
17794
17808
  chatId,
17795
17809
  messageId: assistantMessageId,
17796
17810
  assistant,
17797
17811
  category,
17798
17812
  modelName,
17799
- mateName: mateName2,
17813
+ mateName: category ? MATE_NAMES[category] ?? null : null,
17800
17814
  followUpSuggestions,
17801
17815
  taskProposals,
17802
17816
  taskUpdateProposals,
@@ -17808,231 +17822,333 @@ var OpenMatesClient = class _OpenMatesClient {
17808
17822
  promptBudget
17809
17823
  };
17810
17824
  }
17811
- 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") {
17812
- throw new Error("Recovery job claim returned invalid lease or identity data.");
17813
- }
17814
- const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
17815
- Buffer.from(chatKeyBytes).toString("base64url"),
17816
- chatId,
17817
- keyVersion
17818
- );
17819
- let envelope;
17820
- try {
17821
- envelope = JSON.parse(claim.sealed_payload);
17822
- } catch {
17823
- throw new Error("Recovery job contained an invalid sealed envelope.");
17824
- }
17825
- const plaintext = await openChatCompletionRecoveryEnvelope(
17826
- envelope,
17827
- {
17828
- recoveryPrivateKey: recoveryKeypair.privateKey,
17829
- ownerId: ownerId2,
17825
+ if (chatKeyBytes) {
17826
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
17827
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
17828
+ }
17829
+ const recoveryJobId = resp.recoveryJobId;
17830
+ let claimRetryDeadline = null;
17831
+ let claim;
17832
+ let claimRetryDelayMs = 500;
17833
+ while (true) {
17834
+ const requestId = randomUUID9();
17835
+ let matchingLeaseConflict = false;
17836
+ const stopWatchingErrors = ws.onMessageType("error", (payload) => {
17837
+ if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
17838
+ matchingLeaseConflict = true;
17839
+ }
17840
+ });
17841
+ const claimPromise = ws.waitForMessage(
17842
+ "recovery_job_claimed",
17843
+ (payload) => {
17844
+ const received = payload;
17845
+ return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
17846
+ },
17847
+ claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
17848
+ );
17849
+ try {
17850
+ try {
17851
+ await ws.sendAsync("recovery_job_claim", {
17852
+ protocol_version: 1,
17853
+ job_id: recoveryJobId,
17854
+ request_id: requestId
17855
+ });
17856
+ } catch (error) {
17857
+ void claimPromise.catch(() => {
17858
+ });
17859
+ throw error;
17860
+ }
17861
+ claim = (await claimPromise).payload;
17862
+ break;
17863
+ } catch (error) {
17864
+ if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
17865
+ claimRetryDeadline ??= Date.now() + Math.min(
17866
+ 1e4,
17867
+ Math.max(0, params.responseTimeoutMs ?? 9e4)
17868
+ );
17869
+ const remainingMs = claimRetryDeadline - Date.now();
17870
+ if (remainingMs <= claimRetryDelayMs) throw error;
17871
+ await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
17872
+ claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
17873
+ } finally {
17874
+ stopWatchingErrors();
17875
+ }
17876
+ }
17877
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
17878
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
17879
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
17880
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
17881
+ const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
17882
+ if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
17883
+ assistant = resp.content;
17884
+ category = resp.category;
17885
+ modelName = resp.modelName;
17886
+ assistantMessageId = assistantId;
17887
+ await this.persistStreamedEmbeds({
17888
+ ws,
17889
+ embeds: resp.embeds,
17890
+ chatId,
17891
+ chatKeyBytes,
17892
+ fallbackMessageId: assistantId,
17893
+ ownerId: ownerId2,
17894
+ teamId
17895
+ });
17896
+ clearSyncCache(teamId);
17897
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17898
+ await persistTaskEventSystemMessages(taskEvents);
17899
+ await recoverCurrentTurnOutputs();
17900
+ await projectFocusCountdown?.flush();
17901
+ await focusPersistence;
17902
+ await chatContextEvents?.flush();
17903
+ if (focusPersistenceError) throw focusPersistenceError;
17904
+ const mateName2 = category ? MATE_NAMES[category] ?? null : null;
17905
+ return {
17906
+ status: "completed",
17907
+ chatId,
17908
+ messageId: assistantMessageId,
17909
+ assistant,
17910
+ category,
17911
+ modelName,
17912
+ mateName: mateName2,
17913
+ followUpSuggestions,
17914
+ taskProposals,
17915
+ taskUpdateProposals,
17916
+ taskEvents,
17917
+ pendingTaskUpdateJobs,
17918
+ subChatEvents,
17919
+ appSettingsMemoryRequests,
17920
+ tokenUsage,
17921
+ promptBudget
17922
+ };
17923
+ }
17924
+ 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") {
17925
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
17926
+ }
17927
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
17928
+ Buffer.from(chatKeyBytes).toString("base64url"),
17830
17929
  chatId,
17831
- turnId: savedTurnId,
17832
- jobId: recoveryJobId,
17833
- assistantMessageId: assistantId,
17834
17930
  keyVersion
17931
+ );
17932
+ let envelope;
17933
+ try {
17934
+ envelope = JSON.parse(claim.sealed_payload);
17935
+ } catch {
17936
+ throw new Error("Recovery job contained an invalid sealed envelope.");
17835
17937
  }
17836
- );
17837
- let recovered;
17838
- try {
17839
- recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
17840
- } catch {
17841
- throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
17842
- }
17843
- const expectedRecoveryFields = [
17844
- "assistant_message_id",
17845
- "category",
17846
- "chat_id",
17847
- "content",
17848
- "job_id",
17849
- "key_version",
17850
- "model_name",
17851
- "turn_id"
17852
- ];
17853
- if (Object.keys(recovered).filter((field3) => ![
17854
- "thinking_content",
17855
- "thinking_signature",
17856
- "has_thinking",
17857
- "thinking_token_count",
17858
- "thinking_duration_seconds"
17859
- ].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") {
17860
- throw new Error("Recovery job plaintext did not match the terminal completion identity.");
17861
- }
17862
- assistant = recovered.content;
17863
- category = recovered.category;
17864
- modelName = recovered.model_name;
17865
- const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
17866
- const encryptedAssistantContent = await encryptWithAesGcmCombined(
17867
- assistant,
17868
- chatKeyBytes
17869
- );
17870
- const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
17871
- const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
17872
- const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
17873
- const terminalPersistPayload = {
17874
- protocol_version: 1,
17875
- job_id: recoveryJobId,
17876
- lease_token: leaseToken,
17877
- lease_generation: leaseGeneration,
17878
- encrypted_assistant_message: {
17879
- client_message_id: assistantId,
17880
- chat_id: chatId,
17881
- encrypted_content: encryptedAssistantContent,
17882
- ...await this.encryptThinkingFields(recovered, chatKeyBytes),
17883
- encrypted_sender_name: encryptedSenderName,
17884
- encrypted_category: encryptedCategory,
17885
- encrypted_model_name: encryptedModelName,
17886
- role: "assistant",
17887
- user_message_id: messageId2,
17888
- created_at: completedAt,
17889
- updated_at: completedAt
17938
+ const plaintext = await openChatCompletionRecoveryEnvelope(
17939
+ envelope,
17940
+ {
17941
+ recoveryPrivateKey: recoveryKeypair.privateKey,
17942
+ ownerId: ownerId2,
17943
+ chatId,
17944
+ turnId: savedTurnId,
17945
+ jobId: recoveryJobId,
17946
+ assistantMessageId: assistantId,
17947
+ keyVersion
17948
+ }
17949
+ );
17950
+ let recovered;
17951
+ try {
17952
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
17953
+ } catch {
17954
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
17890
17955
  }
17891
- };
17892
- const persistTerminalRecovery = async (expectedMessagesV) => {
17893
- const persistedPromise = ws.waitForMessage(
17894
- "recovery_job_persisted",
17895
- (payload) => payload.job_id === recoveryJobId,
17896
- 2e4
17956
+ const expectedRecoveryFields = [
17957
+ "assistant_message_id",
17958
+ "category",
17959
+ "chat_id",
17960
+ "content",
17961
+ "job_id",
17962
+ "key_version",
17963
+ "model_name",
17964
+ "turn_id"
17965
+ ];
17966
+ if (Object.keys(recovered).filter((field3) => ![
17967
+ "thinking_content",
17968
+ "thinking_signature",
17969
+ "has_thinking",
17970
+ "thinking_token_count",
17971
+ "thinking_duration_seconds"
17972
+ ].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") {
17973
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
17974
+ }
17975
+ assistant = recovered.content;
17976
+ category = recovered.category;
17977
+ modelName = recovered.model_name;
17978
+ const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
17979
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
17980
+ assistant,
17981
+ chatKeyBytes
17897
17982
  );
17898
- await ws.sendAsync("recovery_job_persist", {
17899
- ...terminalPersistPayload,
17900
- expected_messages_v: expectedMessagesV
17901
- });
17902
- await persistedPromise;
17903
- };
17904
- let terminalPersisted = false;
17905
- try {
17906
- await persistTerminalRecovery(terminalExpectedMessagesV);
17907
- terminalPersisted = true;
17908
- } catch (error) {
17909
- if (error instanceof WebSocketProtocolError) {
17910
- if (error.code === "version_conflict") {
17911
- const latestCache = await this.ensureSynced(
17912
- true,
17913
- [chatId],
17914
- { teamId },
17915
- false,
17916
- void 0,
17917
- void 0,
17918
- { skipTypedRecoveryOutputReplay: true }
17919
- );
17920
- const latestChat = latestCache.chats.find(
17921
- (chat) => String(chat.details.id ?? "") === chatId
17922
- );
17923
- const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
17924
- if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
17925
- await persistTerminalRecovery(latestMessagesV);
17926
- terminalExpectedMessagesV = latestMessagesV;
17927
- terminalPersisted = true;
17928
- }
17983
+ const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
17984
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
17985
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
17986
+ const terminalPersistPayload = {
17987
+ protocol_version: 1,
17988
+ job_id: recoveryJobId,
17989
+ lease_token: leaseToken,
17990
+ lease_generation: leaseGeneration,
17991
+ encrypted_assistant_message: {
17992
+ client_message_id: assistantId,
17993
+ chat_id: chatId,
17994
+ encrypted_content: encryptedAssistantContent,
17995
+ ...await this.encryptThinkingFields(recovered, chatKeyBytes),
17996
+ encrypted_sender_name: encryptedSenderName,
17997
+ encrypted_category: encryptedCategory,
17998
+ encrypted_model_name: encryptedModelName,
17999
+ role: "assistant",
18000
+ user_message_id: messageId2,
18001
+ created_at: completedAt,
18002
+ updated_at: completedAt
17929
18003
  }
17930
- if (!terminalPersisted) {
17931
- throw new Error(`${error.message} (${error.code})`);
18004
+ };
18005
+ const persistTerminalRecovery = async (expectedMessagesV) => {
18006
+ const persistedPromise = ws.waitForMessage(
18007
+ "recovery_job_persisted",
18008
+ (payload) => payload.job_id === recoveryJobId,
18009
+ 2e4
18010
+ );
18011
+ await ws.sendAsync("recovery_job_persist", {
18012
+ ...terminalPersistPayload,
18013
+ expected_messages_v: expectedMessagesV
18014
+ });
18015
+ await persistedPromise;
18016
+ };
18017
+ let terminalPersisted = false;
18018
+ try {
18019
+ await persistTerminalRecovery(terminalExpectedMessagesV);
18020
+ terminalPersisted = true;
18021
+ } catch (error) {
18022
+ if (error instanceof WebSocketProtocolError) {
18023
+ if (error.code === "version_conflict") {
18024
+ const latestCache = await this.ensureSynced(
18025
+ true,
18026
+ [chatId],
18027
+ { teamId },
18028
+ false,
18029
+ void 0,
18030
+ void 0,
18031
+ { skipTypedRecoveryOutputReplay: true }
18032
+ );
18033
+ const latestChat = latestCache.chats.find(
18034
+ (chat) => String(chat.details.id ?? "") === chatId
18035
+ );
18036
+ const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
18037
+ if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
18038
+ await persistTerminalRecovery(latestMessagesV);
18039
+ terminalExpectedMessagesV = latestMessagesV;
18040
+ terminalPersisted = true;
18041
+ }
18042
+ }
18043
+ if (!terminalPersisted) {
18044
+ throw new Error(`${error.message} (${error.code})`);
18045
+ }
18046
+ } else {
18047
+ throw error;
17932
18048
  }
17933
- } else {
17934
- throw error;
17935
18049
  }
17936
- }
17937
- if (!terminalPersisted) {
17938
- throw new Error("Encrypted completion recovery did not persist terminal state.");
17939
- }
17940
- assistantMessageId = assistantId;
17941
- await this.persistStreamedEmbeds({
17942
- ws,
17943
- embeds: resp.embeds,
17944
- chatId,
17945
- chatKeyBytes,
17946
- fallbackMessageId: assistantId,
17947
- ownerId: ownerId2,
17948
- teamId
17949
- });
17950
- if (initialHeaderWork) persistOptionalHeader = () => initialHeaderWork;
17951
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
17952
- if (taskUpdateJobsEnabled) {
17953
- const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
18050
+ if (!terminalPersisted) {
18051
+ throw new Error("Encrypted completion recovery did not persist terminal state.");
18052
+ }
18053
+ assistantMessageId = assistantId;
18054
+ await this.persistStreamedEmbeds({
17954
18055
  ws,
17955
- jobs: pendingTaskUpdateJobs,
17956
- events: taskEvents,
17957
- teamId,
17958
- activeChatId: chatId,
17959
- activeChatKeyBytes: chatKeyBytes,
17960
- fallbackUserMessageId: messageId2,
17961
- requireActiveTurnEvent: true
18056
+ embeds: resp.embeds,
18057
+ chatId,
18058
+ chatKeyBytes,
18059
+ fallbackMessageId: assistantId,
18060
+ ownerId: ownerId2,
18061
+ teamId
17962
18062
  });
17963
- pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
17964
- }
17965
- await persistTaskEventSystemMessages(taskEvents);
17966
- await recoverCurrentTurnOutputs();
17967
- clearSyncCache(teamId);
17968
- if (optionalMetadata) {
17969
- metadataHandoffReady = true;
17970
- metadataHandoffSuggestion = resp.suggestedFollowupMessage;
17971
- }
17972
- }
17973
- } finally {
17974
- try {
17975
- await projectFocusCountdown?.flush();
17976
- await focusPersistence;
17977
- if (projectAuthoringAvailable && assistant && chatContextEvents) {
17978
- try {
17979
- const history = boundedAuthoringHistory([
17980
- ...messageHistoryForRequest ?? [],
17981
- { role: "user", content: finalMessage },
17982
- { role: "assistant", content: assistant }
17983
- ]);
17984
- const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
17985
- for (const event of recommendations) chatContextEvents.persist(event);
17986
- } catch {
18063
+ if (initialHeaderWork) persistOptionalHeader = () => initialHeaderWork;
18064
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
18065
+ if (taskUpdateJobsEnabled) {
18066
+ const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
18067
+ ws,
18068
+ jobs: pendingTaskUpdateJobs,
18069
+ events: taskEvents,
18070
+ teamId,
18071
+ activeChatId: chatId,
18072
+ activeChatKeyBytes: chatKeyBytes,
18073
+ fallbackUserMessageId: messageId2,
18074
+ requireActiveTurnEvent: true
18075
+ });
18076
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
18077
+ }
18078
+ await persistTaskEventSystemMessages(taskEvents);
18079
+ await recoverCurrentTurnOutputs();
18080
+ clearSyncCache(teamId);
18081
+ if (optionalMetadata) {
18082
+ metadataHandoffReady = true;
18083
+ metadataHandoffSuggestion = resp.suggestedFollowupMessage;
17987
18084
  }
17988
18085
  }
17989
- await chatContextEvents?.flush();
17990
18086
  } finally {
17991
- projectFocusCountdown?.();
17992
- chatContextEvents?.stop();
17993
- if (!metadataHandoffReady) ws.close();
18087
+ try {
18088
+ await projectFocusCountdown?.flush();
18089
+ await focusPersistence;
18090
+ if (projectAuthoringAvailable && assistant && chatContextEvents) {
18091
+ try {
18092
+ const history = boundedAuthoringHistory([
18093
+ ...messageHistoryForRequest ?? [],
18094
+ { role: "user", content: finalMessage },
18095
+ { role: "assistant", content: assistant }
18096
+ ]);
18097
+ const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
18098
+ for (const event of recommendations) chatContextEvents.persist(event);
18099
+ } catch {
18100
+ }
18101
+ }
18102
+ await chatContextEvents?.flush();
18103
+ } finally {
18104
+ projectFocusCountdown?.();
18105
+ chatContextEvents?.stop();
18106
+ if (!metadataHandoffReady) ws.close();
18107
+ }
18108
+ }
18109
+ if (focusPersistenceError) throw focusPersistenceError;
18110
+ if (metadataHandoffReady && optionalMetadata) {
18111
+ metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
18112
+ if (!metadataTransportOwned) ws.close();
17994
18113
  }
17995
18114
  }
17996
- if (focusPersistenceError) throw focusPersistenceError;
17997
- if (metadataHandoffReady && optionalMetadata) {
17998
- metadataTransportOwned = optionalMetadata.handOff(metadataHandoffSuggestion, persistOptionalHeader);
17999
- if (!metadataTransportOwned) ws.close();
18115
+ const mateName = category ? MATE_NAMES[category] ?? null : null;
18116
+ return {
18117
+ status: "completed",
18118
+ chatId,
18119
+ messageId: assistantMessageId,
18120
+ userMessageId: messageId2,
18121
+ assistant,
18122
+ category,
18123
+ modelName,
18124
+ mateName,
18125
+ followUpSuggestions,
18126
+ suggestedFollowupMessage,
18127
+ taskProposals,
18128
+ taskUpdateProposals,
18129
+ taskEvents,
18130
+ pendingTaskUpdateJobs,
18131
+ subChatEvents,
18132
+ appSettingsMemoryRequests,
18133
+ tokenUsage,
18134
+ promptBudget
18135
+ };
18136
+ } finally {
18137
+ removeContextAckWatcher?.();
18138
+ earlyContextAbort.abort();
18139
+ if (!metadataTransportOwned) {
18140
+ initialHeaderStopped = true;
18141
+ optionalMetadata?.stop();
18142
+ ws.close();
18000
18143
  }
18144
+ if (earlyContext) await earlyContext;
18001
18145
  }
18002
- const mateName = category ? MATE_NAMES[category] ?? null : null;
18003
- return {
18004
- status: "completed",
18005
- chatId,
18006
- messageId: assistantMessageId,
18007
- userMessageId: messageId2,
18008
- assistant,
18009
- category,
18010
- modelName,
18011
- mateName,
18012
- followUpSuggestions,
18013
- suggestedFollowupMessage,
18014
- taskProposals,
18015
- taskUpdateProposals,
18016
- taskEvents,
18017
- pendingTaskUpdateJobs,
18018
- subChatEvents,
18019
- appSettingsMemoryRequests,
18020
- tokenUsage,
18021
- promptBudget
18022
- };
18023
- } finally {
18024
- removeContextAckWatcher?.();
18025
- earlyContextAbort.abort();
18026
- if (!metadataTransportOwned) {
18027
- initialHeaderStopped = true;
18028
- optionalMetadata?.stop();
18029
- ws.close();
18030
- }
18031
- if (earlyContext) await earlyContext;
18146
+ } catch (error) {
18147
+ memorySocket?.ws.close();
18148
+ throw error;
18032
18149
  }
18033
- } catch (error) {
18034
- memorySocket?.ws.close();
18035
- throw error;
18150
+ } finally {
18151
+ historyAbort.abort();
18036
18152
  }
18037
18153
  }
18038
18154
  async persistEncryptedSystemMessage(ws, systemMessage, targetChatId) {
@@ -20337,6 +20453,25 @@ var OpenMatesClient = class _OpenMatesClient {
20337
20453
  // -------------------------------------------------------------------------
20338
20454
  // User tasks
20339
20455
  // -------------------------------------------------------------------------
20456
+ /** One fresh authorized discovery page; never an exhaustive Task inventory. */
20457
+ async listUserTaskDiscoveryPage(options = {}) {
20458
+ const owner2 = this.captureTeamAccountOwner();
20459
+ const activeTeamId = this.getActiveTeamId();
20460
+ const teamId = this.resolveTeamContext(options);
20461
+ const params = new URLSearchParams({ paginate: "true", limit: "100" });
20462
+ if (teamId) params.set("team_id", teamId);
20463
+ const response = await this.http.get(
20464
+ `/v1/user-tasks?${params}`,
20465
+ this.getCliRequestHeaders()
20466
+ );
20467
+ if (!owner2.isCurrent() || this.getActiveTeamId() !== activeTeamId || this.resolveTeamContext(options) !== teamId) throw new Error("Task context workspace changed");
20468
+ if (!response.ok) throw new Error(`Task discovery failed with HTTP ${response.status}`);
20469
+ const rows2 = response.data?.tasks;
20470
+ if (!Array.isArray(rows2) || rows2.length > 100 || rows2.some((row3) => !row3 || typeof row3.task_id !== "string" || !row3.task_id || row3.task_id.length > 128)) {
20471
+ throw new Error("Invalid bounded Task discovery page");
20472
+ }
20473
+ return rows2.filter((row3) => row3.source !== "workflow_run");
20474
+ }
20340
20475
  async listUserTasks(filters = {}, options = {}) {
20341
20476
  const session = this.requireSession();
20342
20477
  const owner2 = [
@@ -22908,6 +23043,7 @@ Required: ${schema.required.join(", ")}`
22908
23043
  }
22909
23044
  interactiveViewerChatId = null;
22910
23045
  interactiveViewerSocket = null;
23046
+ interactiveViewerOpenController = null;
22911
23047
  interactiveViewerAllowed = false;
22912
23048
  interactiveViewerGeneration = 0;
22913
23049
  beginInteractiveViewerSession() {
@@ -22935,8 +23071,10 @@ Required: ${schema.required.join(", ")}`
22935
23071
  this.clearInteractiveChatViewer();
22936
23072
  this.interactiveViewerChatId = chatId;
22937
23073
  const generation = this.interactiveViewerGeneration;
23074
+ const controller = new AbortController();
23075
+ this.interactiveViewerOpenController = controller;
22938
23076
  try {
22939
- const { ws } = await this.openWsClient({ taskUpdateJobs: false, interactiveHuman: true });
23077
+ const { ws } = await this.openWsClient({ taskUpdateJobs: false, interactiveHuman: true, signal: controller.signal });
22940
23078
  if (!this.interactiveViewerAllowed || this.interactiveViewerGeneration !== generation || this.interactiveViewerChatId !== chatId) {
22941
23079
  ws.close();
22942
23080
  return;
@@ -22958,9 +23096,14 @@ Required: ${schema.required.join(", ")}`
22958
23096
  if (this.interactiveViewerGeneration === generation && this.interactiveViewerChatId === chatId) void this.setInteractiveChatViewer(chatId);
22959
23097
  }, 1e3);
22960
23098
  retry.unref?.();
23099
+ } finally {
23100
+ if (this.interactiveViewerOpenController === controller) this.interactiveViewerOpenController = null;
22961
23101
  }
22962
23102
  }
22963
23103
  clearInteractiveChatViewer() {
23104
+ const controller = this.interactiveViewerOpenController;
23105
+ this.interactiveViewerOpenController = null;
23106
+ controller?.abort();
22964
23107
  this.interactiveViewerGeneration = (this.interactiveViewerGeneration ?? 0) + 1;
22965
23108
  this.interactiveViewerChatId = null;
22966
23109
  const ws = this.interactiveViewerSocket;