openmates 0.27.0-alpha.57 → 0.27.0-alpha.59

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.
@@ -2395,8 +2395,13 @@ async function parseGenericImportBuffer(payload, sourceName = "generic-transcrip
2395
2395
  }
2396
2396
 
2397
2397
  // src/http.ts
2398
+ import { AsyncLocalStorage } from "async_hooks";
2398
2399
  var DEFAULT_JSON_REQUEST_TIMEOUT_MS = 18e4;
2399
2400
  var JSON_REQUEST_TIMEOUT_ENV = "OPENMATES_CLI_HTTP_TIMEOUT_MS";
2401
+ var requestAbortScope = new AsyncLocalStorage();
2402
+ function withHttpAbortSignal(signal, operation) {
2403
+ return requestAbortScope.run(signal, operation);
2404
+ }
2400
2405
  var OpenMatesHttpClient = class {
2401
2406
  apiUrl;
2402
2407
  cookies;
@@ -2505,7 +2510,12 @@ var OpenMatesHttpClient = class {
2505
2510
  if (body !== void 0) {
2506
2511
  requestHeaders["Content-Type"] = "application/json";
2507
2512
  }
2513
+ const scopedSignal = requestAbortScope.getStore();
2514
+ scopedSignal?.throwIfAborted();
2508
2515
  const controller = new AbortController();
2516
+ const onScopedAbort = () => controller.abort(scopedSignal?.reason);
2517
+ scopedSignal?.addEventListener("abort", onScopedAbort, { once: true });
2518
+ if (scopedSignal?.aborted) onScopedAbort();
2509
2519
  const timeout = setTimeout(() => controller.abort(), getJsonRequestTimeoutMs());
2510
2520
  try {
2511
2521
  let response;
@@ -2517,19 +2527,23 @@ var OpenMatesHttpClient = class {
2517
2527
  signal: controller.signal
2518
2528
  });
2519
2529
  } catch (error) {
2530
+ if (scopedSignal?.aborted) throw new Error(`HTTP ${method} ${path2} cancelled`);
2520
2531
  if (controller.signal.aborted) {
2521
2532
  throw new Error(`HTTP ${method} ${path2} timed out after ${getJsonRequestTimeoutMs()}ms`);
2522
2533
  }
2523
2534
  throw error;
2524
2535
  }
2525
2536
  await this.captureCookies(response, path2, sessionId, sentToken);
2537
+ scopedSignal?.throwIfAborted();
2526
2538
  let data = {};
2527
2539
  try {
2528
2540
  data = await response.json();
2529
2541
  } catch {
2542
+ if (scopedSignal?.aborted) throw new Error(`HTTP ${method} ${path2} cancelled`);
2530
2543
  if (controller.signal.aborted) throw new Error(`HTTP ${method} ${path2} timed out`);
2531
2544
  data = {};
2532
2545
  }
2546
+ scopedSignal?.throwIfAborted();
2533
2547
  return {
2534
2548
  ok: response.ok,
2535
2549
  status: response.status,
@@ -2538,6 +2552,7 @@ var OpenMatesHttpClient = class {
2538
2552
  };
2539
2553
  } finally {
2540
2554
  clearTimeout(timeout);
2555
+ scopedSignal?.removeEventListener("abort", onScopedAbort);
2541
2556
  }
2542
2557
  }
2543
2558
  formatCookieHeader() {
@@ -13203,6 +13218,20 @@ var OpenMatesClient = class _OpenMatesClient {
13203
13218
  if (!current2()) throw new Error("Chat workspace changed");
13204
13219
  return result;
13205
13220
  }
13221
+ chatActivitySocket = null;
13222
+ /** Warm the active chat scope over the TUI's existing authenticated activity socket. */
13223
+ warmChatMetadata() {
13224
+ const activity = this.chatActivitySocket;
13225
+ if (!activity || !activity.ownerCurrent()) return;
13226
+ const teamId = this.getActiveTeamId();
13227
+ const scope2 = teamId ?? "personal";
13228
+ if (activity.lastWarmupScope === scope2) return;
13229
+ try {
13230
+ activity.ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
13231
+ activity.lastWarmupScope = scope2;
13232
+ } catch {
13233
+ }
13234
+ }
13206
13235
  /** Observe lifecycle metadata without claiming or collecting AI responses. */
13207
13236
  async observeChatActivity(onChange) {
13208
13237
  const session = this.requireSession();
@@ -13231,6 +13260,7 @@ var OpenMatesClient = class _OpenMatesClient {
13231
13260
  removeHandlers = [];
13232
13261
  const active = socket;
13233
13262
  socket = null;
13263
+ if (this.chatActivitySocket?.ws === active) this.chatActivitySocket = null;
13234
13264
  active?.close();
13235
13265
  };
13236
13266
  const scheduleReconnect = () => {
@@ -13256,6 +13286,8 @@ var OpenMatesClient = class _OpenMatesClient {
13256
13286
  return;
13257
13287
  }
13258
13288
  socket = opened;
13289
+ this.chatActivitySocket = { ws: opened, ownerCurrent, lastWarmupScope: null };
13290
+ this.warmChatMetadata();
13259
13291
  removeHandlers = types.map((type) => opened.onMessageType(type, () => {
13260
13292
  if (!ownerCurrent()) {
13261
13293
  stop();
@@ -13269,6 +13301,7 @@ var OpenMatesClient = class _OpenMatesClient {
13269
13301
  removeHandlers.push(opened.onClose(() => {
13270
13302
  if (socket !== opened) return;
13271
13303
  socket = null;
13304
+ if (this.chatActivitySocket?.ws === opened) this.chatActivitySocket = null;
13272
13305
  removeHandlers.forEach((remove) => remove());
13273
13306
  removeHandlers = [];
13274
13307
  scheduleReconnect();
@@ -15510,6 +15543,7 @@ var OpenMatesClient = class _OpenMatesClient {
15510
15543
  params.onPrivacyPrepared?.(finalMessage);
15511
15544
  const explicitTasksAppSkill = messageExplicitlyRequestsTasksAppSkill(finalMessage);
15512
15545
  const taskUpdateJobsEnabled = params.taskUpdateJobs !== false && !explicitTasksAppSkill;
15546
+ const authSocketStartedAt = Date.now();
15513
15547
  const { ws, ownerId: ownerId2 } = await this.openWsClient({
15514
15548
  taskUpdateJobs: taskUpdateJobsEnabled,
15515
15549
  projectFileJobs: !params.incognito,
@@ -15520,994 +15554,937 @@ var OpenMatesClient = class _OpenMatesClient {
15520
15554
  ws.close();
15521
15555
  throw new Error("Authenticated user identity is required for saved chat recovery.");
15522
15556
  }
15523
- if (!params.incognito) {
15524
- try {
15525
- await ws.waitForRecoveryOutputDiscovery();
15526
- } catch (error) {
15527
- ws.close();
15528
- throw error;
15529
- }
15530
- }
15531
- const messageId = randomUUID9();
15532
- const wallCreatedAt = Math.floor(Date.now() / 1e3);
15533
- const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
15534
- const isNewChat = !params.chatId;
15535
- let messageHistoryForRequest = params.messageHistory;
15536
- if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
15537
- const { messages } = savedPrivacyHistory;
15538
- messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
15557
+ const authSocketMs = Date.now() - authSocketStartedAt;
15558
+ let contextSession;
15559
+ try {
15560
+ contextSession = this.requireSession();
15561
+ } catch (error) {
15562
+ ws.close();
15563
+ throw error;
15539
15564
  }
15540
- ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
15541
- const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
15542
- params.connectedAccountDirectory
15543
- );
15544
- const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
15545
- chatId,
15546
- messageId,
15547
- refs: params.connectedAccountTokenRefInputs,
15548
- teamId
15549
- }) : [];
15550
- assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
15551
- let chatKeyBytes = null;
15552
- let activeFocusId = null;
15553
- let encryptedChatKey = null;
15554
- let chatSlugLookupKey = null;
15555
- let focusPhaseState = {};
15556
- let baselineMessagesV = 0;
15557
- let notificationTitle = null;
15558
- let terminalExpectedMessagesV = 1;
15559
- let savedTurnId = null;
15560
- if (!params.incognito) {
15561
- const masterKey = this.getMasterKeyBytes();
15562
- const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
15563
- chatSlugLookupKey = wrappingKey;
15564
- if (isNewChat) {
15565
- chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
15566
- (await import("crypto")).webcrypto.getRandomValues(
15567
- new Uint8Array(32)
15568
- )
15569
- );
15570
- encryptedChatKey = await encryptBytesWithAesGcm(
15571
- chatKeyBytes,
15572
- wrappingKey
15573
- );
15574
- } else {
15575
- const cache = loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
15576
- const chat = cache.chats.find(
15577
- (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
15578
- );
15579
- if (chat) {
15580
- baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
15581
- const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
15582
- if (encKey) {
15583
- chatKeyBytes = await decryptBytesWithAesGcm(encKey, wrappingKey);
15584
- encryptedChatKey = encKey;
15585
- if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
15586
- if (typeof chat.details.encrypted_title === "string") {
15587
- try {
15588
- notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
15589
- } catch {
15565
+ const {
15566
+ apiUrl: contextApiUrl,
15567
+ hashedEmail: contextHashedEmail,
15568
+ sessionId: contextSessionId,
15569
+ createdAt: contextSessionCreatedAt,
15570
+ masterKeyExportedB64: contextMasterKey,
15571
+ activeTeamId: contextActiveTeamId
15572
+ } = contextSession;
15573
+ const contextScope = { teamId, personal: !teamId };
15574
+ const contextStartedAt = Date.now();
15575
+ let recoveryMs = 0;
15576
+ const contextOwnerIsCurrent = () => {
15577
+ const current2 = this.session;
15578
+ return Boolean(current2 && current2.apiUrl === contextApiUrl && current2.hashedEmail === contextHashedEmail && current2.sessionId === contextSessionId && current2.createdAt === contextSessionCreatedAt && current2.masterKeyExportedB64 === contextMasterKey && current2.activeTeamId === contextActiveTeamId);
15579
+ };
15580
+ const earlyContextAbort = new AbortController();
15581
+ const earlyContext = !params.incognito && !params.chatId && !params.projectId ? withHttpAbortSignal(earlyContextAbort.signal, () => prepareCliJevContext(this, chatId, contextScope, params.jevContext, finalMessage)).then(
15582
+ (value) => ({ ok: true, value, elapsedMs: Date.now() - contextStartedAt }),
15583
+ (error) => ({ ok: false, error, elapsedMs: Date.now() - contextStartedAt })
15584
+ ) : null;
15585
+ try {
15586
+ if (!params.incognito) {
15587
+ try {
15588
+ ws.send("chat_metadata_warmup", teamId ? { team_id: teamId } : {});
15589
+ } catch {
15590
+ }
15591
+ try {
15592
+ const recoveryStartedAt = Date.now();
15593
+ await ws.waitForRecoveryOutputDiscovery();
15594
+ recoveryMs = Date.now() - recoveryStartedAt;
15595
+ } catch (error) {
15596
+ ws.close();
15597
+ throw error;
15598
+ }
15599
+ }
15600
+ const messageId = randomUUID9();
15601
+ const wallCreatedAt = Math.floor(Date.now() / 1e3);
15602
+ const createdAt = !teamId && !params.incognito && params.chatId ? nextPersonalUserCreatedAt(wallCreatedAt, savedPrivacyHistory?.messages.map((row3) => row3.createdAt) ?? []) : wallCreatedAt;
15603
+ const isNewChat = !params.chatId;
15604
+ let messageHistoryForRequest = params.messageHistory;
15605
+ if (!params.incognito && !isNewChat && !messageHistoryForRequest) {
15606
+ const { messages } = savedPrivacyHistory;
15607
+ messageHistoryForRequest = buildSavedChatMessageHistory(messages, chatId);
15608
+ }
15609
+ ws.send("set_active_chat", { chat_id: chatId, ...teamId ? { team_id: teamId } : {} });
15610
+ const connectedAccountDirectory = buildConnectedAccountDirectoryPayload(
15611
+ params.connectedAccountDirectory
15612
+ );
15613
+ const connectedAccountTokenRefs = params.connectedAccountTokenRefInputs?.length ? await this.createTurnTokenRefs({
15614
+ chatId,
15615
+ messageId,
15616
+ refs: params.connectedAccountTokenRefInputs,
15617
+ teamId
15618
+ }) : [];
15619
+ assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
15620
+ let chatKeyBytes = null;
15621
+ let activeFocusId = null;
15622
+ let encryptedChatKey = null;
15623
+ let chatSlugLookupKey = null;
15624
+ let focusPhaseState = {};
15625
+ let baselineMessagesV = 0;
15626
+ let notificationTitle = null;
15627
+ let terminalExpectedMessagesV = 1;
15628
+ let savedTurnId = null;
15629
+ if (!params.incognito) {
15630
+ const masterKey = this.getMasterKeyBytes();
15631
+ const wrappingKey = await this.getChatWrappingKey(teamId, masterKey);
15632
+ chatSlugLookupKey = wrappingKey;
15633
+ if (isNewChat) {
15634
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
15635
+ (await import("crypto")).webcrypto.getRandomValues(
15636
+ new Uint8Array(32)
15637
+ )
15638
+ );
15639
+ encryptedChatKey = await encryptBytesWithAesGcm(
15640
+ chatKeyBytes,
15641
+ wrappingKey
15642
+ );
15643
+ } else {
15644
+ const cache = loadSyncCache(teamId) ?? await this.ensureSynced(false, [], { teamId });
15645
+ const chat = cache.chats.find(
15646
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
15647
+ );
15648
+ if (chat) {
15649
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
15650
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
15651
+ if (encKey) {
15652
+ chatKeyBytes = await decryptBytesWithAesGcm(encKey, wrappingKey);
15653
+ encryptedChatKey = encKey;
15654
+ if (!chatKeyBytes) throw new Error("Could not decrypt the saved chat key.");
15655
+ if (typeof chat.details.encrypted_title === "string") {
15656
+ try {
15657
+ notificationTitle = await decryptWithAesGcmCombined(chat.details.encrypted_title, chatKeyBytes);
15658
+ } catch {
15659
+ }
15590
15660
  }
15591
- }
15592
- const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
15593
- if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
15594
- const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
15595
- if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
15596
- focusPhaseState = JSON.parse(phaseText);
15597
- }
15598
- const encryptedFocusId = chat.details.encrypted_active_focus_id;
15599
- if (typeof encryptedFocusId === "string" && encryptedFocusId) {
15600
- activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
15601
- if (!activeFocusId) {
15602
- throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
15661
+ const encryptedPhaseState = chat.details.encrypted_focus_phase_state;
15662
+ if (typeof encryptedPhaseState === "string" && encryptedPhaseState) {
15663
+ const phaseText = await decryptWithAesGcmCombined(encryptedPhaseState, chatKeyBytes);
15664
+ if (!phaseText) throw new Error("Could not decrypt focus phase progress. Sync before sending again.");
15665
+ focusPhaseState = JSON.parse(phaseText);
15666
+ }
15667
+ const encryptedFocusId = chat.details.encrypted_active_focus_id;
15668
+ if (typeof encryptedFocusId === "string" && encryptedFocusId) {
15669
+ activeFocusId = await decryptWithAesGcmCombined(encryptedFocusId, chatKeyBytes);
15670
+ if (!activeFocusId) {
15671
+ throw new Error("Could not decrypt the chat's active focus. Sync before sending again.");
15672
+ }
15603
15673
  }
15604
15674
  }
15605
15675
  }
15676
+ if (!chatKeyBytes || !encryptedChatKey) {
15677
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
15678
+ }
15606
15679
  }
15607
- if (!chatKeyBytes || !encryptedChatKey) {
15608
- throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
15609
- }
15610
- }
15611
- }
15612
- let focusPersistence = Promise.resolve();
15613
- let focusPersistenceError = null;
15614
- const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
15615
- ws,
15616
- chatId,
15617
- chatKey: chatKeyBytes,
15618
- existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
15619
- previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
15620
- onApplied: params.onChatContextApplied
15621
- }) : null;
15622
- let projectAuthoringAvailable = null;
15623
- if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
15624
- if (!value || typeof value !== "object") return;
15625
- const frame = value;
15626
- if (frame.chat_id === chatId && frame.user_message_id === messageId && typeof frame.project_id === "string")
15627
- projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId };
15628
- });
15629
- const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
15630
- client: this,
15631
- ws,
15632
- chatId,
15633
- teamId,
15634
- onPending: params.onProjectFocusPending,
15635
- onError: (error) => {
15636
- focusPersistenceError = error;
15637
15680
  }
15638
- }) : null;
15639
- ws.onMessageType("focus_mode_activated", (event) => {
15640
- if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
15641
- const focusId = event.focus_id;
15642
- if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
15643
- const key = chatKeyBytes;
15644
- focusPersistence = focusPersistence.then(async () => {
15645
- const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
15646
- await ws.sendAsync("update_encrypted_active_focus_id", {
15647
- chat_id: chatId,
15648
- encrypted_active_focus_id: encryptedFocusId
15649
- });
15650
- }).catch((error) => {
15651
- focusPersistenceError = error;
15681
+ let focusPersistence = Promise.resolve();
15682
+ let focusPersistenceError = null;
15683
+ const chatContextEvents = !params.incognito && chatKeyBytes ? registerChatContextEvents({
15684
+ ws,
15685
+ chatId,
15686
+ chatKey: chatKeyBytes,
15687
+ existingMessageIds: savedPrivacyHistory?.messages.map(canonicalSavedChatMessageId) ?? [],
15688
+ previousRulesSetKey: savedPrivacyHistory?.messages.filter((message) => message.role === "system").map((message) => parseChatContextContent(message.content)).filter((event) => event?.type === "rules_loaded").at(-1)?.set_key,
15689
+ onApplied: params.onChatContextApplied
15690
+ }) : null;
15691
+ let projectAuthoringAvailable = null;
15692
+ if (!params.incognito) ws.onMessageType("project_authoring_available", (value) => {
15693
+ if (!value || typeof value !== "object") return;
15694
+ const frame = value;
15695
+ if (frame.chat_id === chatId && frame.user_message_id === messageId && typeof frame.project_id === "string")
15696
+ projectAuthoringAvailable = { chat_id: chatId, project_id: frame.project_id, user_message_id: messageId };
15652
15697
  });
15653
- });
15654
- ws.onMessageType("focus_phases_updated", (event) => {
15655
- if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
15656
- const key = chatKeyBytes;
15657
- const states2 = event.states;
15658
- focusPersistence = focusPersistence.then(async () => {
15659
- for (const [focusId, phase] of Object.entries(states2)) {
15660
- 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) {
15661
- throw new Error("Invalid focus phase state received");
15662
- }
15663
- if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
15698
+ const projectFocusCountdown = !params.incognito ? registerProjectFocusCountdown({
15699
+ client: this,
15700
+ ws,
15701
+ chatId,
15702
+ teamId,
15703
+ onPending: params.onProjectFocusPending,
15704
+ onError: (error) => {
15705
+ focusPersistenceError = error;
15664
15706
  }
15665
- focusPhaseState = states2;
15666
- const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
15667
- const persisted = ws.waitForMessage(
15668
- "encrypted_metadata_stored",
15669
- (payload) => payload.chat_id === chatId,
15670
- 2e4
15671
- );
15672
- await ws.sendAsync("encrypted_chat_metadata", {
15673
- chat_id: chatId,
15674
- ...teamId ? { team_id: teamId } : {},
15675
- encrypted_focus_phase_state: encryptedState
15676
- });
15677
- await persisted;
15678
- const eventIds = /* @__PURE__ */ new Set();
15679
- for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
15680
- if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
15681
- eventIds.add(transition.event_id);
15682
- const content = JSON.stringify(transition);
15683
- await ws.sendAsync("chat_system_message_added", {
15707
+ }) : null;
15708
+ ws.onMessageType("focus_mode_activated", (event) => {
15709
+ if (event.chat_id !== chatId || !event.focus_id || !chatKeyBytes || params.incognito) return;
15710
+ const focusId = event.focus_id;
15711
+ if (!focusId.startsWith("project-")) delete focusPhaseState[focusId];
15712
+ const key = chatKeyBytes;
15713
+ focusPersistence = focusPersistence.then(async () => {
15714
+ const encryptedFocusId = await encryptWithAesGcmCombined(focusId, key);
15715
+ await ws.sendAsync("update_encrypted_active_focus_id", {
15684
15716
  chat_id: chatId,
15685
- message: {
15686
- message_id: transition.event_id,
15687
- role: "system",
15688
- status: "synced",
15689
- encrypted_content: await encryptWithAesGcmCombined(content, key),
15690
- created_at: transition.created_at
15691
- }
15717
+ encrypted_active_focus_id: encryptedFocusId
15692
15718
  });
15693
- }
15694
- clearSyncCache(teamId);
15695
- }).catch((error) => {
15696
- focusPersistenceError = error;
15719
+ }).catch((error) => {
15720
+ focusPersistenceError = error;
15721
+ });
15697
15722
  });
15698
- });
15699
- const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
15700
- if (!params.incognito && chatKeyBytes) {
15701
- registerCliProjectFileExecutor({
15702
- client: this,
15703
- ws,
15704
- chatId,
15705
- chatKey: chatKeyBytes,
15706
- piiMappings,
15707
- memories: availableMemories,
15708
- requestApproval: params.onProjectWriteApproval,
15709
- requestReadApproval: params.onProjectReadApproval,
15710
- onHostedVersionCommitted: params.onHostedVersionCommitted
15723
+ ws.onMessageType("focus_phases_updated", (event) => {
15724
+ if (event.chat_id !== chatId || !event.states || !chatKeyBytes || params.incognito) return;
15725
+ const key = chatKeyBytes;
15726
+ const states2 = event.states;
15727
+ focusPersistence = focusPersistence.then(async () => {
15728
+ for (const [focusId, phase] of Object.entries(states2)) {
15729
+ 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) {
15730
+ throw new Error("Invalid focus phase state received");
15731
+ }
15732
+ if (focusPhaseState[focusId]?.run_id === phase.run_id && focusPhaseState[focusId].version > phase.version) states2[focusId] = focusPhaseState[focusId];
15733
+ }
15734
+ focusPhaseState = states2;
15735
+ const encryptedState = await encryptWithAesGcmCombined(JSON.stringify(states2), key);
15736
+ const persisted = ws.waitForMessage(
15737
+ "encrypted_metadata_stored",
15738
+ (payload) => payload.chat_id === chatId,
15739
+ 2e4
15740
+ );
15741
+ await ws.sendAsync("encrypted_chat_metadata", {
15742
+ chat_id: chatId,
15743
+ ...teamId ? { team_id: teamId } : {},
15744
+ encrypted_focus_phase_state: encryptedState
15745
+ });
15746
+ await persisted;
15747
+ const eventIds = /* @__PURE__ */ new Set();
15748
+ for (const phase of Object.values(states2)) for (const transition of phase.transitions) {
15749
+ if (transition.chat_id !== chatId || transition.focus_id !== phase.focus_id || !transition.event_id || eventIds.has(transition.event_id)) continue;
15750
+ eventIds.add(transition.event_id);
15751
+ const content = JSON.stringify(transition);
15752
+ await ws.sendAsync("chat_system_message_added", {
15753
+ chat_id: chatId,
15754
+ message: {
15755
+ message_id: transition.event_id,
15756
+ role: "system",
15757
+ status: "synced",
15758
+ encrypted_content: await encryptWithAesGcmCombined(content, key),
15759
+ created_at: transition.created_at
15760
+ }
15761
+ });
15762
+ }
15763
+ clearSyncCache(teamId);
15764
+ }).catch((error) => {
15765
+ focusPersistenceError = error;
15766
+ });
15711
15767
  });
15712
- clientCapabilities.push("project_file_jobs");
15713
- registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
15714
- clientCapabilities.push("remote_command_jobs");
15715
- }
15716
- const messagePayload = {
15717
- chat_id: chatId,
15718
- ...teamId ? { team_id: teamId } : {},
15719
- client_capabilities: clientCapabilities,
15720
- // Only decrypted current metadata restores focus; history is never authority.
15721
- active_focus_id: activeFocusId,
15722
- focus_phase_state: focusPhaseState,
15723
- is_incognito: Boolean(params.incognito),
15724
- message: {
15725
- message_id: messageId,
15726
- chat_id: chatId,
15727
- role: "user",
15728
- sender_name: "User",
15729
- status: "sent",
15730
- content: finalMessage,
15731
- created_at: createdAt,
15732
- chat_has_title: Boolean(params.chatId)
15768
+ const clientCapabilities = taskUpdateJobsEnabled ? ["task_update_jobs"] : [];
15769
+ if (!params.incognito && chatKeyBytes) {
15770
+ registerCliProjectFileExecutor({
15771
+ client: this,
15772
+ ws,
15773
+ chatId,
15774
+ chatKey: chatKeyBytes,
15775
+ piiMappings,
15776
+ memories: availableMemories,
15777
+ requestApproval: params.onProjectWriteApproval,
15778
+ requestReadApproval: params.onProjectReadApproval,
15779
+ onHostedVersionCommitted: params.onHostedVersionCommitted
15780
+ });
15781
+ clientCapabilities.push("project_file_jobs");
15782
+ registerRemoteCommandOriginClient({ ws, client: this, chatId, onReview: params.onRemoteCommandReview, onEvent: params.onRemoteCommandEvent });
15783
+ clientCapabilities.push("remote_command_jobs");
15733
15784
  }
15734
- };
15735
- if (!params.incognito) Object.assign(
15736
- messagePayload,
15737
- await prepareCliJevContext(this, chatId, { teamId, personal: !teamId }, params.jevContext, finalMessage)
15738
- );
15739
- if (params.testMockMarker !== void 0) {
15740
- const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
15741
- messagePayload.test_mock_marker = capacityReplayMarker(
15742
- params.testMockMarker,
15743
- this.apiUrl,
15744
- process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
15745
- );
15746
- }
15747
- if (isNewChat && encryptedChatKey) {
15748
- messagePayload.encrypted_chat_key = encryptedChatKey;
15749
- }
15750
- if (memoryMetadataKeys.length > 0) {
15751
- messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
15752
- }
15753
- if (connectedAccountDirectory) {
15754
- messagePayload.connected_account_directory = connectedAccountDirectory;
15755
- }
15756
- if (connectedAccountTokenRefs.length > 0) {
15757
- messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
15758
- }
15759
- if (params.benchmarkMetadata) {
15760
- messagePayload.benchmark_metadata = params.benchmarkMetadata;
15761
- }
15762
- if (params.learningMode) {
15763
- messagePayload.learning_mode = {
15764
- enabled: params.learningMode.enabled,
15765
- age_group: params.learningMode.ageGroup ?? null
15766
- };
15767
- }
15768
- if (params.incognito) {
15769
- const providedHistory = (params.messageHistory ?? []).map(
15770
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
15771
- );
15772
- messagePayload.message_history = [...providedHistory, {
15773
- message_id: messageId,
15785
+ const messagePayload = {
15774
15786
  chat_id: chatId,
15775
- role: "user",
15776
- sender_name: "User",
15777
- content: finalMessage,
15778
- created_at: createdAt
15779
- }];
15780
- } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
15781
- messagePayload.message_history = messageHistoryForRequest.map(
15782
- (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
15783
- );
15784
- }
15785
- if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
15786
- messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
15787
- embed_id: embed.embedId,
15788
- type: embed.type,
15789
- content: embed.content,
15790
- status: embed.status,
15791
- text_preview: embed.textPreview
15792
- }));
15793
- }
15794
- const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
15795
- if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
15796
- if (!ownerId2) {
15797
- throw new Error("Authenticated user identity is required to encrypt embeds.");
15787
+ ...teamId ? { team_id: teamId } : {},
15788
+ client_capabilities: clientCapabilities,
15789
+ // Only decrypted current metadata restores focus; history is never authority.
15790
+ active_focus_id: activeFocusId,
15791
+ focus_phase_state: focusPhaseState,
15792
+ is_incognito: Boolean(params.incognito),
15793
+ message: {
15794
+ message_id: messageId,
15795
+ chat_id: chatId,
15796
+ role: "user",
15797
+ sender_name: "User",
15798
+ status: "sent",
15799
+ content: finalMessage,
15800
+ created_at: createdAt,
15801
+ chat_has_title: Boolean(params.chatId)
15802
+ }
15803
+ };
15804
+ if (!params.incognito) {
15805
+ const serialContextStartedAt = Date.now();
15806
+ const prepared = earlyContext ? await earlyContext : { ok: true, value: await prepareCliJevContext(this, chatId, contextScope, params.jevContext, finalMessage), elapsedMs: Date.now() - serialContextStartedAt };
15807
+ if (!prepared.ok) throw prepared.error;
15808
+ if (!contextOwnerIsCurrent()) throw new Error("Chat workspace changed while preparing context.");
15809
+ if (process.env.OPENMATES_DEBUG === "1") {
15810
+ console.error(`[chat preparation] auth_ws_ms=${authSocketMs} recovery_ms=${recoveryMs} context_ms=${prepared.elapsedMs}`);
15811
+ }
15812
+ Object.assign(messagePayload, prepared.value);
15813
+ }
15814
+ if (params.testMockMarker !== void 0) {
15815
+ const { capacityReplayMarker } = await import("./capacityReplayMarker-3NFJNDTZ.js");
15816
+ messagePayload.test_mock_marker = capacityReplayMarker(
15817
+ params.testMockMarker,
15818
+ this.apiUrl,
15819
+ process.env.OPENMATES_CAPACITY_CLIENT_REPLAY === "1"
15820
+ );
15798
15821
  }
15799
- if (!chatKeyBytes) {
15800
- throw new Error("Saved chat key is required to encrypt every prepared embed.");
15822
+ if (isNewChat && encryptedChatKey) {
15823
+ messagePayload.encrypted_chat_key = encryptedChatKey;
15801
15824
  }
15802
- if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
15803
- throw new Error("Prepared message embed IDs must be unique.");
15825
+ if (memoryMetadataKeys.length > 0) {
15826
+ messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
15804
15827
  }
15805
- const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
15806
- if (preparedIds.some((id) => !id || id.length > 128)) {
15807
- throw new Error("Prepared message embed ID is invalid.");
15828
+ if (connectedAccountDirectory) {
15829
+ messagePayload.connected_account_directory = connectedAccountDirectory;
15808
15830
  }
15809
- const canonicalReady = /* @__PURE__ */ new Set();
15810
- for (let offset = 0; offset < preparedIds.length; offset += 20) {
15811
- const selectedIds = preparedIds.slice(offset, offset + 20);
15812
- if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
15813
- throw new Error("Message embed availability request exceeds its byte limit.");
15814
- }
15815
- const path2 = this.appendTeamQuery(
15816
- `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
15817
- { teamId }
15818
- );
15819
- const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
15820
- if (!availability.ok) {
15821
- throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
15822
- }
15823
- const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
15824
- for (const id of page.ready) canonicalReady.add(id);
15831
+ if (connectedAccountTokenRefs.length > 0) {
15832
+ messagePayload.connected_account_token_refs = connectedAccountTokenRefs;
15825
15833
  }
15826
- const masterKey = this.getMasterKeyBytes();
15827
- for (const embed of params.preparedEmbeds) {
15828
- if (canonicalReady.has(embed.embedId)) continue;
15829
- const encrypted = await encryptEmbed(
15830
- embed,
15831
- masterKey,
15832
- chatKeyBytes,
15833
- chatId,
15834
- messageId,
15835
- ownerId2
15836
- );
15837
- if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
15838
- throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
15839
- }
15840
- encryptedEmbeds.push(encrypted);
15834
+ if (params.benchmarkMetadata) {
15835
+ messagePayload.benchmark_metadata = params.benchmarkMetadata;
15841
15836
  }
15842
- }
15843
- if (!params.incognito && encryptedEmbeds.length > 0) {
15844
- if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
15845
- assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId, ownerId: ownerId2 });
15846
- }
15847
- if (encryptedEmbeds.length > 0) {
15848
- messagePayload.encrypted_embeds = encryptedEmbeds;
15849
- }
15850
- let dispatchHandlersReady = () => {
15851
- };
15852
- const handlersReady = new Promise((resolve20) => {
15853
- dispatchHandlersReady = resolve20;
15854
- });
15855
- let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
15856
- if (!params.incognito && chatKeyBytes && encryptedChatKey) {
15857
- const protocolVersion = 1;
15858
- const chatKeyVersion = 1;
15859
- const turnId = randomUUID9();
15860
- savedTurnId = turnId;
15861
- terminalExpectedMessagesV = baselineMessagesV + 1;
15862
- const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
15863
- Buffer.from(chatKeyBytes).toString("base64url"),
15864
- chatId,
15865
- chatKeyVersion
15866
- );
15867
- const encryptedContent = await encryptWithAesGcmCombined(
15868
- finalMessage,
15869
- chatKeyBytes
15870
- );
15871
- const encryptedUserMessage = {
15872
- client_message_id: messageId,
15873
- chat_id: chatId,
15874
- encrypted_content: encryptedContent,
15875
- role: "user",
15876
- created_at: createdAt,
15877
- updated_at: createdAt
15878
- };
15879
- encryptedUserMessage.encrypted_sender_name = await encryptWithAesGcmCombined(
15880
- teamSenderName,
15881
- chatKeyBytes
15882
- );
15883
- if (piiMappings.length > 0) {
15884
- encryptedUserMessage.encrypted_pii_mappings = await encryptWithAesGcmCombined(
15885
- JSON.stringify(piiMappings),
15886
- chatKeyBytes
15837
+ if (params.learningMode) {
15838
+ messagePayload.learning_mode = {
15839
+ enabled: params.learningMode.enabled,
15840
+ age_group: params.learningMode.ageGroup ?? null
15841
+ };
15842
+ }
15843
+ if (params.incognito) {
15844
+ const providedHistory = (params.messageHistory ?? []).map(
15845
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, historyMessage.chat_id ?? chatId)
15846
+ );
15847
+ messagePayload.message_history = [...providedHistory, {
15848
+ message_id: messageId,
15849
+ chat_id: chatId,
15850
+ role: "user",
15851
+ sender_name: "User",
15852
+ content: finalMessage,
15853
+ created_at: createdAt
15854
+ }];
15855
+ } else if (messageHistoryForRequest && messageHistoryForRequest.length > 0) {
15856
+ messagePayload.message_history = messageHistoryForRequest.map(
15857
+ (historyMessage) => buildInferenceHistoryMessage(historyMessage, chatId)
15887
15858
  );
15888
15859
  }
15889
- Object.assign(messagePayload, {
15890
- turn_id: turnId,
15891
- recovery_public_key: recoveryKeypair.publicKey,
15892
- chat_key_version: chatKeyVersion
15893
- });
15894
- let preflightInferenceRequest = messagePayload;
15895
- if (teamId) {
15896
- const teamInferenceHistory = [
15897
- ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
15898
- role: historyMessage.role,
15899
- content: historyMessage.content,
15900
- category: historyMessage.category ?? null,
15901
- sender_name: historyMessage.sender_name ?? historyMessage.role,
15902
- created_at: historyMessage.created_at
15903
- })),
15904
- {
15905
- role: "user",
15906
- content: finalMessage,
15907
- category: null,
15908
- sender_name: teamSenderName,
15909
- created_at: createdAt
15860
+ if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
15861
+ messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
15862
+ embed_id: embed.embedId,
15863
+ type: embed.type,
15864
+ content: embed.content,
15865
+ status: embed.status,
15866
+ text_preview: embed.textPreview
15867
+ }));
15868
+ }
15869
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
15870
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
15871
+ if (!ownerId2) {
15872
+ throw new Error("Authenticated user identity is required to encrypt embeds.");
15873
+ }
15874
+ if (!chatKeyBytes) {
15875
+ throw new Error("Saved chat key is required to encrypt every prepared embed.");
15876
+ }
15877
+ if (new Set(params.preparedEmbeds.map((embed) => embed.embedId)).size !== params.preparedEmbeds.length) {
15878
+ throw new Error("Prepared message embed IDs must be unique.");
15879
+ }
15880
+ const preparedIds = params.preparedEmbeds.map((embed) => embed.embedId);
15881
+ if (preparedIds.some((id) => !id || id.length > 128)) {
15882
+ throw new Error("Prepared message embed ID is invalid.");
15883
+ }
15884
+ const canonicalReady = /* @__PURE__ */ new Set();
15885
+ for (let offset = 0; offset < preparedIds.length; offset += 20) {
15886
+ const selectedIds = preparedIds.slice(offset, offset + 20);
15887
+ if (Buffer.byteLength(JSON.stringify({ embed_ids: selectedIds }), "utf8") > 4096) {
15888
+ throw new Error("Message embed availability request exceeds its byte limit.");
15910
15889
  }
15911
- ];
15912
- const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId };
15913
- delete messagePayload.message_history;
15914
- messagePayload.message = teamMessageEnvelope;
15915
- if (shouldWaitForAi) {
15916
- messagePayload.team_ai_invocation = { history: teamInferenceHistory };
15917
- preflightInferenceRequest = {
15918
- ...messagePayload,
15919
- message_history: teamInferenceHistory
15920
- };
15890
+ const path2 = this.appendTeamQuery(
15891
+ `/v1/embeds/chats/${encodeURIComponent(chatId)}/references/availability`,
15892
+ { teamId }
15893
+ );
15894
+ const availability = await this.http.post(path2, { embed_ids: selectedIds }, this.getCliRequestHeaders());
15895
+ if (!availability.ok) {
15896
+ throw new Error(`Message embed availability lookup failed with HTTP ${availability.status}.`);
15897
+ }
15898
+ const page = classifyMessageEmbedAvailability(selectedIds, availability.data.results);
15899
+ for (const id of page.ready) canonicalReady.add(id);
15900
+ }
15901
+ const masterKey = this.getMasterKeyBytes();
15902
+ for (const embed of params.preparedEmbeds) {
15903
+ if (canonicalReady.has(embed.embedId)) continue;
15904
+ const encrypted = await encryptEmbed(
15905
+ embed,
15906
+ masterKey,
15907
+ chatKeyBytes,
15908
+ chatId,
15909
+ messageId,
15910
+ ownerId2
15911
+ );
15912
+ if (!encrypted?.encrypted_content || !encrypted.encrypted_type || encrypted.embed_keys.length !== 2) {
15913
+ throw new Error(`Prepared message embed ${embed.embedId} is missing required ciphertext or key wrappers.`);
15914
+ }
15915
+ encryptedEmbeds.push(encrypted);
15921
15916
  }
15922
15917
  }
15923
- const preflightPayload = {
15924
- protocol_version: protocolVersion,
15925
- chat_id: chatId,
15926
- ...teamId ? { team_id: teamId } : {},
15927
- turn_id: turnId,
15928
- message_id: messageId,
15929
- chat_key_version: chatKeyVersion,
15930
- encrypted_chat_key: encryptedChatKey,
15931
- recovery_public_key: recoveryKeypair.publicKey,
15932
- expected_messages_v: baselineMessagesV,
15933
- encrypted_user_message: encryptedUserMessage,
15934
- inference_request: preflightInferenceRequest
15918
+ if (!params.incognito && encryptedEmbeds.length > 0) {
15919
+ if (!ownerId2) throw new Error("Authenticated owner identity is required for encrypted message embeds.");
15920
+ assertCompleteEncryptedEmbedBundle(encryptedEmbeds, { chatId, messageId, ownerId: ownerId2 });
15921
+ }
15922
+ if (encryptedEmbeds.length > 0) {
15923
+ messagePayload.encrypted_embeds = encryptedEmbeds;
15924
+ }
15925
+ let dispatchHandlersReady = () => {
15935
15926
  };
15936
- if (isNewChat) {
15937
- if (!chatSlugLookupKey) {
15938
- throw new Error("Encrypted chat slug lookup key was not initialized.");
15939
- }
15940
- const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
15941
- const slugMetadata = await buildEncryptedObjectSlugMetadata({
15942
- value: selectNewChatSlugValue({
15943
- message: finalMessage,
15944
- chatId,
15945
- explicitSlug: params.slug
15946
- }),
15947
- encryptionKey: chatKeyBytes,
15948
- lookupKey: chatSlugLookupKey
15949
- });
15950
- preflightPayload.encrypted_chat_metadata = {
15951
- encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
15952
- encrypted_slug: slugMetadata.encrypted_slug,
15953
- slug_lookup_hash: slugMetadata.slug_lookup_hash,
15954
- encrypted_chat_key: encryptedChatKey,
15927
+ const handlersReady = new Promise((resolve20) => {
15928
+ dispatchHandlersReady = resolve20;
15929
+ });
15930
+ let precollectedResponse = params.precollectResponse && params.incognito && shouldWaitForAi ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
15931
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
15932
+ const protocolVersion = 1;
15933
+ const chatKeyVersion = 1;
15934
+ const turnId = randomUUID9();
15935
+ savedTurnId = turnId;
15936
+ terminalExpectedMessagesV = baselineMessagesV + 1;
15937
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
15938
+ Buffer.from(chatKeyBytes).toString("base64url"),
15939
+ chatId,
15940
+ chatKeyVersion
15941
+ );
15942
+ const encryptedContent = await encryptWithAesGcmCombined(
15943
+ finalMessage,
15944
+ chatKeyBytes
15945
+ );
15946
+ const encryptedUserMessage = {
15947
+ client_message_id: messageId,
15948
+ chat_id: chatId,
15949
+ encrypted_content: encryptedContent,
15950
+ role: "user",
15955
15951
  created_at: createdAt,
15956
15952
  updated_at: createdAt
15957
15953
  };
15958
- }
15959
- let ackPayload;
15960
- try {
15954
+ encryptedUserMessage.encrypted_sender_name = await encryptWithAesGcmCombined(
15955
+ teamSenderName,
15956
+ chatKeyBytes
15957
+ );
15958
+ if (piiMappings.length > 0) {
15959
+ encryptedUserMessage.encrypted_pii_mappings = await encryptWithAesGcmCombined(
15960
+ JSON.stringify(piiMappings),
15961
+ chatKeyBytes
15962
+ );
15963
+ }
15964
+ Object.assign(messagePayload, {
15965
+ turn_id: turnId,
15966
+ recovery_public_key: recoveryKeypair.publicKey,
15967
+ chat_key_version: chatKeyVersion
15968
+ });
15969
+ let preflightInferenceRequest = messagePayload;
15961
15970
  if (teamId) {
15962
- await stageCliTeamNotificationPreview(ws, {
15963
- teamId,
15964
- chatId,
15965
- messageId,
15966
- content: finalMessage,
15967
- title: notificationTitle
15968
- });
15971
+ const teamInferenceHistory = [
15972
+ ...(messageHistoryForRequest ?? []).map((historyMessage) => ({
15973
+ role: historyMessage.role,
15974
+ content: historyMessage.content,
15975
+ category: historyMessage.category ?? null,
15976
+ sender_name: historyMessage.sender_name ?? historyMessage.role,
15977
+ created_at: historyMessage.created_at
15978
+ })),
15979
+ {
15980
+ role: "user",
15981
+ content: finalMessage,
15982
+ category: null,
15983
+ sender_name: teamSenderName,
15984
+ created_at: createdAt
15985
+ }
15986
+ ];
15987
+ const teamMessageEnvelope = { ...encryptedUserMessage, message_id: messageId };
15988
+ delete messagePayload.message_history;
15989
+ messagePayload.message = teamMessageEnvelope;
15990
+ if (shouldWaitForAi) {
15991
+ messagePayload.team_ai_invocation = { history: teamInferenceHistory };
15992
+ preflightInferenceRequest = {
15993
+ ...messagePayload,
15994
+ message_history: teamInferenceHistory
15995
+ };
15996
+ }
15969
15997
  }
15970
- if (params.projectId) {
15971
- await prepareCliProjectFocusForPreflight({
15972
- ws,
15973
- chatId,
15974
- teamId,
15975
- isNewChat,
15976
- encryptedChatKey,
15977
- createdAt,
15978
- activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
15998
+ const preflightPayload = {
15999
+ protocol_version: protocolVersion,
16000
+ chat_id: chatId,
16001
+ ...teamId ? { team_id: teamId } : {},
16002
+ turn_id: turnId,
16003
+ message_id: messageId,
16004
+ chat_key_version: chatKeyVersion,
16005
+ encrypted_chat_key: encryptedChatKey,
16006
+ recovery_public_key: recoveryKeypair.publicKey,
16007
+ expected_messages_v: baselineMessagesV,
16008
+ encrypted_user_message: encryptedUserMessage,
16009
+ inference_request: preflightInferenceRequest
16010
+ };
16011
+ if (isNewChat) {
16012
+ if (!chatSlugLookupKey) {
16013
+ throw new Error("Encrypted chat slug lookup key was not initialized.");
16014
+ }
16015
+ const initialTitle = teamId && !shouldWaitForAi ? teamChatTitleFromMessage(params.message) : "";
16016
+ const slugMetadata = await buildEncryptedObjectSlugMetadata({
16017
+ value: selectNewChatSlugValue({
16018
+ message: finalMessage,
16019
+ chatId,
16020
+ explicitSlug: params.slug
16021
+ }),
16022
+ encryptionKey: chatKeyBytes,
16023
+ lookupKey: chatSlugLookupKey
15979
16024
  });
15980
- const focusedContext = await prepareCliJevContext(
15981
- this,
15982
- chatId,
15983
- { teamId, personal: !teamId },
15984
- params.jevContext,
15985
- finalMessage
15986
- );
15987
- Object.assign(messagePayload, focusedContext);
15988
- Object.assign(preflightInferenceRequest, focusedContext);
16025
+ preflightPayload.encrypted_chat_metadata = {
16026
+ encrypted_title: await encryptWithAesGcmCombined(initialTitle, chatKeyBytes),
16027
+ encrypted_slug: slugMetadata.encrypted_slug,
16028
+ slug_lookup_hash: slugMetadata.slug_lookup_hash,
16029
+ encrypted_chat_key: encryptedChatKey,
16030
+ created_at: createdAt,
16031
+ updated_at: createdAt
16032
+ };
15989
16033
  }
15990
- const preflightAck = ws.waitForMessage(
15991
- "chat_turn_preflight_ack",
15992
- (payload) => payload.turn_id === turnId
15993
- );
16034
+ let ackPayload;
15994
16035
  try {
15995
- await ws.sendAsync("chat_turn_preflight", preflightPayload);
16036
+ if (teamId) {
16037
+ await stageCliTeamNotificationPreview(ws, {
16038
+ teamId,
16039
+ chatId,
16040
+ messageId,
16041
+ content: finalMessage,
16042
+ title: notificationTitle
16043
+ });
16044
+ }
16045
+ if (params.projectId) {
16046
+ await prepareCliProjectFocusForPreflight({
16047
+ ws,
16048
+ chatId,
16049
+ teamId,
16050
+ isNewChat,
16051
+ encryptedChatKey,
16052
+ createdAt,
16053
+ activateFocus: () => activateCliProjectFocus(this, params.projectId, chatId, teamId)
16054
+ });
16055
+ const focusedContext = await prepareCliJevContext(
16056
+ this,
16057
+ chatId,
16058
+ { teamId, personal: !teamId },
16059
+ params.jevContext,
16060
+ finalMessage
16061
+ );
16062
+ Object.assign(messagePayload, focusedContext);
16063
+ Object.assign(preflightInferenceRequest, focusedContext);
16064
+ }
16065
+ const preflightAck = ws.waitForMessage(
16066
+ "chat_turn_preflight_ack",
16067
+ (payload) => payload.turn_id === turnId
16068
+ );
16069
+ try {
16070
+ await ws.sendAsync("chat_turn_preflight", preflightPayload);
16071
+ } catch (error) {
16072
+ void preflightAck.catch(() => {
16073
+ });
16074
+ throw error;
16075
+ }
16076
+ ackPayload = (await preflightAck).payload;
15996
16077
  } catch (error) {
15997
- void preflightAck.catch(() => {
15998
- });
16078
+ ws.close();
15999
16079
  throw error;
16000
16080
  }
16001
- ackPayload = (await preflightAck).payload;
16002
- } catch (error) {
16003
- ws.close();
16004
- throw error;
16005
- }
16006
- if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
16007
- ws.close();
16008
- throw new Error("Encrypted chat preflight acknowledgement omitted preflight_id.");
16081
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
16082
+ ws.close();
16083
+ throw new Error("Encrypted chat preflight acknowledgement omitted preflight_id.");
16084
+ }
16085
+ Object.assign(messagePayload, {
16086
+ protocol_version: protocolVersion,
16087
+ preflight_id: ackPayload.preflight_id
16088
+ });
16089
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
16090
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
16091
+ }
16009
16092
  }
16010
- Object.assign(messagePayload, {
16011
- protocol_version: protocolVersion,
16012
- preflight_id: ackPayload.preflight_id
16013
- });
16014
- if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
16015
- terminalExpectedMessagesV = ackPayload.committed_messages_v;
16093
+ if (!params.incognito && shouldWaitForAi) {
16094
+ precollectedResponse = ws.collectAiResponse(messageId, chatId, {
16095
+ onStream: params.onStream,
16096
+ onSubChatEvent: async (event) => {
16097
+ await handlersReady;
16098
+ await handleSubChatEvent(event);
16099
+ },
16100
+ onAppSettingsMemoriesRequest: async (event) => {
16101
+ await handlersReady;
16102
+ await handleAppSettingsMemoriesRequest(event);
16103
+ },
16104
+ timeoutMs: params.responseTimeoutMs,
16105
+ recoveryTurnId: savedTurnId,
16106
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
16107
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
16108
+ });
16016
16109
  }
16017
- }
16018
- if (!params.incognito && shouldWaitForAi) {
16019
- precollectedResponse = ws.collectAiResponse(messageId, chatId, {
16020
- onStream: params.onStream,
16021
- onSubChatEvent: async (event) => {
16022
- await handlersReady;
16023
- await handleSubChatEvent(event);
16024
- },
16025
- onAppSettingsMemoriesRequest: async (event) => {
16026
- await handlersReady;
16027
- await handleAppSettingsMemoriesRequest(event);
16028
- },
16029
- timeoutMs: params.responseTimeoutMs,
16030
- recoveryTurnId: savedTurnId,
16031
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
16032
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
16110
+ const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
16111
+ const event = payload;
16112
+ return event.chat_id === chatId && event.user_message_id === messageId;
16113
+ }, params.responseTimeoutMs ?? 9e4) : null;
16114
+ if (queuedAdmission) void queuedAdmission.catch(() => {
16033
16115
  });
16034
- }
16035
- const queuedAdmission = !params.incognito && shouldWaitForAi ? ws.waitForMessage("message_queued", (payload) => {
16036
- const event = payload;
16037
- return event.chat_id === chatId && event.user_message_id === messageId;
16038
- }, params.responseTimeoutMs ?? 9e4) : null;
16039
- if (queuedAdmission) void queuedAdmission.catch(() => {
16040
- });
16041
- const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
16042
- const event = payload;
16043
- return event.chat_id === chatId && (event.message_id === messageId || event.message_id === void 0);
16044
- }, params.responseTimeoutMs ?? 9e4) : null;
16045
- if (pausedHandoff) void pausedHandoff.catch(() => {
16046
- });
16047
- const confirmed = ws.waitForMessage(
16048
- "chat_message_confirmed",
16049
- (payload) => {
16050
- const eventPayload = payload;
16051
- return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
16052
- },
16053
- params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
16054
- );
16055
- let confirmedPayload;
16056
- try {
16057
- await ws.sendAsync("chat_message_added", messagePayload);
16058
- confirmedPayload = (await confirmed).payload;
16059
- } catch (error) {
16060
- dispatchHandlersReady();
16061
- void confirmed.catch(() => {
16116
+ const pausedHandoff = !params.incognito && shouldWaitForAi ? ws.waitForMessage("queued_handoff_paused", (payload) => {
16117
+ const event = payload;
16118
+ return event.chat_id === chatId && (event.message_id === messageId || event.message_id === void 0);
16119
+ }, params.responseTimeoutMs ?? 9e4) : null;
16120
+ if (pausedHandoff) void pausedHandoff.catch(() => {
16062
16121
  });
16063
- if (precollectedResponse) void precollectedResponse.catch(() => {
16064
- });
16065
- ws.close();
16066
- throw error;
16067
- }
16068
- if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
16069
- terminalExpectedMessagesV = confirmedPayload.new_messages_v;
16070
- }
16071
- if (!params.incognito && (!teamId || shouldWaitForAi)) {
16072
- clearSyncCache(teamId);
16073
- }
16074
- let assistant = "";
16075
- let assistantMessageId = null;
16076
- let category = null;
16077
- let modelName = null;
16078
- let tokenUsage = null;
16079
- let promptBudget = null;
16080
- let followUpSuggestions = [];
16081
- let taskProposals = [];
16082
- let taskUpdateProposals = [];
16083
- let taskEvents = [];
16084
- let pendingTaskUpdateJobs = [];
16085
- let subChatEvents = [];
16086
- const appSettingsMemoryRequests = [];
16087
- const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
16088
- const isApprovalWithinServerLimits = (request) => {
16089
- const count = request.subChats.length;
16090
- if (count === 0) return false;
16091
- if (request.remainingSubChats !== null && count > request.remainingSubChats) {
16092
- return false;
16122
+ const confirmed = ws.waitForMessage(
16123
+ "chat_message_confirmed",
16124
+ (payload) => {
16125
+ const eventPayload = payload;
16126
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
16127
+ },
16128
+ params.responseTimeoutMs ?? DEFAULT_CHAT_MESSAGE_CONFIRMATION_TIMEOUT_MS
16129
+ );
16130
+ let confirmedPayload;
16131
+ try {
16132
+ await ws.sendAsync("chat_message_added", messagePayload);
16133
+ confirmedPayload = (await confirmed).payload;
16134
+ } catch (error) {
16135
+ dispatchHandlersReady();
16136
+ void confirmed.catch(() => {
16137
+ });
16138
+ if (precollectedResponse) void precollectedResponse.catch(() => {
16139
+ });
16140
+ ws.close();
16141
+ throw error;
16093
16142
  }
16094
- if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
16095
- return false;
16143
+ if (!params.incognito && typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
16144
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v;
16096
16145
  }
16097
- return true;
16098
- };
16099
- const isAutoApprovalWithinServerLimits = (request) => {
16100
- if (!isApprovalWithinServerLimits(request)) return false;
16101
- return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
16102
- };
16103
- const handleSubChatEvent = async (event) => {
16104
- params.onSubChatEvent?.(event);
16105
- if (event.type === "spawn_sub_chats") {
16106
- if (params.incognito || !chatKeyBytes) return;
16107
- const rawSubChats = event.payload.sub_chats;
16108
- if (!Array.isArray(rawSubChats)) return;
16109
- const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
16110
- parentChatId: chatId,
16111
- parentChatKey: chatKeyBytes,
16112
- encryptedParentChatKey: encryptedChatKey,
16113
- subChats: rawSubChats
16114
- });
16115
- for (const metadataPayload of metadataPayloads) {
16116
- await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
16117
- }
16118
- return;
16146
+ if (!params.incognito && (!teamId || shouldWaitForAi)) {
16147
+ clearSyncCache(teamId);
16119
16148
  }
16120
- if (event.type !== "sub_chat_confirmation_required") return;
16121
- const payload = event.payload;
16122
- const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
16123
- const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
16124
- const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
16125
- if (!taskId) return;
16126
- const request = {
16127
- chatId: confirmationChatId,
16128
- taskId,
16129
- subChats,
16130
- maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
16131
- maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
16132
- existingSubChats: numberOrNull(payload.existing_sub_chats),
16133
- remainingSubChats: numberOrNull(payload.remaining_sub_chats)
16134
- };
16135
- const withinLimits = isApprovalWithinServerLimits(request);
16136
- const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
16137
- await ws.sendAsync(
16138
- "sub_chat_confirmation",
16139
- buildSubChatConfirmationPayload({
16140
- chatId: request.chatId,
16141
- taskId: request.taskId,
16142
- approved,
16143
- approveCount: approved ? request.subChats.length : void 0
16144
- })
16145
- );
16146
- };
16147
- const persistSystemMessage = async (systemMessage) => {
16148
- if (!chatKeyBytes) return;
16149
- await ws.sendAsync("chat_system_message_added", {
16150
- chat_id: chatId,
16151
- message: {
16152
- message_id: systemMessage.message_id,
16153
- role: "system",
16154
- encrypted_content: await encryptWithAesGcmCombined(
16155
- systemMessage.content,
16156
- chatKeyBytes
16157
- ),
16158
- created_at: systemMessage.created_at,
16159
- user_message_id: systemMessage.user_message_id
16149
+ let assistant = "";
16150
+ let assistantMessageId = null;
16151
+ let category = null;
16152
+ let modelName = null;
16153
+ let tokenUsage = null;
16154
+ let promptBudget = null;
16155
+ let followUpSuggestions = [];
16156
+ let taskProposals = [];
16157
+ let taskUpdateProposals = [];
16158
+ let taskEvents = [];
16159
+ let pendingTaskUpdateJobs = [];
16160
+ let subChatEvents = [];
16161
+ const appSettingsMemoryRequests = [];
16162
+ const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
16163
+ const isApprovalWithinServerLimits = (request) => {
16164
+ const count = request.subChats.length;
16165
+ if (count === 0) return false;
16166
+ if (request.remainingSubChats !== null && count > request.remainingSubChats) {
16167
+ return false;
16160
16168
  }
16161
- });
16162
- await ws.waitForMessage(
16163
- "system_message_confirmed",
16164
- (payload) => {
16165
- const p = payload;
16166
- return p.message_id === systemMessage.message_id;
16167
- },
16168
- 2e4
16169
- );
16170
- };
16171
- const persistMemoryRequestSystemMessage = async (event) => {
16172
- const requestId = event.requestId ?? `${messageId}-memory-request`;
16173
- await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
16174
- userMessageId: messageId,
16175
- requestId,
16176
- requestedKeys: event.requestedKeys,
16177
- entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
16178
- createdAt: Math.floor(Date.now() / 1e3)
16179
- }));
16180
- };
16181
- const responseCategoriesFromMemories = (approvedMemories) => {
16182
- const counts = /* @__PURE__ */ new Map();
16183
- for (const memory of approvedMemories) {
16184
- const key = `${memory.app_id}-${memory.item_key}`;
16185
- const existing = counts.get(key);
16186
- if (existing) {
16187
- existing.entryCount += 1;
16188
- } else {
16189
- counts.set(key, {
16190
- appId: memory.app_id,
16191
- itemType: memory.item_key,
16192
- entryCount: 1
16169
+ if (request.maxDirectSubChats !== null && request.existingSubChats !== null && request.existingSubChats + count > request.maxDirectSubChats) {
16170
+ return false;
16171
+ }
16172
+ return true;
16173
+ };
16174
+ const isAutoApprovalWithinServerLimits = (request) => {
16175
+ if (!isApprovalWithinServerLimits(request)) return false;
16176
+ return request.maxAutoSubChats === null || request.subChats.length <= request.maxAutoSubChats;
16177
+ };
16178
+ const handleSubChatEvent = async (event) => {
16179
+ params.onSubChatEvent?.(event);
16180
+ if (event.type === "spawn_sub_chats") {
16181
+ if (params.incognito || !chatKeyBytes) return;
16182
+ const rawSubChats = event.payload.sub_chats;
16183
+ if (!Array.isArray(rawSubChats)) return;
16184
+ const metadataPayloads = await buildSubChatEncryptedMetadataPayloads({
16185
+ parentChatId: chatId,
16186
+ parentChatKey: chatKeyBytes,
16187
+ encryptedParentChatKey: encryptedChatKey,
16188
+ subChats: rawSubChats
16193
16189
  });
16190
+ for (const metadataPayload of metadataPayloads) {
16191
+ await ws.sendAsync("encrypted_chat_metadata", metadataPayload);
16192
+ }
16193
+ return;
16194
16194
  }
16195
- }
16196
- return [...counts.values()];
16197
- };
16198
- const handleAppSettingsMemoriesRequest = async (event) => {
16199
- await persistMemoryRequestSystemMessage(event);
16200
- if (params.autoApproveMemories) {
16201
- const requested = new Set(event.requestedKeys);
16202
- const unadvertisedKeys = event.requestedKeys.filter(
16203
- (key) => !memoryMetadataKeys.includes(key)
16195
+ if (event.type !== "sub_chat_confirmation_required") return;
16196
+ const payload = event.payload;
16197
+ const confirmationChatId = typeof payload.chat_id === "string" ? payload.chat_id : chatId;
16198
+ const taskId = typeof payload.task_id === "string" ? payload.task_id : null;
16199
+ const subChats = Array.isArray(payload.sub_chats) ? payload.sub_chats : [];
16200
+ if (!taskId) return;
16201
+ const request = {
16202
+ chatId: confirmationChatId,
16203
+ taskId,
16204
+ subChats,
16205
+ maxAutoSubChats: numberOrNull(payload.max_auto_sub_chats),
16206
+ maxDirectSubChats: numberOrNull(payload.max_direct_sub_chats),
16207
+ existingSubChats: numberOrNull(payload.existing_sub_chats),
16208
+ remainingSubChats: numberOrNull(payload.remaining_sub_chats)
16209
+ };
16210
+ const withinLimits = isApprovalWithinServerLimits(request);
16211
+ const approved = params.autoApproveSubChats ? isAutoApprovalWithinServerLimits(request) : withinLimits && params.onSubChatApprovalRequest ? await params.onSubChatApprovalRequest(request) : false;
16212
+ await ws.sendAsync(
16213
+ "sub_chat_confirmation",
16214
+ buildSubChatConfirmationPayload({
16215
+ chatId: request.chatId,
16216
+ taskId: request.taskId,
16217
+ approved,
16218
+ approveCount: approved ? request.subChats.length : void 0
16219
+ })
16204
16220
  );
16205
- if (unadvertisedKeys.length > 0) {
16206
- throw new Error(
16207
- `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
16208
- );
16209
- }
16210
- const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
16211
- app_id: memory.app_id,
16212
- item_key: memory.item_type,
16213
- content: memory.data
16214
- }));
16215
- const approvedKeys = [
16216
- ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
16217
- ];
16218
- const categories = responseCategoriesFromMemories(approvedMemories);
16219
- appSettingsMemoryRequests.push({
16220
- requestId: event.requestId,
16221
- requestedKeys: event.requestedKeys,
16222
- approvedKeys,
16223
- entryCount: approvedMemories.length
16224
- });
16225
- await ws.sendAsync("app_settings_memories_confirmed", {
16226
- chat_id: event.chatId,
16227
- app_settings_memories: approvedMemories
16221
+ };
16222
+ const persistSystemMessage = async (systemMessage) => {
16223
+ if (!chatKeyBytes) return;
16224
+ await ws.sendAsync("chat_system_message_added", {
16225
+ chat_id: chatId,
16226
+ message: {
16227
+ message_id: systemMessage.message_id,
16228
+ role: "system",
16229
+ encrypted_content: await encryptWithAesGcmCombined(
16230
+ systemMessage.content,
16231
+ chatKeyBytes
16232
+ ),
16233
+ created_at: systemMessage.created_at,
16234
+ user_message_id: systemMessage.user_message_id
16235
+ }
16228
16236
  });
16229
- await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
16237
+ await ws.waitForMessage(
16238
+ "system_message_confirmed",
16239
+ (payload) => {
16240
+ const p = payload;
16241
+ return p.message_id === systemMessage.message_id;
16242
+ },
16243
+ 2e4
16244
+ );
16245
+ };
16246
+ const persistMemoryRequestSystemMessage = async (event) => {
16247
+ const requestId = event.requestId ?? `${messageId}-memory-request`;
16248
+ await persistSystemMessage(buildAppSettingsMemoryRequestSystemMessage({
16230
16249
  userMessageId: messageId,
16231
- messageId: `${messageId}-memory-response`,
16232
- action: "included",
16233
- categories,
16250
+ requestId,
16251
+ requestedKeys: event.requestedKeys,
16252
+ entryCounts: memoryCountsLoaded ? new Map(event.requestedKeys.map((key) => [key, availableMemories.filter((memory) => `${memory.app_id}-${memory.item_type}` === key).length])) : void 0,
16234
16253
  createdAt: Math.floor(Date.now() / 1e3)
16235
16254
  }));
16236
- return;
16237
- }
16238
- appSettingsMemoryRequests.push({
16239
- requestId: event.requestId,
16240
- requestedKeys: event.requestedKeys,
16241
- approvedKeys: [],
16242
- entryCount: 0
16243
- });
16244
- throw new Error(
16245
- `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.`
16246
- );
16247
- };
16248
- const persistedTaskEventIds = /* @__PURE__ */ new Set();
16249
- const persistTaskEventSystemMessages = async (events) => {
16250
- if (!chatKeyBytes || events.length === 0) return;
16251
- for (const event of events) {
16252
- if (persistedTaskEventIds.has(event.event_id)) continue;
16253
- await this.persistEncryptedSystemMessage(
16254
- ws,
16255
- await buildTaskEventSystemMessage({
16256
- chatKey: chatKeyBytes,
16257
- userMessageId: messageId,
16258
- event
16259
- }),
16260
- chatId
16261
- );
16262
- persistedTaskEventIds.add(event.event_id);
16263
- }
16264
- };
16265
- const streamOpts = {
16266
- onStream: params.onStream,
16267
- onSubChatEvent: handleSubChatEvent,
16268
- onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
16269
- projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
16270
- remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
16271
- };
16272
- dispatchHandlersReady();
16273
- if (!shouldWaitForAi) {
16274
- ws.close();
16275
- } else if (params.incognito) {
16276
- try {
16277
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
16278
- ...streamOpts,
16279
- timeoutMs: params.responseTimeoutMs,
16280
- recoveryTurnId: savedTurnId
16281
- }));
16282
- assistantMessageId = resp.messageId;
16283
- assistant = resp.content;
16284
- category = resp.category;
16285
- modelName = resp.modelName;
16286
- tokenUsage = resp.tokenUsage;
16287
- promptBudget = resp.promptBudget;
16288
- taskEvents = resp.taskEvents;
16289
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
16290
- subChatEvents = resp.subChatEvents;
16291
- if (resp.status === "waiting_for_user") {
16292
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16293
- return {
16294
- status: resp.status,
16295
- chatId,
16296
- messageId: assistantMessageId,
16297
- assistant,
16298
- category,
16299
- modelName,
16300
- mateName: category ? MATE_NAMES[category] ?? null : null,
16301
- followUpSuggestions,
16302
- taskProposals,
16303
- taskUpdateProposals,
16304
- taskEvents,
16305
- pendingTaskUpdateJobs,
16306
- subChatEvents,
16307
- appSettingsMemoryRequests,
16308
- tokenUsage,
16309
- promptBudget
16310
- };
16311
- }
16312
- } finally {
16313
- ws.close();
16314
- }
16315
- } else {
16316
- try {
16317
- const recoverCurrentTurnOutputs = async () => {
16318
- const pages = ws.drainAvailableRecoveryOutputPages();
16319
- if (pages.length === 0) return;
16320
- if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
16321
- if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
16322
- throw new Error("Saved chat recovery output requires the root chat key.");
16255
+ };
16256
+ const responseCategoriesFromMemories = (approvedMemories) => {
16257
+ const counts = /* @__PURE__ */ new Map();
16258
+ for (const memory of approvedMemories) {
16259
+ const key = `${memory.app_id}-${memory.item_key}`;
16260
+ const existing = counts.get(key);
16261
+ if (existing) {
16262
+ existing.entryCount += 1;
16263
+ } else {
16264
+ counts.set(key, {
16265
+ appId: memory.app_id,
16266
+ itemType: memory.item_key,
16267
+ entryCount: 1
16268
+ });
16323
16269
  }
16324
- const cache = loadSyncCache(teamId) ?? {
16325
- syncedAt: Date.now(),
16326
- totalChatCount: 0,
16327
- loadedChatCount: 0,
16328
- chats: [],
16329
- embeds: [],
16330
- embedKeys: [],
16331
- chatKeyWrappers: []
16332
- };
16333
- if (encryptedChatKey) {
16334
- const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
16335
- if (root) root.details.encrypted_chat_key = encryptedChatKey;
16336
- else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
16270
+ }
16271
+ return [...counts.values()];
16272
+ };
16273
+ const handleAppSettingsMemoriesRequest = async (event) => {
16274
+ await persistMemoryRequestSystemMessage(event);
16275
+ if (params.autoApproveMemories) {
16276
+ const requested = new Set(event.requestedKeys);
16277
+ const unadvertisedKeys = event.requestedKeys.filter(
16278
+ (key) => !memoryMetadataKeys.includes(key)
16279
+ );
16280
+ if (unadvertisedKeys.length > 0) {
16281
+ throw new Error(
16282
+ `Refusing to auto-approve unadvertised memory categories: ${unadvertisedKeys.join(", ")}`
16283
+ );
16337
16284
  }
16338
- await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
16339
- };
16340
- const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
16341
- ...streamOpts,
16342
- timeoutMs: params.responseTimeoutMs,
16343
- recoveryTurnId: savedTurnId
16344
- });
16345
- const outcome = await Promise.race([
16346
- responsePromise.then((response) => ({ kind: "response", response })),
16347
- ...queuedAdmission ? [queuedAdmission.then(
16348
- (frame) => ({ kind: "queued", frame }),
16349
- () => new Promise(() => {
16350
- })
16351
- )] : [],
16352
- ...pausedHandoff ? [pausedHandoff.then(
16353
- (frame) => ({ kind: "paused", frame }),
16354
- () => new Promise(() => {
16355
- })
16356
- )] : []
16357
- ]);
16358
- if (outcome.kind === "paused") {
16359
- void responsePromise.catch(() => {
16285
+ const approvedMemories = availableMemories.filter((memory) => requested.has(`${memory.app_id}-${memory.item_type}`)).map((memory) => ({
16286
+ app_id: memory.app_id,
16287
+ item_key: memory.item_type,
16288
+ content: memory.data
16289
+ }));
16290
+ const approvedKeys = [
16291
+ ...new Set(approvedMemories.map((memory) => `${memory.app_id}-${memory.item_key}`))
16292
+ ];
16293
+ const categories = responseCategoriesFromMemories(approvedMemories);
16294
+ appSettingsMemoryRequests.push({
16295
+ requestId: event.requestId,
16296
+ requestedKeys: event.requestedKeys,
16297
+ approvedKeys,
16298
+ entryCount: approvedMemories.length
16360
16299
  });
16361
- throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
16362
- }
16363
- if (outcome.kind === "queued") {
16364
- void responsePromise.catch(() => {
16300
+ await ws.sendAsync("app_settings_memories_confirmed", {
16301
+ chat_id: event.chatId,
16302
+ app_settings_memories: approvedMemories
16365
16303
  });
16366
- const frame = outcome.frame.payload;
16367
- return {
16368
- status: "queued",
16369
- chatId,
16370
- messageId: null,
16304
+ await persistSystemMessage(buildAppSettingsMemoryResponseSystemMessage({
16371
16305
  userMessageId: messageId,
16372
- queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
16373
- queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
16374
- assistant: "",
16375
- category: null,
16376
- modelName: null,
16377
- mateName: null,
16378
- followUpSuggestions: [],
16379
- taskProposals: [],
16380
- taskUpdateProposals: [],
16381
- taskEvents: [],
16382
- pendingTaskUpdateJobs: [],
16383
- subChatEvents: [],
16384
- appSettingsMemoryRequests: [],
16385
- tokenUsage: null,
16386
- promptBudget: null
16387
- };
16306
+ messageId: `${messageId}-memory-response`,
16307
+ action: "included",
16308
+ categories,
16309
+ createdAt: Math.floor(Date.now() / 1e3)
16310
+ }));
16311
+ return;
16388
16312
  }
16389
- const resp = outcome.response;
16390
- assistantMessageId = resp.messageId;
16391
- assistant = resp.content;
16392
- category = resp.category;
16393
- modelName = resp.modelName;
16394
- tokenUsage = resp.tokenUsage;
16395
- promptBudget = resp.promptBudget;
16396
- followUpSuggestions = resp.followUpSuggestions;
16397
- taskProposals = resp.taskProposals;
16398
- taskUpdateProposals = resp.taskUpdateProposals;
16399
- taskEvents = resp.taskEvents;
16400
- pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
16401
- subChatEvents = resp.subChatEvents;
16402
- if (resp.status === "waiting_for_user") {
16403
- await persistTaskEventSystemMessages(taskEvents);
16404
- await recoverCurrentTurnOutputs();
16405
- return {
16406
- status: resp.status,
16407
- chatId,
16408
- messageId: assistantMessageId,
16409
- assistant,
16410
- category,
16411
- modelName,
16412
- mateName: category ? MATE_NAMES[category] ?? null : null,
16413
- followUpSuggestions,
16414
- taskProposals,
16415
- taskUpdateProposals,
16416
- taskEvents,
16417
- pendingTaskUpdateJobs,
16418
- subChatEvents,
16419
- appSettingsMemoryRequests,
16420
- tokenUsage,
16421
- promptBudget
16422
- };
16313
+ appSettingsMemoryRequests.push({
16314
+ requestId: event.requestId,
16315
+ requestedKeys: event.requestedKeys,
16316
+ approvedKeys: [],
16317
+ entryCount: 0
16318
+ });
16319
+ throw new Error(
16320
+ `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.`
16321
+ );
16322
+ };
16323
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
16324
+ const persistTaskEventSystemMessages = async (events) => {
16325
+ if (!chatKeyBytes || events.length === 0) return;
16326
+ for (const event of events) {
16327
+ if (persistedTaskEventIds.has(event.event_id)) continue;
16328
+ await this.persistEncryptedSystemMessage(
16329
+ ws,
16330
+ await buildTaskEventSystemMessage({
16331
+ chatKey: chatKeyBytes,
16332
+ userMessageId: messageId,
16333
+ event
16334
+ }),
16335
+ chatId
16336
+ );
16337
+ persistedTaskEventIds.add(event.event_id);
16423
16338
  }
16424
- if (chatKeyBytes) {
16425
- if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
16426
- throw new Error("Saved chat completion did not include recoverable terminal identity.");
16339
+ };
16340
+ const streamOpts = {
16341
+ onStream: params.onStream,
16342
+ onSubChatEvent: handleSubChatEvent,
16343
+ onAppSettingsMemoriesRequest: handleAppSettingsMemoriesRequest,
16344
+ projectWriteApprovalInteractive: Boolean(params.onProjectWriteApproval),
16345
+ remoteCommandReviewInteractive: Boolean(params.onRemoteCommandReview)
16346
+ };
16347
+ dispatchHandlersReady();
16348
+ if (!shouldWaitForAi) {
16349
+ ws.close();
16350
+ } else if (params.incognito) {
16351
+ try {
16352
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
16353
+ ...streamOpts,
16354
+ timeoutMs: params.responseTimeoutMs,
16355
+ recoveryTurnId: savedTurnId
16356
+ }));
16357
+ assistantMessageId = resp.messageId;
16358
+ assistant = resp.content;
16359
+ category = resp.category;
16360
+ modelName = resp.modelName;
16361
+ tokenUsage = resp.tokenUsage;
16362
+ promptBudget = resp.promptBudget;
16363
+ taskEvents = resp.taskEvents;
16364
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
16365
+ subChatEvents = resp.subChatEvents;
16366
+ if (resp.status === "waiting_for_user") {
16367
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16368
+ return {
16369
+ status: resp.status,
16370
+ chatId,
16371
+ messageId: assistantMessageId,
16372
+ assistant,
16373
+ category,
16374
+ modelName,
16375
+ mateName: category ? MATE_NAMES[category] ?? null : null,
16376
+ followUpSuggestions,
16377
+ taskProposals,
16378
+ taskUpdateProposals,
16379
+ taskEvents,
16380
+ pendingTaskUpdateJobs,
16381
+ subChatEvents,
16382
+ appSettingsMemoryRequests,
16383
+ tokenUsage,
16384
+ promptBudget
16385
+ };
16427
16386
  }
16428
- const recoveryJobId = resp.recoveryJobId;
16429
- let claimRetryDeadline = null;
16430
- let claim;
16431
- let claimRetryDelayMs = 500;
16432
- while (true) {
16433
- const requestId = randomUUID9();
16434
- let matchingLeaseConflict = false;
16435
- const stopWatchingErrors = ws.onMessageType("error", (payload) => {
16436
- if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
16437
- matchingLeaseConflict = true;
16438
- }
16439
- });
16440
- const claimPromise = ws.waitForMessage(
16441
- "recovery_job_claimed",
16442
- (payload) => {
16443
- const received = payload;
16444
- return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
16445
- },
16446
- claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
16447
- );
16448
- try {
16449
- try {
16450
- await ws.sendAsync("recovery_job_claim", {
16451
- protocol_version: 1,
16452
- job_id: recoveryJobId,
16453
- request_id: requestId
16454
- });
16455
- } catch (error) {
16456
- void claimPromise.catch(() => {
16457
- });
16458
- throw error;
16459
- }
16460
- claim = (await claimPromise).payload;
16461
- break;
16462
- } catch (error) {
16463
- if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
16464
- claimRetryDeadline ??= Date.now() + Math.min(
16465
- 1e4,
16466
- Math.max(0, params.responseTimeoutMs ?? 9e4)
16467
- );
16468
- const remainingMs = claimRetryDeadline - Date.now();
16469
- if (remainingMs <= claimRetryDelayMs) throw error;
16470
- await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
16471
- claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
16472
- } finally {
16473
- stopWatchingErrors();
16387
+ } finally {
16388
+ ws.close();
16389
+ }
16390
+ } else {
16391
+ try {
16392
+ const recoverCurrentTurnOutputs = async () => {
16393
+ const pages = ws.drainAvailableRecoveryOutputPages();
16394
+ if (pages.length === 0) return;
16395
+ if (!ownerId2) throw new Error("Saved chat recovery output requires the owner identity.");
16396
+ if (pages.some((page) => page.some((output) => output.root_chat_id === chatId || output.target_chat_id === chatId)) && (!chatKeyBytes || !encryptedChatKey)) {
16397
+ throw new Error("Saved chat recovery output requires the root chat key.");
16474
16398
  }
16399
+ const cache = loadSyncCache(teamId) ?? {
16400
+ syncedAt: Date.now(),
16401
+ totalChatCount: 0,
16402
+ loadedChatCount: 0,
16403
+ chats: [],
16404
+ embeds: [],
16405
+ embedKeys: [],
16406
+ chatKeyWrappers: []
16407
+ };
16408
+ if (encryptedChatKey) {
16409
+ const root = cache.chats.find((chat) => String(chat.details.id ?? "") === chatId);
16410
+ if (root) root.details.encrypted_chat_key = encryptedChatKey;
16411
+ else cache.chats.push({ details: { id: chatId, encrypted_chat_key: encryptedChatKey }, messages: [] });
16412
+ }
16413
+ await this.replayCurrentTurnRecoveryOutputPages(ws, ownerId2, pages, cache, teamId, chatId);
16414
+ };
16415
+ const responsePromise = precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
16416
+ ...streamOpts,
16417
+ timeoutMs: params.responseTimeoutMs,
16418
+ recoveryTurnId: savedTurnId
16419
+ });
16420
+ const outcome = await Promise.race([
16421
+ responsePromise.then((response) => ({ kind: "response", response })),
16422
+ ...queuedAdmission ? [queuedAdmission.then(
16423
+ (frame) => ({ kind: "queued", frame }),
16424
+ () => new Promise(() => {
16425
+ })
16426
+ )] : [],
16427
+ ...pausedHandoff ? [pausedHandoff.then(
16428
+ (frame) => ({ kind: "paused", frame }),
16429
+ () => new Promise(() => {
16430
+ })
16431
+ )] : []
16432
+ ]);
16433
+ if (outcome.kind === "paused") {
16434
+ void responsePromise.catch(() => {
16435
+ });
16436
+ throw new Error("Message saved, but AI handoff is paused. No assistant run started; check the chat before retrying.");
16475
16437
  }
16476
- const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
16477
- const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
16478
- const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
16479
- const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
16480
- const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
16481
- if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
16482
- assistant = resp.content;
16483
- category = resp.category;
16484
- modelName = resp.modelName;
16485
- assistantMessageId = assistantId;
16486
- await this.persistStreamedEmbeds({
16487
- ws,
16488
- embeds: resp.embeds,
16489
- chatId,
16490
- chatKeyBytes,
16491
- fallbackMessageId: assistantId,
16492
- ownerId: ownerId2
16438
+ if (outcome.kind === "queued") {
16439
+ void responsePromise.catch(() => {
16493
16440
  });
16494
- clearSyncCache(teamId);
16495
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16441
+ const frame = outcome.frame.payload;
16442
+ return {
16443
+ status: "queued",
16444
+ chatId,
16445
+ messageId: null,
16446
+ userMessageId: messageId,
16447
+ queueMode: frame.queue_mode === "steer" ? "steer" : "follow_up",
16448
+ queueRevision: typeof frame.queue_revision === "number" ? frame.queue_revision : 1,
16449
+ assistant: "",
16450
+ category: null,
16451
+ modelName: null,
16452
+ mateName: null,
16453
+ followUpSuggestions: [],
16454
+ taskProposals: [],
16455
+ taskUpdateProposals: [],
16456
+ taskEvents: [],
16457
+ pendingTaskUpdateJobs: [],
16458
+ subChatEvents: [],
16459
+ appSettingsMemoryRequests: [],
16460
+ tokenUsage: null,
16461
+ promptBudget: null
16462
+ };
16463
+ }
16464
+ const resp = outcome.response;
16465
+ assistantMessageId = resp.messageId;
16466
+ assistant = resp.content;
16467
+ category = resp.category;
16468
+ modelName = resp.modelName;
16469
+ tokenUsage = resp.tokenUsage;
16470
+ promptBudget = resp.promptBudget;
16471
+ followUpSuggestions = resp.followUpSuggestions;
16472
+ taskProposals = resp.taskProposals;
16473
+ taskUpdateProposals = resp.taskUpdateProposals;
16474
+ taskEvents = resp.taskEvents;
16475
+ pendingTaskUpdateJobs = taskUpdateJobsEnabled ? resp.pendingTaskUpdateJobs : [];
16476
+ subChatEvents = resp.subChatEvents;
16477
+ if (resp.status === "waiting_for_user") {
16496
16478
  await persistTaskEventSystemMessages(taskEvents);
16497
16479
  await recoverCurrentTurnOutputs();
16498
- await projectFocusCountdown?.flush();
16499
- await focusPersistence;
16500
- await chatContextEvents?.flush();
16501
- if (focusPersistenceError) throw focusPersistenceError;
16502
- const mateName2 = category ? MATE_NAMES[category] ?? null : null;
16503
16480
  return {
16504
- status: "completed",
16481
+ status: resp.status,
16505
16482
  chatId,
16506
16483
  messageId: assistantMessageId,
16507
16484
  assistant,
16508
16485
  category,
16509
16486
  modelName,
16510
- mateName: mateName2,
16487
+ mateName: category ? MATE_NAMES[category] ?? null : null,
16511
16488
  followUpSuggestions,
16512
16489
  taskProposals,
16513
16490
  taskUpdateProposals,
@@ -16519,223 +16496,326 @@ var OpenMatesClient = class _OpenMatesClient {
16519
16496
  promptBudget
16520
16497
  };
16521
16498
  }
16522
- 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") {
16523
- throw new Error("Recovery job claim returned invalid lease or identity data.");
16524
- }
16525
- const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
16526
- Buffer.from(chatKeyBytes).toString("base64url"),
16527
- chatId,
16528
- keyVersion
16529
- );
16530
- let envelope;
16531
- try {
16532
- envelope = JSON.parse(claim.sealed_payload);
16533
- } catch {
16534
- throw new Error("Recovery job contained an invalid sealed envelope.");
16535
- }
16536
- const plaintext = await openChatCompletionRecoveryEnvelope(
16537
- envelope,
16538
- {
16539
- recoveryPrivateKey: recoveryKeypair.privateKey,
16540
- ownerId: ownerId2,
16499
+ if (chatKeyBytes) {
16500
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId2) {
16501
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
16502
+ }
16503
+ const recoveryJobId = resp.recoveryJobId;
16504
+ let claimRetryDeadline = null;
16505
+ let claim;
16506
+ let claimRetryDelayMs = 500;
16507
+ while (true) {
16508
+ const requestId = randomUUID9();
16509
+ let matchingLeaseConflict = false;
16510
+ const stopWatchingErrors = ws.onMessageType("error", (payload) => {
16511
+ if (payload?.code === "lease_conflict" && payload.job_id === recoveryJobId && payload.request_id === requestId) {
16512
+ matchingLeaseConflict = true;
16513
+ }
16514
+ });
16515
+ const claimPromise = ws.waitForMessage(
16516
+ "recovery_job_claimed",
16517
+ (payload) => {
16518
+ const received = payload;
16519
+ return received.job_id === recoveryJobId && (received.request_id === void 0 || received.request_id === requestId);
16520
+ },
16521
+ claimRetryDeadline === null ? 2e4 : Math.min(2e4, Math.max(1, claimRetryDeadline - Date.now()))
16522
+ );
16523
+ try {
16524
+ try {
16525
+ await ws.sendAsync("recovery_job_claim", {
16526
+ protocol_version: 1,
16527
+ job_id: recoveryJobId,
16528
+ request_id: requestId
16529
+ });
16530
+ } catch (error) {
16531
+ void claimPromise.catch(() => {
16532
+ });
16533
+ throw error;
16534
+ }
16535
+ claim = (await claimPromise).payload;
16536
+ break;
16537
+ } catch (error) {
16538
+ if (!(error instanceof WebSocketProtocolError) || error.code !== "lease_conflict" || !matchingLeaseConflict) throw error;
16539
+ claimRetryDeadline ??= Date.now() + Math.min(
16540
+ 1e4,
16541
+ Math.max(0, params.responseTimeoutMs ?? 9e4)
16542
+ );
16543
+ const remainingMs = claimRetryDeadline - Date.now();
16544
+ if (remainingMs <= claimRetryDelayMs) throw error;
16545
+ await new Promise((resolve20) => setTimeout(resolve20, claimRetryDelayMs));
16546
+ claimRetryDelayMs = Math.min(claimRetryDelayMs * 2, 2e3);
16547
+ } finally {
16548
+ stopWatchingErrors();
16549
+ }
16550
+ }
16551
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
16552
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
16553
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
16554
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
16555
+ const claimMatchesExpectedTerminal = assistantId && keyVersion === 1 && claim.chat_id === chatId && claim.turn_id === savedTurnId;
16556
+ if (claim.state === "TERMINAL" && claimMatchesExpectedTerminal) {
16557
+ assistant = resp.content;
16558
+ category = resp.category;
16559
+ modelName = resp.modelName;
16560
+ assistantMessageId = assistantId;
16561
+ await this.persistStreamedEmbeds({
16562
+ ws,
16563
+ embeds: resp.embeds,
16564
+ chatId,
16565
+ chatKeyBytes,
16566
+ fallbackMessageId: assistantId,
16567
+ ownerId: ownerId2
16568
+ });
16569
+ clearSyncCache(teamId);
16570
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16571
+ await persistTaskEventSystemMessages(taskEvents);
16572
+ await recoverCurrentTurnOutputs();
16573
+ await projectFocusCountdown?.flush();
16574
+ await focusPersistence;
16575
+ await chatContextEvents?.flush();
16576
+ if (focusPersistenceError) throw focusPersistenceError;
16577
+ const mateName2 = category ? MATE_NAMES[category] ?? null : null;
16578
+ return {
16579
+ status: "completed",
16580
+ chatId,
16581
+ messageId: assistantMessageId,
16582
+ assistant,
16583
+ category,
16584
+ modelName,
16585
+ mateName: mateName2,
16586
+ followUpSuggestions,
16587
+ taskProposals,
16588
+ taskUpdateProposals,
16589
+ taskEvents,
16590
+ pendingTaskUpdateJobs,
16591
+ subChatEvents,
16592
+ appSettingsMemoryRequests,
16593
+ tokenUsage,
16594
+ promptBudget
16595
+ };
16596
+ }
16597
+ 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") {
16598
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
16599
+ }
16600
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
16601
+ Buffer.from(chatKeyBytes).toString("base64url"),
16541
16602
  chatId,
16542
- turnId: savedTurnId,
16543
- jobId: recoveryJobId,
16544
- assistantMessageId: assistantId,
16545
16603
  keyVersion
16604
+ );
16605
+ let envelope;
16606
+ try {
16607
+ envelope = JSON.parse(claim.sealed_payload);
16608
+ } catch {
16609
+ throw new Error("Recovery job contained an invalid sealed envelope.");
16546
16610
  }
16547
- );
16548
- let recovered;
16549
- try {
16550
- recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
16551
- } catch {
16552
- throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
16553
- }
16554
- const expectedRecoveryFields = [
16555
- "assistant_message_id",
16556
- "category",
16557
- "chat_id",
16558
- "content",
16559
- "job_id",
16560
- "key_version",
16561
- "model_name",
16562
- "turn_id"
16563
- ];
16564
- if (Object.keys(recovered).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") {
16565
- throw new Error("Recovery job plaintext did not match the terminal completion identity.");
16566
- }
16567
- assistant = recovered.content;
16568
- category = recovered.category;
16569
- modelName = recovered.model_name;
16570
- const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
16571
- const encryptedAssistantContent = await encryptWithAesGcmCombined(
16572
- assistant,
16573
- chatKeyBytes
16574
- );
16575
- const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
16576
- const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
16577
- const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
16578
- const terminalPersistPayload = {
16579
- protocol_version: 1,
16580
- job_id: recoveryJobId,
16581
- lease_token: leaseToken,
16582
- lease_generation: leaseGeneration,
16583
- encrypted_assistant_message: {
16584
- client_message_id: assistantId,
16585
- chat_id: chatId,
16586
- encrypted_content: encryptedAssistantContent,
16587
- encrypted_sender_name: encryptedSenderName,
16588
- encrypted_category: encryptedCategory,
16589
- encrypted_model_name: encryptedModelName,
16590
- role: "assistant",
16591
- user_message_id: messageId,
16592
- created_at: completedAt,
16593
- updated_at: completedAt
16611
+ const plaintext = await openChatCompletionRecoveryEnvelope(
16612
+ envelope,
16613
+ {
16614
+ recoveryPrivateKey: recoveryKeypair.privateKey,
16615
+ ownerId: ownerId2,
16616
+ chatId,
16617
+ turnId: savedTurnId,
16618
+ jobId: recoveryJobId,
16619
+ assistantMessageId: assistantId,
16620
+ keyVersion
16621
+ }
16622
+ );
16623
+ let recovered;
16624
+ try {
16625
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
16626
+ } catch {
16627
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
16594
16628
  }
16595
- };
16596
- const persistTerminalRecovery = async (expectedMessagesV) => {
16597
- const persistedPromise = ws.waitForMessage(
16598
- "recovery_job_persisted",
16599
- (payload) => payload.job_id === recoveryJobId,
16600
- 2e4
16629
+ const expectedRecoveryFields = [
16630
+ "assistant_message_id",
16631
+ "category",
16632
+ "chat_id",
16633
+ "content",
16634
+ "job_id",
16635
+ "key_version",
16636
+ "model_name",
16637
+ "turn_id"
16638
+ ];
16639
+ if (Object.keys(recovered).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") {
16640
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
16641
+ }
16642
+ assistant = recovered.content;
16643
+ category = recovered.category;
16644
+ modelName = recovered.model_name;
16645
+ const completedAt = resp.recoveryCreatedAt ?? Math.floor(Date.now() / 1e3);
16646
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
16647
+ assistant,
16648
+ chatKeyBytes
16601
16649
  );
16602
- await ws.sendAsync("recovery_job_persist", {
16603
- ...terminalPersistPayload,
16604
- expected_messages_v: expectedMessagesV
16605
- });
16606
- await persistedPromise;
16607
- };
16608
- let terminalPersisted = false;
16609
- try {
16610
- await persistTerminalRecovery(terminalExpectedMessagesV);
16611
- terminalPersisted = true;
16612
- } catch (error) {
16613
- if (error instanceof WebSocketProtocolError) {
16614
- if (error.code === "version_conflict") {
16615
- const latestCache = await this.ensureSynced(true, [chatId], { teamId });
16616
- const latestChat = latestCache.chats.find(
16617
- (chat) => String(chat.details.id ?? "") === chatId
16618
- );
16619
- const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
16620
- if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
16621
- await persistTerminalRecovery(latestMessagesV);
16622
- terminalExpectedMessagesV = latestMessagesV;
16623
- terminalPersisted = true;
16624
- }
16650
+ const encryptedSenderName = await encryptWithAesGcmCombined(category ? MATE_NAMES[category] ?? "Assistant" : "Assistant", chatKeyBytes);
16651
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
16652
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
16653
+ const terminalPersistPayload = {
16654
+ protocol_version: 1,
16655
+ job_id: recoveryJobId,
16656
+ lease_token: leaseToken,
16657
+ lease_generation: leaseGeneration,
16658
+ encrypted_assistant_message: {
16659
+ client_message_id: assistantId,
16660
+ chat_id: chatId,
16661
+ encrypted_content: encryptedAssistantContent,
16662
+ encrypted_sender_name: encryptedSenderName,
16663
+ encrypted_category: encryptedCategory,
16664
+ encrypted_model_name: encryptedModelName,
16665
+ role: "assistant",
16666
+ user_message_id: messageId,
16667
+ created_at: completedAt,
16668
+ updated_at: completedAt
16625
16669
  }
16626
- if (!terminalPersisted) {
16627
- throw new Error(`${error.message} (${error.code})`);
16670
+ };
16671
+ const persistTerminalRecovery = async (expectedMessagesV) => {
16672
+ const persistedPromise = ws.waitForMessage(
16673
+ "recovery_job_persisted",
16674
+ (payload) => payload.job_id === recoveryJobId,
16675
+ 2e4
16676
+ );
16677
+ await ws.sendAsync("recovery_job_persist", {
16678
+ ...terminalPersistPayload,
16679
+ expected_messages_v: expectedMessagesV
16680
+ });
16681
+ await persistedPromise;
16682
+ };
16683
+ let terminalPersisted = false;
16684
+ try {
16685
+ await persistTerminalRecovery(terminalExpectedMessagesV);
16686
+ terminalPersisted = true;
16687
+ } catch (error) {
16688
+ if (error instanceof WebSocketProtocolError) {
16689
+ if (error.code === "version_conflict") {
16690
+ const latestCache = await this.ensureSynced(true, [chatId], { teamId });
16691
+ const latestChat = latestCache.chats.find(
16692
+ (chat) => String(chat.details.id ?? "") === chatId
16693
+ );
16694
+ const latestMessagesV = typeof latestChat?.details.messages_v === "number" && Number.isSafeInteger(latestChat.details.messages_v) ? latestChat.details.messages_v : null;
16695
+ if (latestMessagesV !== null && latestMessagesV > terminalExpectedMessagesV) {
16696
+ await persistTerminalRecovery(latestMessagesV);
16697
+ terminalExpectedMessagesV = latestMessagesV;
16698
+ terminalPersisted = true;
16699
+ }
16700
+ }
16701
+ if (!terminalPersisted) {
16702
+ throw new Error(`${error.message} (${error.code})`);
16703
+ }
16704
+ } else {
16705
+ throw error;
16628
16706
  }
16629
- } else {
16630
- throw error;
16631
16707
  }
16632
- }
16633
- if (!terminalPersisted) {
16634
- throw new Error("Encrypted completion recovery did not persist terminal state.");
16635
- }
16636
- assistantMessageId = assistantId;
16637
- await this.persistStreamedEmbeds({
16638
- ws,
16639
- embeds: resp.embeds,
16640
- chatId,
16641
- chatKeyBytes,
16642
- fallbackMessageId: assistantId,
16643
- ownerId: ownerId2
16644
- });
16645
- if (isNewChat && (resp.generatedTitle || resp.generatedIcon)) {
16646
- const metadataStored = ws.waitForMessage(
16647
- "encrypted_metadata_stored",
16648
- (payload) => payload.chat_id === chatId,
16649
- 2e4
16650
- );
16651
- await ws.sendAsync("encrypted_chat_metadata", {
16652
- chat_id: chatId,
16653
- ...teamId ? { team_id: teamId } : {},
16654
- encrypted_chat_key: encryptedChatKey,
16655
- ...resp.generatedTitle ? { encrypted_title: await encryptWithAesGcmCombined(resp.generatedTitle, chatKeyBytes) } : {},
16656
- ...category ? { encrypted_chat_category: await encryptWithAesGcmCombined(category, chatKeyBytes) } : {},
16657
- ...resp.generatedIcon ? { encrypted_icon: await encryptWithAesGcmCombined(resp.generatedIcon, chatKeyBytes) } : {},
16658
- versions: { messages_v: terminalExpectedMessagesV + 1 }
16708
+ if (!terminalPersisted) {
16709
+ throw new Error("Encrypted completion recovery did not persist terminal state.");
16710
+ }
16711
+ assistantMessageId = assistantId;
16712
+ await this.persistStreamedEmbeds({
16713
+ ws,
16714
+ embeds: resp.embeds,
16715
+ chatId,
16716
+ chatKeyBytes,
16717
+ fallbackMessageId: assistantId,
16718
+ ownerId: ownerId2
16659
16719
  });
16660
- await metadataStored;
16661
- }
16662
- await this.persistPostProcessingMetadata({
16663
- ws,
16664
- chatId,
16665
- teamId,
16666
- chatKeyBytes,
16667
- followUpSuggestions: resp.followUpSuggestions,
16668
- newChatSuggestions: resp.newChatSuggestions,
16669
- chatSummary: resp.chatSummary,
16670
- chatTags: resp.chatTags,
16671
- updatedChatTitle: resp.updatedChatTitle,
16672
- sourceTitleVersion: resp.sourceTitleVersion,
16673
- sourceMetadataVersion: resp.sourceMetadataVersion,
16674
- encryptedChatKey
16675
- });
16676
- await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16677
- if (taskUpdateJobsEnabled) {
16678
- const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
16720
+ if (isNewChat && (resp.generatedTitle || resp.generatedIcon)) {
16721
+ const metadataStored = ws.waitForMessage(
16722
+ "encrypted_metadata_stored",
16723
+ (payload) => payload.chat_id === chatId,
16724
+ 2e4
16725
+ );
16726
+ await ws.sendAsync("encrypted_chat_metadata", {
16727
+ chat_id: chatId,
16728
+ ...teamId ? { team_id: teamId } : {},
16729
+ encrypted_chat_key: encryptedChatKey,
16730
+ ...resp.generatedTitle ? { encrypted_title: await encryptWithAesGcmCombined(resp.generatedTitle, chatKeyBytes) } : {},
16731
+ ...category ? { encrypted_chat_category: await encryptWithAesGcmCombined(category, chatKeyBytes) } : {},
16732
+ ...resp.generatedIcon ? { encrypted_icon: await encryptWithAesGcmCombined(resp.generatedIcon, chatKeyBytes) } : {},
16733
+ versions: { messages_v: terminalExpectedMessagesV + 1 }
16734
+ });
16735
+ await metadataStored;
16736
+ }
16737
+ await this.persistPostProcessingMetadata({
16679
16738
  ws,
16680
- jobs: pendingTaskUpdateJobs,
16681
- events: taskEvents,
16739
+ chatId,
16682
16740
  teamId,
16683
- activeChatId: chatId,
16684
- activeChatKeyBytes: chatKeyBytes,
16685
- fallbackUserMessageId: messageId,
16686
- requireActiveTurnEvent: true
16741
+ chatKeyBytes,
16742
+ followUpSuggestions: resp.followUpSuggestions,
16743
+ newChatSuggestions: resp.newChatSuggestions,
16744
+ chatSummary: resp.chatSummary,
16745
+ chatTags: resp.chatTags,
16746
+ updatedChatTitle: resp.updatedChatTitle,
16747
+ sourceTitleVersion: resp.sourceTitleVersion,
16748
+ sourceMetadataVersion: resp.sourceMetadataVersion,
16749
+ encryptedChatKey
16687
16750
  });
16688
- pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
16689
- }
16690
- await persistTaskEventSystemMessages(taskEvents);
16691
- await recoverCurrentTurnOutputs();
16692
- clearSyncCache(teamId);
16693
- }
16694
- } finally {
16695
- try {
16696
- await projectFocusCountdown?.flush();
16697
- await focusPersistence;
16698
- if (projectAuthoringAvailable && assistant && chatContextEvents) {
16699
- try {
16700
- const history = boundedAuthoringHistory([
16701
- ...messageHistoryForRequest ?? [],
16702
- { role: "user", content: finalMessage },
16703
- { role: "assistant", content: assistant }
16704
- ]);
16705
- const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
16706
- for (const event of recommendations) chatContextEvents.persist(event);
16707
- } catch {
16751
+ await persistCompressionCheckpoints(ws, chatKeyBytes, resp.compressionCheckpoints);
16752
+ if (taskUpdateJobsEnabled) {
16753
+ const persistedTaskJobIds = await this.persistPendingTaskUpdateJobs({
16754
+ ws,
16755
+ jobs: pendingTaskUpdateJobs,
16756
+ events: taskEvents,
16757
+ teamId,
16758
+ activeChatId: chatId,
16759
+ activeChatKeyBytes: chatKeyBytes,
16760
+ fallbackUserMessageId: messageId,
16761
+ requireActiveTurnEvent: true
16762
+ });
16763
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
16708
16764
  }
16765
+ await persistTaskEventSystemMessages(taskEvents);
16766
+ await recoverCurrentTurnOutputs();
16767
+ clearSyncCache(teamId);
16709
16768
  }
16710
- await chatContextEvents?.flush();
16711
16769
  } finally {
16712
- projectFocusCountdown?.();
16713
- chatContextEvents?.stop();
16714
- ws.close();
16770
+ try {
16771
+ await projectFocusCountdown?.flush();
16772
+ await focusPersistence;
16773
+ if (projectAuthoringAvailable && assistant && chatContextEvents) {
16774
+ try {
16775
+ const history = boundedAuthoringHistory([
16776
+ ...messageHistoryForRequest ?? [],
16777
+ { role: "user", content: finalMessage },
16778
+ { role: "assistant", content: assistant }
16779
+ ]);
16780
+ const recommendations = await assessCliProjectAuthoring(this, projectAuthoringAvailable, history);
16781
+ for (const event of recommendations) chatContextEvents.persist(event);
16782
+ } catch {
16783
+ }
16784
+ }
16785
+ await chatContextEvents?.flush();
16786
+ } finally {
16787
+ projectFocusCountdown?.();
16788
+ chatContextEvents?.stop();
16789
+ ws.close();
16790
+ }
16715
16791
  }
16792
+ if (focusPersistenceError) throw focusPersistenceError;
16716
16793
  }
16717
- if (focusPersistenceError) throw focusPersistenceError;
16794
+ const mateName = category ? MATE_NAMES[category] ?? null : null;
16795
+ return {
16796
+ status: "completed",
16797
+ chatId,
16798
+ messageId: assistantMessageId,
16799
+ userMessageId: messageId,
16800
+ assistant,
16801
+ category,
16802
+ modelName,
16803
+ mateName,
16804
+ followUpSuggestions,
16805
+ taskProposals,
16806
+ taskUpdateProposals,
16807
+ taskEvents,
16808
+ pendingTaskUpdateJobs,
16809
+ subChatEvents,
16810
+ appSettingsMemoryRequests,
16811
+ tokenUsage,
16812
+ promptBudget
16813
+ };
16814
+ } finally {
16815
+ earlyContextAbort.abort();
16816
+ ws.close();
16817
+ if (earlyContext) await earlyContext;
16718
16818
  }
16719
- const mateName = category ? MATE_NAMES[category] ?? null : null;
16720
- return {
16721
- status: "completed",
16722
- chatId,
16723
- messageId: assistantMessageId,
16724
- userMessageId: messageId,
16725
- assistant,
16726
- category,
16727
- modelName,
16728
- mateName,
16729
- followUpSuggestions,
16730
- taskProposals,
16731
- taskUpdateProposals,
16732
- taskEvents,
16733
- pendingTaskUpdateJobs,
16734
- subChatEvents,
16735
- appSettingsMemoryRequests,
16736
- tokenUsage,
16737
- promptBudget
16738
- };
16739
16819
  }
16740
16820
  async persistEncryptedSystemMessage(ws, systemMessage, targetChatId) {
16741
16821
  await ws.sendAsync("chat_system_message_added", {
@@ -104687,6 +104767,9 @@ async function runTui(client2, terminal = new TuiTerminal(), startupServices) {
104687
104767
  let lastFrameOwner = null;
104688
104768
  let closed = false;
104689
104769
  let stopActivity;
104770
+ let workspaceReady = false;
104771
+ let activityOwner = null;
104772
+ let activityEpoch = 0;
104690
104773
  let activityTimer;
104691
104774
  let refreshingActivity = false;
104692
104775
  let identityScope = "";
@@ -104745,6 +104828,35 @@ async function runTui(client2, terminal = new TuiTerminal(), startupServices) {
104745
104828
  hydrateExamples(state);
104746
104829
  }
104747
104830
  };
104831
+ const syncActivityObserver = () => {
104832
+ if (!workspaceReady || closed) return;
104833
+ const session = state.signedIn && client2.hasSession() && typeof client2.getSession === "function" ? client2.getSession() : null;
104834
+ const owner2 = session ? JSON.stringify([
104835
+ session.apiUrl,
104836
+ session.hashedEmail,
104837
+ session.sessionId,
104838
+ session.createdAt,
104839
+ session.masterKeyExportedB64
104840
+ ]) : null;
104841
+ if (owner2 === activityOwner) return;
104842
+ activityOwner = owner2;
104843
+ const epoch = ++activityEpoch;
104844
+ stopActivity?.();
104845
+ stopActivity = void 0;
104846
+ if (!owner2 || typeof client2.observeChatActivity !== "function") return;
104847
+ void client2.observeChatActivity(() => {
104848
+ if (closed || activityEpoch !== epoch || activityOwner !== owner2) return;
104849
+ teamChatRefresh?.request();
104850
+ clearTimeout(activityTimer);
104851
+ activityTimer = setTimeout(() => {
104852
+ void refreshActivity();
104853
+ }, 300);
104854
+ }).then((stop) => {
104855
+ if (closed || activityEpoch !== epoch || activityOwner !== owner2) stop();
104856
+ else stopActivity = stop;
104857
+ }).catch(() => {
104858
+ });
104859
+ };
104748
104860
  const render = () => {
104749
104861
  if (closed) return;
104750
104862
  syncSessionView();
@@ -104784,6 +104896,8 @@ async function runTui(client2, terminal = new TuiTerminal(), startupServices) {
104784
104896
  }
104785
104897
  workspaceScope = nextScope;
104786
104898
  state.activeTeamId = teamId;
104899
+ syncActivityObserver();
104900
+ if (workspaceReady && state.signedIn && typeof client2.warmChatMetadata === "function") client2.warmChatMetadata();
104787
104901
  if (teamId && state.signedIn && nextScope) {
104788
104902
  if (nextScope !== identityScope) {
104789
104903
  identityScope = nextScope;
@@ -104825,6 +104939,7 @@ async function runTui(client2, terminal = new TuiTerminal(), startupServices) {
104825
104939
  const stopHomeSync = startHomeSync(state, client2, render, () => closed);
104826
104940
  const finish = (result2) => {
104827
104941
  closed = true;
104942
+ activityEpoch++;
104828
104943
  state.homeAbortController?.abort();
104829
104944
  Object.values(state.chatContextAuthoringControls).forEach((control) => control.stop());
104830
104945
  stopHomeSync();
@@ -104852,22 +104967,11 @@ async function runTui(client2, terminal = new TuiTerminal(), startupServices) {
104852
104967
  services: startupServices,
104853
104968
  updated: () => finish({ action: "restart" }),
104854
104969
  ready: () => {
104970
+ workspaceReady = true;
104971
+ syncActivityObserver();
104855
104972
  void loadHomeData(state, client2, render);
104856
104973
  void refreshActivity();
104857
104974
  warmTuiWorkspaceLists(client2);
104858
- if (state.signedIn && typeof client2.observeChatActivity === "function") {
104859
- void client2.observeChatActivity(() => {
104860
- teamChatRefresh?.request();
104861
- clearTimeout(activityTimer);
104862
- activityTimer = setTimeout(() => {
104863
- void refreshActivity();
104864
- }, 300);
104865
- }).then((stop) => {
104866
- if (closed) stop();
104867
- else stopActivity = stop;
104868
- }).catch(() => {
104869
- });
104870
- }
104871
104975
  }
104872
104976
  });
104873
104977
  terminal.enter();