codex-to-im 1.0.43 → 1.0.45

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.
package/dist/daemon.mjs CHANGED
@@ -1408,8 +1408,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1408
1408
  botIds = /* @__PURE__ */ new Set();
1409
1409
  /** Track last incoming message ID per chat for typing indicator. */
1410
1410
  lastIncomingMessageId = /* @__PURE__ */ new Map();
1411
- /** Track active typing reaction IDs per chat for cleanup. */
1411
+ /** Track active typing reaction IDs per stream key for cleanup. */
1412
1412
  typingReactions = /* @__PURE__ */ new Map();
1413
+ /** Track in-flight typing reaction creates so repeated status updates stay idempotent. */
1414
+ typingReactionCreatePromises = /* @__PURE__ */ new Map();
1415
+ /** Track streams that ended before the async reaction create finished. */
1416
+ typingReactionCleanupRequested = /* @__PURE__ */ new Set();
1413
1417
  /** Active streaming card state per stream key. */
1414
1418
  activeCards = /* @__PURE__ */ new Map();
1415
1419
  /** In-flight card creation promises per stream key — prevents duplicate creation. */
@@ -1515,6 +1519,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1515
1519
  this.seenMessageIds.clear();
1516
1520
  this.lastIncomingMessageId.clear();
1517
1521
  this.typingReactions.clear();
1522
+ this.typingReactionCreatePromises.clear();
1523
+ this.typingReactionCleanupRequested.clear();
1518
1524
  console.log("[feishu-adapter] Stopped");
1519
1525
  }
1520
1526
  isRunning() {
@@ -1531,25 +1537,38 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1531
1537
  */
1532
1538
  onMessageStart(chatId, streamKey) {
1533
1539
  const messageId = this.lastIncomingMessageId.get(chatId);
1540
+ const reactionKey = this.resolveStreamKey(chatId, streamKey);
1534
1541
  if (messageId && this.isStreamingEnabled()) {
1535
1542
  this.createStreamingCard(chatId, messageId, streamKey).catch(() => {
1536
1543
  });
1537
1544
  }
1538
1545
  if (!messageId || !this.restClient) return;
1539
- this.restClient.im.messageReaction.create({
1546
+ if (this.typingReactions.has(reactionKey) || this.typingReactionCreatePromises.has(reactionKey)) {
1547
+ return;
1548
+ }
1549
+ const createPromise = this.restClient.im.messageReaction.create({
1540
1550
  path: { message_id: messageId },
1541
1551
  data: { reaction_type: { emoji_type: TYPING_EMOJI } }
1542
1552
  }).then((res) => {
1543
1553
  const reactionId = res?.data?.reaction_id;
1544
1554
  if (reactionId) {
1545
- this.typingReactions.set(chatId, reactionId);
1555
+ this.typingReactions.set(reactionKey, { messageId, reactionId });
1556
+ if (this.typingReactionCleanupRequested.delete(reactionKey)) {
1557
+ this.removeTypingReaction(reactionKey);
1558
+ }
1546
1559
  }
1547
1560
  }).catch((err) => {
1548
1561
  const code2 = err?.code;
1549
1562
  if (code2 !== 99991400 && code2 !== 99991403) {
1550
1563
  console.warn("[feishu-adapter] Typing indicator failed:", err instanceof Error ? err.message : err);
1551
1564
  }
1565
+ }).finally(() => {
1566
+ this.typingReactionCreatePromises.delete(reactionKey);
1567
+ if (!this.typingReactions.has(reactionKey)) {
1568
+ this.typingReactionCleanupRequested.delete(reactionKey);
1569
+ }
1552
1570
  });
1571
+ this.typingReactionCreatePromises.set(reactionKey, createPromise);
1553
1572
  }
1554
1573
  /**
1555
1574
  * Remove the "Typing" emoji reaction and clean up card state.
@@ -1557,12 +1576,19 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1557
1576
  */
1558
1577
  onMessageEnd(chatId, streamKey) {
1559
1578
  this.cleanupCard(chatId, streamKey);
1560
- const reactionId = this.typingReactions.get(chatId);
1561
- const messageId = this.lastIncomingMessageId.get(chatId);
1562
- if (!reactionId || !messageId || !this.restClient) return;
1563
- this.typingReactions.delete(chatId);
1579
+ const reactionKey = this.resolveStreamKey(chatId, streamKey);
1580
+ if (this.typingReactionCreatePromises.has(reactionKey) && !this.typingReactions.has(reactionKey)) {
1581
+ this.typingReactionCleanupRequested.add(reactionKey);
1582
+ return;
1583
+ }
1584
+ this.removeTypingReaction(reactionKey);
1585
+ }
1586
+ removeTypingReaction(reactionKey) {
1587
+ const reaction = this.typingReactions.get(reactionKey);
1588
+ if (!reaction || !this.restClient) return;
1589
+ this.typingReactions.delete(reactionKey);
1564
1590
  this.restClient.im.messageReaction.delete({
1565
- path: { message_id: messageId, reaction_id: reactionId }
1591
+ path: { message_id: reaction.messageId, reaction_id: reaction.reactionId }
1566
1592
  }).catch(() => {
1567
1593
  });
1568
1594
  }
@@ -10447,6 +10473,18 @@ function readDesktopSessionEventStream(threadId) {
10447
10473
  return readDesktopSessionEventStreamByFilePath(session.filePath);
10448
10474
  }
10449
10475
 
10476
+ // src/lib/bridge/turns/turn-classifier.ts
10477
+ function normalizeThreadId(value) {
10478
+ const normalized = value?.trim();
10479
+ return normalized || void 0;
10480
+ }
10481
+ function getCodexThreadId(session, binding) {
10482
+ return normalizeThreadId(session?.codex_thread_id) || normalizeThreadId(binding?.sdkSessionId) || normalizeThreadId(session?.sdk_session_id);
10483
+ }
10484
+ function getExplicitDesktopThreadId(session) {
10485
+ return normalizeThreadId(session?.desktop_thread_id) || (session?.thread_origin === "desktop" ? normalizeThreadId(session.sdk_session_id) : void 0);
10486
+ }
10487
+
10450
10488
  // src/session-bindings.ts
10451
10489
  function asChannelProvider(value) {
10452
10490
  return value === "feishu" || value === "weixin" ? value : void 0;
@@ -10492,15 +10530,31 @@ function assertBindingTargetAvailable(store, current, opts) {
10492
10530
  function getSessionMode(store, session) {
10493
10531
  return session.preferred_mode || store.getSetting("bridge_default_mode") || "code";
10494
10532
  }
10533
+ function getBindingResumeThreadId(session) {
10534
+ return getCodexThreadId(session) || "";
10535
+ }
10536
+ function markSessionAsDesktopBacked(store, sessionId, desktopThreadId) {
10537
+ store.updateSdkSessionId(sessionId, desktopThreadId);
10538
+ store.updateSession(sessionId, {
10539
+ sdk_session_id: desktopThreadId,
10540
+ codex_thread_id: desktopThreadId,
10541
+ desktop_thread_id: desktopThreadId,
10542
+ thread_origin: "desktop"
10543
+ });
10544
+ }
10495
10545
  function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10496
10546
  const session = store.getSession(sessionId);
10497
10547
  if (!session) return null;
10498
10548
  assertBindingTargetAvailable(
10499
10549
  store,
10500
10550
  { channelType, chatId },
10501
- { sessionId: session.id, sdkSessionId: session.sdk_session_id || void 0 }
10551
+ {
10552
+ sessionId: session.id,
10553
+ sdkSessionId: getCodexThreadId(session) || getExplicitDesktopThreadId(session)
10554
+ }
10502
10555
  );
10503
10556
  const meta = resolveChannelMeta(channelType);
10557
+ const sdkSessionId = getBindingResumeThreadId(session);
10504
10558
  return store.upsertChannelBinding({
10505
10559
  channelType,
10506
10560
  channelProvider: meta.provider,
@@ -10509,7 +10563,7 @@ function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10509
10563
  chatUserId: chatMeta?.chatUserId,
10510
10564
  chatDisplayName: chatMeta?.chatDisplayName,
10511
10565
  codepilotSessionId: session.id,
10512
- sdkSessionId: session.sdk_session_id || "",
10566
+ sdkSessionId,
10513
10567
  workingDirectory: session.working_directory,
10514
10568
  model: session.model,
10515
10569
  mode: getSessionMode(store, session)
@@ -10524,6 +10578,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10524
10578
  const meta = resolveChannelMeta(channelType);
10525
10579
  const existing = store.findSessionBySdkSessionId(sdkSessionId);
10526
10580
  if (existing) {
10581
+ markSessionAsDesktopBacked(store, existing.id, sdkSessionId);
10527
10582
  return store.upsertChannelBinding({
10528
10583
  channelType,
10529
10584
  channelProvider: meta.provider,
@@ -10548,7 +10603,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10548
10603
  workingDirectory,
10549
10604
  "code"
10550
10605
  );
10551
- store.updateSdkSessionId(session.id, sdkSessionId);
10606
+ markSessionAsDesktopBacked(store, session.id, sdkSessionId);
10552
10607
  return store.upsertChannelBinding({
10553
10608
  channelType,
10554
10609
  channelProvider: meta.provider,
@@ -10921,15 +10976,17 @@ async function deliver(adapter, message, opts) {
10921
10976
  } catch {
10922
10977
  }
10923
10978
  }
10924
- try {
10925
- store.insertAuditLog({
10926
- channelType: adapter.channelType,
10927
- chatId: message.address.chatId,
10928
- direction: "outbound",
10929
- messageId: lastMessageId || "",
10930
- summary: message.text.slice(0, 200)
10931
- });
10932
- } catch {
10979
+ if (opts?.audit !== false) {
10980
+ try {
10981
+ store.insertAuditLog({
10982
+ channelType: adapter.channelType,
10983
+ chatId: message.address.chatId,
10984
+ direction: "outbound",
10985
+ messageId: lastMessageId || "",
10986
+ summary: message.text.slice(0, 200)
10987
+ });
10988
+ } catch {
10989
+ }
10933
10990
  }
10934
10991
  return { ok: true, messageId: lastMessageId };
10935
10992
  }
@@ -11653,6 +11710,8 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
11653
11710
  streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
11654
11711
  startedAt: safeTimestamp,
11655
11712
  lastActivityAt: safeTimestamp,
11713
+ lastContentResponseAt: null,
11714
+ lastResponseAt: null,
11656
11715
  lastStatusText: null,
11657
11716
  lastStatusAt: 0,
11658
11717
  statusNote: null,
@@ -11699,8 +11758,14 @@ function ensureMirrorTurnState(subscription, record) {
11699
11758
  }
11700
11759
  return subscription.pendingTurn;
11701
11760
  }
11702
- function markMirrorResponse(turnState, timestamp) {
11703
- turnState.lastResponseAt = timestamp || nowIso2();
11761
+ function markMirrorActivity(turnState, timestamp) {
11762
+ turnState.lastActivityAt = timestamp || nowIso2();
11763
+ }
11764
+ function markMirrorContentResponse(turnState, timestamp) {
11765
+ const responseAt = timestamp || nowIso2();
11766
+ markMirrorActivity(turnState, responseAt);
11767
+ turnState.lastContentResponseAt = responseAt;
11768
+ turnState.lastResponseAt = responseAt;
11704
11769
  }
11705
11770
  function finalizeMirrorTurn(subscription, signature, timestamp, status, preferredText) {
11706
11771
  const pendingTurn = subscription.pendingTurn;
@@ -11773,7 +11838,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11773
11838
  if (text2) {
11774
11839
  pendingTurn.lastAssistantText = text2;
11775
11840
  appendMirrorStreamText(pendingTurn, text2);
11776
- markMirrorResponse(pendingTurn, record.timestamp);
11841
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11777
11842
  hooks.onStreamText?.(subscription, pendingTurn);
11778
11843
  }
11779
11844
  } else if (record.role === "commentary") {
@@ -11781,7 +11846,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11781
11846
  if (text2) {
11782
11847
  pendingTurn.lastCommentaryText = text2;
11783
11848
  appendMirrorStreamText(pendingTurn, text2);
11784
- markMirrorResponse(pendingTurn, record.timestamp);
11849
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11785
11850
  hooks.onStreamText?.(subscription, pendingTurn);
11786
11851
  }
11787
11852
  }
@@ -11792,14 +11857,14 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11792
11857
  const text2 = record.content.trim();
11793
11858
  if (!text2) continue;
11794
11859
  pendingTurn.statusNote = text2;
11795
- markMirrorResponse(pendingTurn, record.timestamp);
11860
+ markMirrorActivity(pendingTurn, record.timestamp);
11796
11861
  hooks.onStatusProgress?.(subscription, pendingTurn);
11797
11862
  continue;
11798
11863
  }
11799
11864
  if (record.type === "plan_update") {
11800
11865
  const pendingTurn = ensureMirrorTurnState(subscription, record);
11801
11866
  pendingTurn.taskItems = record.tasks || [];
11802
- markMirrorResponse(pendingTurn, record.timestamp);
11867
+ markMirrorActivity(pendingTurn, record.timestamp);
11803
11868
  hooks.onTaskProgress?.(subscription, pendingTurn);
11804
11869
  continue;
11805
11870
  }
@@ -11812,7 +11877,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11812
11877
  name: toolName,
11813
11878
  status: "running"
11814
11879
  });
11815
- markMirrorResponse(pendingTurn, record.timestamp);
11880
+ markMirrorActivity(pendingTurn, record.timestamp);
11816
11881
  hooks.onToolProgress?.(subscription, pendingTurn);
11817
11882
  continue;
11818
11883
  }
@@ -11825,7 +11890,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11825
11890
  name: existing?.name || record.toolName || "tool",
11826
11891
  status: record.isError ? "error" : "complete"
11827
11892
  });
11828
- markMirrorResponse(pendingTurn, record.timestamp);
11893
+ markMirrorActivity(pendingTurn, record.timestamp);
11829
11894
  hooks.onToolProgress?.(subscription, pendingTurn);
11830
11895
  continue;
11831
11896
  }
@@ -11994,9 +12059,6 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
11994
12059
  }
11995
12060
  target.until = nowMs + config2.suppressionWindowMs;
11996
12061
  }
11997
- function isMirrorSuppressed(store, sessionId, nowMs = Date.now()) {
11998
- return getMirrorSuppressionStates(store, sessionId, nowMs).length > 0;
11999
- }
12000
12062
  function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now()) {
12001
12063
  const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
12002
12064
  const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
@@ -12633,7 +12695,7 @@ function supportsOutboundArtifacts(provider) {
12633
12695
  }
12634
12696
 
12635
12697
  // src/lib/bridge/feedback-delivery.ts
12636
- async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId) {
12698
+ async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId, options) {
12637
12699
  if (!responseText.trim()) return { ok: true };
12638
12700
  const parseMode = getFeedbackParseMode(adapter.channelType);
12639
12701
  const renderedText = renderFeedbackText(responseText, parseMode);
@@ -12643,14 +12705,14 @@ async function deliverTextResponse(adapter, address, responseText, sessionId, re
12643
12705
  text: responseText,
12644
12706
  parseMode: "Markdown",
12645
12707
  replyToMessageId
12646
- }, { sessionId });
12708
+ }, { sessionId, audit: options?.audit });
12647
12709
  }
12648
12710
  return deliver(adapter, {
12649
12711
  address,
12650
12712
  text: parseMode === "Markdown" ? responseText : renderedText,
12651
12713
  parseMode,
12652
12714
  replyToMessageId
12653
- }, { sessionId });
12715
+ }, { sessionId, audit: options?.audit });
12654
12716
  }
12655
12717
  async function deliverBridgeNotice(adapter, address, text2, options) {
12656
12718
  return deliverTextResponse(
@@ -12658,7 +12720,8 @@ async function deliverBridgeNotice(adapter, address, text2, options) {
12658
12720
  address,
12659
12721
  text2,
12660
12722
  options?.sessionId,
12661
- options?.replyToMessageId
12723
+ options?.replyToMessageId,
12724
+ { audit: options?.audit }
12662
12725
  );
12663
12726
  }
12664
12727
  async function deliverResponse(adapter, address, responseText, sessionId, replyToMessageId, attachments = []) {
@@ -12774,6 +12837,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
12774
12837
  }
12775
12838
  let response = "";
12776
12839
  let responseParseMode = getFeedbackParseMode(adapter.channelType);
12840
+ let auditResponse = true;
12777
12841
  const currentBinding = store.getChannelBinding(msg.address.channelType, msg.address.chatId);
12778
12842
  switch (command) {
12779
12843
  case "/start":
@@ -13106,6 +13170,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13106
13170
  break;
13107
13171
  }
13108
13172
  if (!args) {
13173
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13109
13174
  const currentModel = resolveDisplayedModel(
13110
13175
  binding,
13111
13176
  session,
@@ -13117,14 +13182,14 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13117
13182
  [["\u6A21\u578B", formatDisplayedModel(currentModel)]],
13118
13183
  [
13119
13184
  getAvailableModelChoicesText(),
13120
- binding.sdkSessionId ? "\u5F53\u524D\u662F\u5171\u4EAB\u684C\u9762\u7EBF\u7A0B\uFF0C\u53EA\u652F\u6301\u67E5\u770B\u6A21\u578B\uFF1B\u5982\u9700\u5207\u6362\uFF0C\u8BF7\u5148\u7528 `/new` \u65B0\u5EFA\u4E00\u4E2A IM \u4F1A\u8BDD\u7EBF\u7A0B\u3002" : "\u53D1\u9001 `/model gpt-5.4` \u53EF\u5207\u6362\uFF1B\u53D1\u9001 `/model default` \u53EF\u56DE\u9000\u5230\u9ED8\u8BA4\u6A21\u578B\u3002",
13185
+ desktopThreadId ? "\u5F53\u524D\u662F\u5171\u4EAB\u684C\u9762\u7EBF\u7A0B\uFF0C\u53EA\u652F\u6301\u67E5\u770B\u6A21\u578B\uFF1B\u5982\u9700\u5207\u6362\uFF0C\u8BF7\u5148\u7528 `/new` \u65B0\u5EFA\u4E00\u4E2A IM \u4F1A\u8BDD\u7EBF\u7A0B\u3002" : "\u53D1\u9001 `/model gpt-5.4` \u53EF\u5207\u6362\uFF1B\u53D1\u9001 `/model default` \u53EF\u56DE\u9000\u5230\u9ED8\u8BA4\u6A21\u578B\u3002",
13121
13186
  "\u6A21\u578B\u5207\u6362\u53EA\u5F71\u54CD\u540E\u7EED\u4ECE IM \u53D1\u8D77\u7684 Codex CLI \u8BF7\u6C42\u3002"
13122
13187
  ],
13123
13188
  responseParseMode === "Markdown"
13124
13189
  );
13125
13190
  break;
13126
13191
  }
13127
- if (binding.sdkSessionId) {
13192
+ if (getExplicitDesktopThreadId(session)) {
13128
13193
  response = "\u5F53\u524D\u662F\u5171\u4EAB\u684C\u9762\u7EBF\u7A0B\uFF0C\u4E0D\u652F\u6301\u76F4\u63A5\u5207\u6362\u6A21\u578B\u3002\u8BF7\u5148\u7528 `/new` \u65B0\u5EFA\u4E00\u4E2A\u7EBF\u7A0B\uFF0C\u518D\u6267\u884C `/model ...`\u3002";
13129
13194
  break;
13130
13195
  }
@@ -13175,9 +13240,32 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13175
13240
  break;
13176
13241
  }
13177
13242
  case "/status": {
13178
- const binding = resolve(msg.address);
13243
+ auditResponse = false;
13244
+ const binding = currentBinding;
13245
+ if (!binding) {
13246
+ response = buildCommandFields(
13247
+ "\u5F53\u524D\u4F1A\u8BDD",
13248
+ [],
13249
+ ["\u5F53\u524D\u804A\u5929\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u4F1A\u8BDD\u3002\u53EF\u5148\u53D1\u9001 `/t` \u67E5\u770B\u6700\u8FD1\u684C\u9762\u4F1A\u8BDD\uFF0C\u518D\u7528 `/t 1` \u63A5\u7BA1\uFF1B\u6216\u53D1\u9001 `/new proj1` / `/new \u7EDD\u5BF9\u8DEF\u5F84` \u521B\u5EFA\u9879\u76EE\u4F1A\u8BDD\u3002"],
13250
+ responseParseMode === "Markdown"
13251
+ );
13252
+ break;
13253
+ }
13179
13254
  const session = store.getSession(binding.codepilotSessionId);
13180
- const threadTitle = getDesktopThreadTitle(binding.sdkSessionId);
13255
+ if (!session) {
13256
+ response = buildCommandFields(
13257
+ "\u5F53\u524D\u4F1A\u8BDD",
13258
+ [
13259
+ ["Session", binding.codepilotSessionId],
13260
+ ["\u76EE\u5F55", formatCommandPath(binding.workingDirectory)]
13261
+ ],
13262
+ ["\u5F53\u524D\u804A\u5929\u7ED1\u5B9A\u7684\u4F1A\u8BDD\u5DF2\u7ECF\u4E0D\u5B58\u5728\u3002\u53EF\u7528 `/t` \u63A5\u7BA1\u684C\u9762\u4F1A\u8BDD\uFF0C\u6216\u7528 `/new proj1` / `/new \u7EDD\u5BF9\u8DEF\u5F84` \u521B\u5EFA\u65B0\u4F1A\u8BDD\u3002"],
13263
+ responseParseMode === "Markdown"
13264
+ );
13265
+ break;
13266
+ }
13267
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13268
+ const threadTitle = getDesktopThreadTitle(desktopThreadId);
13181
13269
  const sandboxMode = resolveEffectiveSandboxMode();
13182
13270
  const reasoningEffort = resolveEffectiveReasoningEffort(session);
13183
13271
  const currentModel = resolveDisplayedModel(
@@ -13201,27 +13289,32 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13201
13289
  ["\u601D\u8003\u7EA7\u522B", formatReasoningEffort(reasoningEffort)]
13202
13290
  ],
13203
13291
  [
13204
- binding.sdkSessionId ? "\u5F53\u524D\u804A\u5929\u5DF2\u7ED1\u5B9A\u5230\u4E00\u6761\u5171\u4EAB\u4F1A\u8BDD\uFF0C\u76F4\u63A5\u53D1\u9001\u6D88\u606F\u5373\u53EF\u7EE7\u7EED\u3002" : session?.session_type === "draft" ? "\u5F53\u524D\u804A\u5929\u6B63\u5728\u4F7F\u7528\u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B\uFF08\u7B49\u540C `/t 0`\uFF09\u3002\u53EF\u76F4\u63A5\u53D1\u9001\u6D88\u606F\uFF0C\u6216\u7528 `/t` / `/new proj1` / `/new \u7EDD\u5BF9\u8DEF\u5F84` \u5207\u6362\u5230\u6B63\u5F0F\u4F1A\u8BDD\u3002" : "\u5F53\u524D\u804A\u5929\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u684C\u9762\u4F1A\u8BDD\u3002\u53EF\u5148\u53D1\u9001 `/t`\uFF0C\u518D\u7528 `/t 1` \u63A5\u7BA1\u3002"
13292
+ desktopThreadId ? "\u5F53\u524D\u804A\u5929\u5DF2\u7ED1\u5B9A\u5230\u4E00\u6761\u5171\u4EAB\u4F1A\u8BDD\uFF0C\u76F4\u63A5\u53D1\u9001\u6D88\u606F\u5373\u53EF\u7EE7\u7EED\u3002" : session?.session_type === "draft" ? "\u5F53\u524D\u804A\u5929\u6B63\u5728\u4F7F\u7528\u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B\uFF08\u7B49\u540C `/t 0`\uFF09\u3002\u53EF\u76F4\u63A5\u53D1\u9001\u6D88\u606F\uFF0C\u6216\u7528 `/t` / `/new proj1` / `/new \u7EDD\u5BF9\u8DEF\u5F84` \u5207\u6362\u5230\u6B63\u5F0F\u4F1A\u8BDD\u3002" : "\u5F53\u524D\u804A\u5929\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u684C\u9762\u4F1A\u8BDD\u3002\u53EF\u5148\u53D1\u9001 `/t`\uFF0C\u518D\u7528 `/t 1` \u63A5\u7BA1\u3002"
13205
13293
  ],
13206
13294
  responseParseMode === "Markdown"
13207
13295
  );
13208
13296
  break;
13209
13297
  }
13210
13298
  case "/health": {
13299
+ auditResponse = false;
13211
13300
  if (args === "all") {
13212
13301
  const diagnoses = await deps.diagnoseAllActiveSessions();
13213
13302
  response = diagnoses.length > 0 ? buildHealthListResponse(diagnoses, responseParseMode === "Markdown") : "\u5F53\u524D\u6CA1\u6709\u68C0\u6D4B\u5230\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002";
13214
13303
  break;
13215
13304
  }
13216
- const binding = currentBinding || resolve(msg.address);
13217
- const targetSessionId = args.trim() || binding.codepilotSessionId;
13305
+ const explicitTargetSessionId = args.trim();
13306
+ const targetSessionId = explicitTargetSessionId || currentBinding?.codepilotSessionId;
13307
+ if (!targetSessionId) {
13308
+ response = "\u5F53\u524D\u804A\u5929\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u4F1A\u8BDD\u3002\u5148\u53D1\u9001\u6D88\u606F\u521B\u5EFA\u4F1A\u8BDD\uFF0C\u6216\u5148\u7528 `/t 1` \u63A5\u7BA1\u684C\u9762\u4F1A\u8BDD\u3002";
13309
+ break;
13310
+ }
13218
13311
  const diagnosis = await deps.diagnoseSessionHealth(targetSessionId);
13219
13312
  if (!diagnosis) {
13220
13313
  response = `\u6CA1\u6709\u627E\u5230\u4F1A\u8BDD ${targetSessionId}\u3002`;
13221
13314
  break;
13222
13315
  }
13223
13316
  response = buildHealthCommandResponse(
13224
- "\u5F53\u524D\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5",
13317
+ explicitTargetSessionId ? "\u6307\u5B9A\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5" : "\u5F53\u524D\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5",
13225
13318
  diagnosis,
13226
13319
  responseParseMode === "Markdown"
13227
13320
  );
@@ -13233,15 +13326,16 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13233
13326
  break;
13234
13327
  }
13235
13328
  const limit = getHistoryMessageLimit();
13236
- const desktopMessages = currentBinding.sdkSessionId ? readDesktopSessionMessages(currentBinding.sdkSessionId, limit) : [];
13329
+ const session = store.getSession(currentBinding.codepilotSessionId);
13330
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13331
+ const desktopMessages = desktopThreadId ? readDesktopSessionMessages(desktopThreadId, limit) : [];
13237
13332
  const { messages: storedMessages } = store.getMessages(currentBinding.codepilotSessionId, { limit });
13238
13333
  const messages = desktopMessages.length > 0 ? desktopMessages : storedMessages;
13239
13334
  if (messages.length === 0) {
13240
13335
  response = "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u5386\u53F2\u6D88\u606F\u3002";
13241
13336
  break;
13242
13337
  }
13243
- const threadTitle = getDesktopThreadTitle(currentBinding.sdkSessionId);
13244
- const session = store.getSession(currentBinding.codepilotSessionId);
13338
+ const threadTitle = getDesktopThreadTitle(desktopThreadId);
13245
13339
  const header = buildCommandFields(
13246
13340
  "\u6700\u8FD1\u5BF9\u8BDD\uFF08raw\uFF09",
13247
13341
  [
@@ -13373,7 +13467,8 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13373
13467
  }
13374
13468
  if (response) {
13375
13469
  await deliverBridgeNotice(adapter, msg.address, response, {
13376
- replyToMessageId: msg.messageId
13470
+ replyToMessageId: msg.messageId,
13471
+ audit: auditResponse
13377
13472
  });
13378
13473
  }
13379
13474
  }
@@ -13385,6 +13480,39 @@ import path13 from "node:path";
13385
13480
  import fs8 from "fs";
13386
13481
  import path12 from "path";
13387
13482
  import crypto5 from "crypto";
13483
+
13484
+ // src/lib/bridge/turns/final-response-artifacts.ts
13485
+ function attachmentKey(attachment) {
13486
+ return [
13487
+ attachment.kind,
13488
+ attachment.path,
13489
+ attachment.caption || "",
13490
+ attachment.name || ""
13491
+ ].join("\0");
13492
+ }
13493
+ function dedupeOutboundAttachments(attachments) {
13494
+ const seen = /* @__PURE__ */ new Set();
13495
+ const deduped = [];
13496
+ for (const attachment of attachments) {
13497
+ const key = attachmentKey(attachment);
13498
+ if (seen.has(key)) continue;
13499
+ seen.add(key);
13500
+ deduped.push(attachment);
13501
+ }
13502
+ return deduped;
13503
+ }
13504
+ function collectFinalResponseArtifacts(text2, attachments = []) {
13505
+ const parsed = parseOutboundArtifacts(text2 || "");
13506
+ return {
13507
+ text: parsed.cleanText,
13508
+ attachments: dedupeOutboundAttachments([
13509
+ ...attachments,
13510
+ ...parsed.attachments
13511
+ ])
13512
+ };
13513
+ }
13514
+
13515
+ // src/lib/bridge/conversation-engine.ts
13388
13516
  init_runtime_options();
13389
13517
 
13390
13518
  // src/lib/bridge/sse-stream-decoder.ts
@@ -13763,8 +13891,8 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13763
13891
  if (contentBlocks.length > 0) {
13764
13892
  for (const block2 of contentBlocks) {
13765
13893
  if (block2.type !== "text") continue;
13766
- const parsed = parseOutboundArtifacts(block2.text);
13767
- block2.text = parsed.cleanText;
13894
+ const parsed = collectFinalResponseArtifacts(block2.text);
13895
+ block2.text = parsed.text;
13768
13896
  outboundAttachments.push(...parsed.attachments);
13769
13897
  }
13770
13898
  const hasToolBlocks = contentBlocks.some(
@@ -13778,7 +13906,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13778
13906
  const responseText = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
13779
13907
  return {
13780
13908
  responseText,
13781
- outboundAttachments,
13909
+ outboundAttachments: dedupeOutboundAttachments(outboundAttachments),
13782
13910
  tokenUsage,
13783
13911
  hasError,
13784
13912
  errorMessage,
@@ -13811,6 +13939,42 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13811
13939
  }
13812
13940
  }
13813
13941
 
13942
+ // src/lib/bridge/turns/response-assembler.ts
13943
+ function assembleFinalResponse(source, input) {
13944
+ const parsed = collectFinalResponseArtifacts(input.text, input.attachments);
13945
+ return {
13946
+ text: parsed.text,
13947
+ attachments: parsed.attachments,
13948
+ hasError: input.hasError,
13949
+ errorMessage: input.errorMessage,
13950
+ source
13951
+ };
13952
+ }
13953
+ function assembleSdkFinalResponse(input) {
13954
+ return assembleFinalResponse("sdk_result", input);
13955
+ }
13956
+ function assembleDesktopFinalResponse(input) {
13957
+ return assembleFinalResponse("desktop_task_complete", input);
13958
+ }
13959
+ function hasFinalResponsePayload(response) {
13960
+ return Boolean(response.text || response.attachments.length > 0);
13961
+ }
13962
+ function mergeFinalResponses(primary, fallback) {
13963
+ return {
13964
+ text: primary.text || fallback.text,
13965
+ attachments: dedupeOutboundAttachments([
13966
+ ...fallback.attachments,
13967
+ ...primary.attachments
13968
+ ]),
13969
+ hasError: primary.hasError ?? fallback.hasError,
13970
+ errorMessage: primary.errorMessage || fallback.errorMessage,
13971
+ source: primary.source
13972
+ };
13973
+ }
13974
+ function stripFinalOnlyBlocksForStreaming(text2) {
13975
+ return stripOutboundArtifactBlocksForStreaming(text2);
13976
+ }
13977
+
13814
13978
  // src/lib/bridge/stream-feedback-controller.ts
13815
13979
  function pushStreamFeedbackText(target, text2) {
13816
13980
  if (typeof target.adapter.onStreamText !== "function") return;
@@ -13839,13 +14003,15 @@ function pushStreamFeedbackTasks(target, tasks) {
13839
14003
  }
13840
14004
  }
13841
14005
  function pushStreamFeedbackStatus(target, text2) {
13842
- if (typeof target.adapter.onStreamStatus !== "function") return;
14006
+ if (typeof target.adapter.onStreamStatus !== "function") return false;
13843
14007
  target.ensureStarted?.();
13844
14008
  const rendered = renderFeedbackTextForChannel(target.channelType, text2);
13845
- if (!rendered) return;
14009
+ if (!rendered) return false;
13846
14010
  try {
13847
14011
  target.adapter.onStreamStatus(target.chatId, rendered, target.streamKey);
14012
+ return true;
13848
14013
  } catch {
14014
+ return false;
13849
14015
  }
13850
14016
  }
13851
14017
  async function finalizeStreamFeedback(target, status, text2) {
@@ -13858,6 +14024,118 @@ async function finalizeStreamFeedback(target, status, text2) {
13858
14024
  }
13859
14025
  }
13860
14026
 
14027
+ // src/lib/bridge/turns/delivery-pipeline.ts
14028
+ function normalizeUnknownSendResult(result) {
14029
+ if (result && typeof result === "object" && "ok" in result) {
14030
+ return result;
14031
+ }
14032
+ return { ok: true };
14033
+ }
14034
+ async function deliverFinalResponse(context, response, options = {}) {
14035
+ let lastResult = { ok: true };
14036
+ const deliverResponse2 = context.deliverResponse || deliverResponse;
14037
+ if (!options.skipText && response.text.trim()) {
14038
+ if (context.deliverText) {
14039
+ lastResult = await context.deliverText(response.text);
14040
+ } else {
14041
+ lastResult = normalizeUnknownSendResult(await deliverResponse2(
14042
+ context.adapter,
14043
+ context.address,
14044
+ response.text,
14045
+ context.sessionId,
14046
+ context.replyToMessageId,
14047
+ []
14048
+ ));
14049
+ }
14050
+ if (!lastResult.ok) return lastResult;
14051
+ }
14052
+ if (response.attachments.length > 0) {
14053
+ lastResult = normalizeUnknownSendResult(await deliverResponse2(
14054
+ context.adapter,
14055
+ context.address,
14056
+ "",
14057
+ context.sessionId,
14058
+ context.replyToMessageId,
14059
+ response.attachments
14060
+ ));
14061
+ if (!lastResult.ok) return lastResult;
14062
+ }
14063
+ return lastResult;
14064
+ }
14065
+ async function finalizeStreamingUi(target, status, response) {
14066
+ return finalizeStreamFeedback(target, status, response.text);
14067
+ }
14068
+
14069
+ // src/lib/bridge/turns/stream-state.ts
14070
+ function createStreamState(startedAtMs) {
14071
+ const safeStartedAtMs = Number.isFinite(startedAtMs) ? startedAtMs : Date.now();
14072
+ return {
14073
+ startedAtMs: safeStartedAtMs,
14074
+ lastActivityAtMs: safeStartedAtMs,
14075
+ lastContentResponseAtMs: null,
14076
+ statusNote: null,
14077
+ lastStatusText: null,
14078
+ lastStatusAtMs: 0
14079
+ };
14080
+ }
14081
+ function recordStreamActivity(state, nowMs) {
14082
+ if (!Number.isFinite(nowMs)) return;
14083
+ state.lastActivityAtMs = Math.max(state.lastActivityAtMs, nowMs);
14084
+ }
14085
+ function recordStreamContentResponse(state, nowMs) {
14086
+ if (!Number.isFinite(nowMs)) return;
14087
+ recordStreamActivity(state, nowMs);
14088
+ state.lastContentResponseAtMs = nowMs;
14089
+ }
14090
+ function updateStreamStatusNote(state, note, nowMs) {
14091
+ state.statusNote = (note || "").trim() || null;
14092
+ if (state.statusNote) {
14093
+ recordStreamActivity(state, nowMs);
14094
+ }
14095
+ }
14096
+ function formatRuntimeDuration(ms) {
14097
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
14098
+ const seconds = totalSeconds % 60;
14099
+ const totalMinutes = Math.floor(totalSeconds / 60);
14100
+ const minutes = totalMinutes % 60;
14101
+ const hours = Math.floor(totalMinutes / 60);
14102
+ const parts = [];
14103
+ if (hours > 0) parts.push(`${hours}\u5C0F\u65F6`);
14104
+ if (minutes > 0) parts.push(`${minutes}\u5206`);
14105
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}\u79D2`);
14106
+ return parts.join("");
14107
+ }
14108
+ function formatStreamRuntimeStatus(elapsedMs, lastContentResponseAgeMs, statusNote) {
14109
+ const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
14110
+ if (typeof lastContentResponseAgeMs === "number" && lastContentResponseAgeMs >= 0) {
14111
+ parts.push(`\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA ${formatRuntimeDuration(lastContentResponseAgeMs)}`);
14112
+ }
14113
+ const runtimeText = parts.join("\uFF0C");
14114
+ const note = (statusNote || "").trim();
14115
+ return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
14116
+ ${runtimeText}` : runtimeText;
14117
+ }
14118
+ function getStreamLastContentResponseAgeMs(state, nowMs, options = {}) {
14119
+ const fallbackToStart = options.fallbackToStart !== false;
14120
+ const base = state.lastContentResponseAtMs ?? (fallbackToStart ? state.startedAtMs : null);
14121
+ if (base == null || !Number.isFinite(base) || !Number.isFinite(nowMs)) return null;
14122
+ return Math.max(0, nowMs - base);
14123
+ }
14124
+ function shouldShowStreamLastContentResponseAge(state, nowMs, config2) {
14125
+ if (!Number.isFinite(nowMs)) return false;
14126
+ const elapsedMs = nowMs - state.startedAtMs;
14127
+ if (elapsedMs < Math.max(0, config2.idleStartMs)) return false;
14128
+ const ageMs = getStreamLastContentResponseAgeMs(state, nowMs);
14129
+ return ageMs != null && ageMs >= Math.max(1e3, config2.heartbeatMs);
14130
+ }
14131
+ function buildStreamRuntimeStatus(state, nowMs, options = {}) {
14132
+ return formatStreamRuntimeStatus(
14133
+ Math.max(0, nowMs - state.startedAtMs),
14134
+ options.includeLastContentResponseAge ? getStreamLastContentResponseAgeMs(state, nowMs) : null,
14135
+ state.statusNote
14136
+ );
14137
+ }
14138
+
13861
14139
  // src/lib/bridge/interactive-message-runner.ts
13862
14140
  function generateDraftId() {
13863
14141
  return Math.floor(Math.random() * 2147483646) + 1;
@@ -13901,18 +14179,6 @@ function flushPreview(adapter, state, config2) {
13901
14179
  }).catch(() => {
13902
14180
  });
13903
14181
  }
13904
- function formatRuntimeDuration(ms) {
13905
- const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
13906
- const seconds = totalSeconds % 60;
13907
- const totalMinutes = Math.floor(totalSeconds / 60);
13908
- const minutes = totalMinutes % 60;
13909
- const hours = Math.floor(totalMinutes / 60);
13910
- const parts = [];
13911
- if (hours > 0) parts.push(`${hours}\u5C0F\u65F6`);
13912
- if (minutes > 0) parts.push(`${minutes}\u5206`);
13913
- if (seconds > 0 || parts.length === 0) parts.push(`${seconds}\u79D2`);
13914
- return parts.join("");
13915
- }
13916
14182
  function pathBaseName(value) {
13917
14183
  return value.includes("\\") ? path13.win32.basename(value) : path13.basename(value);
13918
14184
  }
@@ -13934,18 +14200,10 @@ function buildStaleTaskCompletionNotice(address, binding) {
13934
14200
  const taskName = formatTaskDisplayName(binding);
13935
14201
  return `\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u4EFB\u52A1\u5DF2\u7ED3\u675F\uFF0C\u4F46\u5F53\u524D\u804A\u5929\u5DF2\u5207\u6362\u5230\u5176\u4ED6\u4F1A\u8BDD\uFF0C\u56DE\u590D\u5DF2\u8DF3\u8FC7\u3002`;
13936
14202
  }
13937
- function formatInteractiveRuntimeStatus(elapsedMs, lastResponseAgeMs, statusNote) {
13938
- const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
13939
- if (typeof lastResponseAgeMs === "number" && lastResponseAgeMs >= 0) {
13940
- parts.push(`\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA ${formatRuntimeDuration(lastResponseAgeMs)}`);
13941
- }
13942
- const runtimeText = parts.join("\uFF0C");
13943
- const note = (statusNote || "").trim();
13944
- return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
13945
- ${runtimeText}` : runtimeText;
13946
- }
13947
14203
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13948
14204
  const binding = resolve(msg.address);
14205
+ const initialSession = getBridgeContext().store.getSession(binding.codepilotSessionId);
14206
+ const desktopThreadId = getExplicitDesktopThreadId(initialSession);
13949
14207
  const streamKey = buildInteractiveStreamKey(binding.codepilotSessionId, msg.messageId);
13950
14208
  const nowMs = deps.nowMs ?? (() => Date.now());
13951
14209
  const setIntervalFn = deps.setIntervalFn ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
@@ -13961,10 +14219,34 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13961
14219
  1e3,
13962
14220
  deps.streamStatusHeartbeatMs ?? structuredStreamStatusConfig.heartbeatMs
13963
14221
  );
13964
- adapter.onMessageStart?.(msg.address.chatId, streamKey);
14222
+ let messageStartCalled = false;
14223
+ const ensureMessageStarted = () => {
14224
+ if (messageStartCalled) return;
14225
+ adapter.onMessageStart?.(msg.address.chatId, streamKey);
14226
+ messageStartCalled = true;
14227
+ };
14228
+ ensureMessageStarted();
13965
14229
  const taskAbort = new AbortController();
13966
14230
  const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
13967
14231
  const taskStartedAt = nowMs();
14232
+ const streamState = createStreamState(taskStartedAt);
14233
+ let externalTerminalRequest = null;
14234
+ let desktopTerminalFinalExpected = false;
14235
+ let resolveExternalTerminal = null;
14236
+ const externalTerminalPromise = new Promise((resolve2) => {
14237
+ resolveExternalTerminal = resolve2;
14238
+ });
14239
+ let processResultSettled = false;
14240
+ let resolveExternalTerminalCompletion = null;
14241
+ let externalTerminalCompletionSettled = false;
14242
+ const externalTerminalCompletion = new Promise((resolve2) => {
14243
+ resolveExternalTerminalCompletion = resolve2;
14244
+ });
14245
+ const settleExternalTerminalCompletion = (finalized) => {
14246
+ if (!externalTerminalRequest || externalTerminalCompletionSettled) return;
14247
+ externalTerminalCompletionSettled = true;
14248
+ resolveExternalTerminalCompletion?.(finalized);
14249
+ };
13968
14250
  deps.resetMirrorSessionForInteractiveRun(binding.codepilotSessionId);
13969
14251
  const taskState = {
13970
14252
  id: taskId,
@@ -13978,12 +14260,35 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13978
14260
  structuredStreamUiActive: false,
13979
14261
  lastActivityAt: taskStartedAt,
13980
14262
  lastResponseAt: null,
13981
- idleReminderSent: false,
14263
+ lastContentResponseAt: null,
13982
14264
  streamFinalized: false,
13983
14265
  uiEnded: false,
13984
- mirrorSuppressionId: null
14266
+ mirrorSuppressionId: null,
14267
+ finalizeFromExternalTerminal: async (outcome, detail, finalText) => {
14268
+ if (externalTerminalRequest) return externalTerminalCompletion;
14269
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return false;
14270
+ externalTerminalRequest = { outcome, detail, finalText };
14271
+ resolveExternalTerminal?.(externalTerminalRequest);
14272
+ if (!processResultSettled && !taskAbort.signal.aborted) {
14273
+ taskAbort.abort();
14274
+ }
14275
+ return externalTerminalCompletion;
14276
+ }
13985
14277
  };
13986
14278
  deps.registerInteractiveTask(taskState);
14279
+ deps.registerBridgeTurn?.({
14280
+ id: taskId,
14281
+ sessionId: binding.codepilotSessionId,
14282
+ kind: desktopThreadId ? "im_desktop_reuse" : "im_sdk",
14283
+ origin: "im",
14284
+ progressSource: "sdk_stream",
14285
+ finalSource: desktopThreadId ? "desktop_task_complete" : "sdk_result",
14286
+ codexThreadId: binding.sdkSessionId || initialSession?.codex_thread_id || initialSession?.sdk_session_id || void 0,
14287
+ desktopThreadId,
14288
+ requestMessageId: msg.messageId,
14289
+ streamKey,
14290
+ startedAt: taskStartedAt
14291
+ });
13987
14292
  deps.recordInteractiveHealthStart(binding.codepilotSessionId);
13988
14293
  let previewState = null;
13989
14294
  const caps = adapter.getPreviewCapabilities?.(msg.address.chatId) ?? null;
@@ -14003,7 +14308,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14003
14308
  const ps = previewState;
14004
14309
  const cfg = streamCfg;
14005
14310
  if (ps.degraded) return;
14006
- const sanitizedText = stripOutboundArtifactBlocksForStreaming(fullText);
14311
+ const sanitizedText = stripFinalOnlyBlocksForStreaming(fullText);
14007
14312
  ps.pendingText = sanitizedText.length > cfg.maxChars ? sanitizedText.slice(0, cfg.maxChars) + "..." : sanitizedText;
14008
14313
  const delta = ps.pendingText.length - ps.lastSentText.length;
14009
14314
  const elapsed = Date.now() - ps.lastSentAt;
@@ -14040,12 +14345,11 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14040
14345
  chatId: msg.address.chatId,
14041
14346
  streamKey,
14042
14347
  ensureStarted: () => {
14043
- adapter.onMessageStart?.(msg.address.chatId, streamKey);
14348
+ ensureMessageStarted();
14044
14349
  }
14045
14350
  };
14046
14351
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
14047
14352
  const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
14048
- let latestStatusNote = null;
14049
14353
  let latestTasks = [];
14050
14354
  const syncStructuredStreamUiState = () => {
14051
14355
  if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
@@ -14064,12 +14368,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14064
14368
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
14065
14369
  pushStreamFeedbackStatus(
14066
14370
  streamFeedbackTarget,
14067
- formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, latestStatusNote)
14371
+ lastResponseAgeMs == null ? buildStreamRuntimeStatus(streamState, nowMs()) : formatStreamRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, streamState.statusNote)
14068
14372
  );
14069
14373
  syncStructuredStreamUiSnapshot();
14070
14374
  };
14071
- const markSuccessfulResponse = () => {
14072
- taskState.lastResponseAt = nowMs();
14375
+ const markActivity = () => {
14376
+ const now2 = nowMs();
14377
+ recordStreamActivity(streamState, now2);
14378
+ taskState.lastActivityAt = streamState.lastActivityAtMs;
14379
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
14380
+ };
14381
+ const markContentResponse = () => {
14382
+ const now2 = nowMs();
14383
+ recordStreamContentResponse(streamState, now2);
14384
+ taskState.lastActivityAt = streamState.lastActivityAtMs;
14385
+ taskState.lastResponseAt = streamState.lastContentResponseAtMs;
14386
+ taskState.lastContentResponseAt = streamState.lastContentResponseAtMs;
14073
14387
  deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
14074
14388
  };
14075
14389
  let streamStatusHeartbeat = null;
@@ -14083,16 +14397,54 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14083
14397
  streamStatusUpdatesClosed = true;
14084
14398
  clearStreamStatusHeartbeat();
14085
14399
  };
14400
+ let structuredStreamInactiveRecorded = false;
14401
+ const recordStructuredStreamInactiveOnce = () => {
14402
+ if (structuredStreamInactiveRecorded) return;
14403
+ structuredStreamInactiveRecorded = true;
14404
+ taskState.structuredStreamUiActive = false;
14405
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
14406
+ };
14407
+ let previewEnded = false;
14408
+ const endPreviewOnce = () => {
14409
+ if (previewEnded) return;
14410
+ previewEnded = true;
14411
+ if (!previewState) return;
14412
+ if (previewState.throttleTimer) {
14413
+ clearTimeout(previewState.throttleTimer);
14414
+ previewState.throttleTimer = null;
14415
+ }
14416
+ adapter.endPreview?.(msg.address.chatId, previewState.draftId);
14417
+ };
14418
+ let streamUiFinalizeAttempted = false;
14419
+ const finalizeStreamUiOnce = async (status, responseText) => {
14420
+ stopStructuredStreamStatusUpdates();
14421
+ recordStructuredStreamInactiveOnce();
14422
+ endPreviewOnce();
14423
+ if (hasStreamingCards && !streamUiFinalizeAttempted) {
14424
+ streamUiFinalizeAttempted = true;
14425
+ taskState.streamFinalized = await finalizeStreamingUi(
14426
+ streamFeedbackTarget,
14427
+ status,
14428
+ assembleDesktopFinalResponse({ text: responseText })
14429
+ );
14430
+ }
14431
+ return taskState.streamFinalized;
14432
+ };
14433
+ const endMessageUiOnce = () => {
14434
+ if (taskState.uiEnded) return;
14435
+ adapter.onMessageEnd?.(msg.address.chatId, streamKey);
14436
+ taskState.uiEnded = true;
14437
+ };
14086
14438
  const onStreamCardText = hasStreamingCards ? (fullText) => {
14087
14439
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14088
14440
  pushStreamFeedbackText(
14089
14441
  streamFeedbackTarget,
14090
- stripOutboundArtifactBlocksForStreaming(fullText)
14442
+ stripFinalOnlyBlocksForStreaming(fullText)
14091
14443
  );
14092
14444
  } : void 0;
14093
14445
  const onToolEvent = (toolId, toolName, status) => {
14094
14446
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14095
- markSuccessfulResponse();
14447
+ markActivity();
14096
14448
  deps.recordInteractiveHealthTool(binding.codepilotSessionId, toolId, toolName, status);
14097
14449
  if (toolName) {
14098
14450
  toolCallTracker.set(toolId, { id: toolId, name: toolName, status });
@@ -14108,7 +14460,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14108
14460
  };
14109
14461
  const onTaskEvent = (tasks) => {
14110
14462
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14111
- markSuccessfulResponse();
14463
+ markActivity();
14112
14464
  latestTasks = tasks;
14113
14465
  if (hasStreamingCards) {
14114
14466
  pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
@@ -14118,17 +14470,15 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14118
14470
  };
14119
14471
  const onStatusNote = (note) => {
14120
14472
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14121
- latestStatusNote = (note || "").trim() || null;
14122
- if (latestStatusNote) {
14123
- markSuccessfulResponse();
14124
- }
14473
+ updateStreamStatusNote(streamState, note, nowMs());
14474
+ if (streamState.statusNote) markActivity();
14125
14475
  pushRunningStatus(null);
14126
14476
  syncStructuredStreamUiSnapshot();
14127
14477
  };
14128
14478
  const onPartialText = (fullText) => {
14129
14479
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14130
14480
  if (fullText.trim()) {
14131
- markSuccessfulResponse();
14481
+ markContentResponse();
14132
14482
  }
14133
14483
  deps.recordInteractiveHealthProgress(binding.codepilotSessionId, "text");
14134
14484
  previewOnPartialText?.(fullText);
@@ -14136,6 +14486,34 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14136
14486
  pushRunningStatus(null);
14137
14487
  syncStructuredStreamUiSnapshot();
14138
14488
  };
14489
+ const waitForDesktopTerminalFinalization = async () => {
14490
+ if (externalTerminalRequest) return externalTerminalRequest;
14491
+ const timeoutMs = Math.max(0, deps.desktopTerminalFinalizationTimeoutMs ?? 0);
14492
+ if (!desktopThreadId || !desktopTerminalFinalExpected || timeoutMs <= 0) return null;
14493
+ if (taskAbort.signal.aborted) return null;
14494
+ return new Promise((resolve2) => {
14495
+ let settled = false;
14496
+ let timer = null;
14497
+ const finish = (terminal) => {
14498
+ if (settled) return;
14499
+ settled = true;
14500
+ if (timer) {
14501
+ clearTimeout(timer);
14502
+ timer = null;
14503
+ }
14504
+ taskAbort.signal.removeEventListener("abort", onAbort);
14505
+ resolve2(terminal);
14506
+ };
14507
+ const onAbort = () => finish(null);
14508
+ timer = setTimeout(() => finish(null), timeoutMs);
14509
+ taskAbort.signal.addEventListener("abort", onAbort, { once: true });
14510
+ externalTerminalPromise.then((terminal) => {
14511
+ finish(terminal);
14512
+ }, () => {
14513
+ finish(null);
14514
+ });
14515
+ });
14516
+ };
14139
14517
  if (supportsStructuredStreamUi) {
14140
14518
  pushRunningStatus(null);
14141
14519
  streamStatusHeartbeat = setIntervalFn(() => {
@@ -14148,8 +14526,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14148
14526
  return;
14149
14527
  }
14150
14528
  const elapsedMs = nowMs() - taskStartedAt;
14151
- const lastResponseAgeMs = taskState.lastResponseAt == null ? null : nowMs() - taskState.lastResponseAt;
14152
- const showLastResponseAge = elapsedMs >= streamStatusIdleDetectionStartMs && lastResponseAgeMs != null && lastResponseAgeMs >= streamStatusHeartbeatMs ? lastResponseAgeMs : null;
14529
+ const showLastResponseAge = shouldShowStreamLastContentResponseAge(streamState, nowMs(), {
14530
+ idleStartMs: streamStatusIdleDetectionStartMs,
14531
+ heartbeatMs: streamStatusHeartbeatMs
14532
+ }) ? getStreamLastContentResponseAgeMs(streamState, nowMs()) : null;
14153
14533
  pushRunningStatus(showLastResponseAge);
14154
14534
  syncStructuredStreamUiSnapshot();
14155
14535
  }, streamStatusHeartbeatMs);
@@ -14159,7 +14539,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14159
14539
  let shouldRecordHealthEnd = true;
14160
14540
  try {
14161
14541
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
14162
- const result = await processMessageImpl(
14542
+ const processPromise = processMessageImpl(
14163
14543
  binding,
14164
14544
  promptText,
14165
14545
  async (perm) => {
@@ -14178,7 +14558,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14178
14558
  "permission_wait",
14179
14559
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
14180
14560
  );
14181
- markSuccessfulResponse();
14561
+ markActivity();
14182
14562
  pushRunningStatus(null);
14183
14563
  syncStructuredStreamUiSnapshot();
14184
14564
  },
@@ -14189,85 +14569,119 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14189
14569
  onTaskEvent,
14190
14570
  onStatusNote,
14191
14571
  (preparedPrompt) => {
14192
- if (!taskState.mirrorSuppressionId) {
14572
+ if (desktopThreadId) {
14573
+ desktopTerminalFinalExpected = true;
14574
+ }
14575
+ if (desktopThreadId && !taskState.mirrorSuppressionId) {
14193
14576
  taskState.mirrorSuppressionId = deps.beginMirrorSuppression(binding.codepilotSessionId, preparedPrompt);
14194
14577
  }
14195
14578
  }
14196
14579
  );
14580
+ let raced;
14581
+ try {
14582
+ raced = await Promise.race([
14583
+ processPromise.then((result2) => ({ kind: "process", result: result2 })),
14584
+ externalTerminalPromise.then((terminal) => ({ kind: "external", terminal }))
14585
+ ]);
14586
+ } catch (error) {
14587
+ if (!externalTerminalRequest) throw error;
14588
+ raced = { kind: "external", terminal: externalTerminalRequest };
14589
+ }
14590
+ if (raced.kind === "external") {
14591
+ processPromise.catch(() => {
14592
+ });
14593
+ finalOutcome = raced.terminal.outcome;
14594
+ finalOutcomeDetail = raced.terminal.detail;
14595
+ const streamEndStatus = raced.terminal.outcome === "completed" ? "completed" : raced.terminal.outcome === "aborted" ? "interrupted" : "error";
14596
+ const staleTaskNotice2 = buildStaleTaskCompletionNotice(msg.address, binding);
14597
+ const terminalResponse2 = assembleDesktopFinalResponse({
14598
+ text: staleTaskNotice2 || raced.terminal.finalText || ""
14599
+ });
14600
+ const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalResponse2.text);
14601
+ if (hasFinalResponsePayload(terminalResponse2)) {
14602
+ await deliverFinalResponse({
14603
+ adapter,
14604
+ address: msg.address,
14605
+ sessionId: binding.codepilotSessionId,
14606
+ replyToMessageId: msg.messageId,
14607
+ deliverResponse: deps.deliverResponse
14608
+ }, terminalResponse2, { skipText: cardFinalized2 });
14609
+ }
14610
+ return;
14611
+ }
14612
+ const result = raced.result;
14613
+ processResultSettled = true;
14197
14614
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
14198
14615
  shouldRecordHealthEnd = false;
14199
14616
  return;
14200
14617
  }
14618
+ const terminalAfterProcess = await waitForDesktopTerminalFinalization();
14619
+ const terminalResponse = terminalAfterProcess?.outcome === "completed" ? assembleDesktopFinalResponse({ text: terminalAfterProcess.finalText || "" }) : null;
14620
+ const sdkResponse = assembleSdkFinalResponse({
14621
+ text: result.responseText,
14622
+ attachments: result.outboundAttachments,
14623
+ hasError: result.hasError,
14624
+ errorMessage: result.errorMessage
14625
+ });
14626
+ const terminalHasFinalPayload = Boolean(
14627
+ terminalResponse && hasFinalResponsePayload(terminalResponse)
14628
+ );
14629
+ const effectiveResponse = terminalResponse && terminalHasFinalPayload ? mergeFinalResponses(terminalResponse, sdkResponse) : sdkResponse;
14201
14630
  let cardFinalized = false;
14202
14631
  const staleTaskNotice = buildStaleTaskCompletionNotice(msg.address, binding);
14632
+ const staleResponse = staleTaskNotice ? assembleDesktopFinalResponse({ text: staleTaskNotice }) : null;
14203
14633
  if (hasStreamingCards) {
14204
- stopStructuredStreamStatusUpdates();
14205
- const streamEndStatus = taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14206
- cardFinalized = await finalizeStreamFeedback(
14207
- streamFeedbackTarget,
14634
+ const streamEndStatus = terminalAfterProcess ? terminalAfterProcess.outcome === "completed" ? "completed" : terminalAfterProcess.outcome === "aborted" ? "interrupted" : "error" : taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14635
+ cardFinalized = await finalizeStreamUiOnce(
14208
14636
  streamEndStatus,
14209
- staleTaskNotice || (streamEndStatus === "interrupted" ? "" : result.responseText)
14637
+ staleResponse?.text || (streamEndStatus === "interrupted" ? "" : effectiveResponse.text)
14210
14638
  );
14211
- taskState.streamFinalized = cardFinalized;
14212
14639
  }
14213
- if (staleTaskNotice) {
14214
- if (!cardFinalized) {
14215
- await deps.deliverResponse(
14216
- adapter,
14217
- msg.address,
14218
- staleTaskNotice,
14219
- binding.codepilotSessionId,
14220
- msg.messageId,
14221
- []
14222
- );
14223
- }
14224
- } else if (result.responseText || result.outboundAttachments.length > 0) {
14225
- const textToDeliver = cardFinalized ? "" : result.responseText;
14226
- if (!cardFinalized || result.outboundAttachments.length > 0) {
14227
- await deps.deliverResponse(
14228
- adapter,
14229
- msg.address,
14230
- textToDeliver,
14231
- binding.codepilotSessionId,
14232
- msg.messageId,
14233
- result.outboundAttachments
14234
- );
14235
- }
14236
- } else if (result.hasError && !taskAbort.signal.aborted) {
14237
- await deps.deliverResponse(
14640
+ if (staleResponse) {
14641
+ await deliverFinalResponse({
14238
14642
  adapter,
14239
- msg.address,
14240
- `**Error:** ${result.errorMessage}`,
14241
- binding.codepilotSessionId,
14242
- msg.messageId,
14243
- []
14643
+ address: msg.address,
14644
+ sessionId: binding.codepilotSessionId,
14645
+ replyToMessageId: msg.messageId,
14646
+ deliverResponse: deps.deliverResponse
14647
+ }, staleResponse, { skipText: cardFinalized });
14648
+ } else if (hasFinalResponsePayload(effectiveResponse)) {
14649
+ await deliverFinalResponse({
14650
+ adapter,
14651
+ address: msg.address,
14652
+ sessionId: binding.codepilotSessionId,
14653
+ replyToMessageId: msg.messageId,
14654
+ deliverResponse: deps.deliverResponse
14655
+ }, effectiveResponse, { skipText: cardFinalized });
14656
+ } else if (result.hasError && !taskAbort.signal.aborted) {
14657
+ await deliverFinalResponse(
14658
+ {
14659
+ adapter,
14660
+ address: msg.address,
14661
+ sessionId: binding.codepilotSessionId,
14662
+ replyToMessageId: msg.messageId,
14663
+ deliverResponse: deps.deliverResponse
14664
+ },
14665
+ assembleSdkFinalResponse({
14666
+ text: `**Error:** ${result.errorMessage}`,
14667
+ hasError: true,
14668
+ errorMessage: result.errorMessage
14669
+ })
14244
14670
  );
14245
14671
  }
14246
14672
  try {
14247
14673
  deps.persistSdkSessionUpdate(binding.codepilotSessionId, result.sdkSessionId, result.hasError);
14248
14674
  } catch {
14249
14675
  }
14250
- finalOutcome = result.hasError ? "failed" : "completed";
14251
- finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
14676
+ finalOutcome = terminalAfterProcess?.outcome || (result.hasError ? "failed" : "completed");
14677
+ finalOutcomeDetail = terminalAfterProcess?.detail || (result.hasError ? result.errorMessage?.trim() || void 0 : void 0);
14252
14678
  } finally {
14253
- stopStructuredStreamStatusUpdates();
14254
- deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
14255
- if (previewState) {
14256
- if (previewState.throttleTimer) {
14257
- clearTimeout(previewState.throttleTimer);
14258
- previewState.throttleTimer = null;
14259
- }
14260
- adapter.endPreview?.(msg.address.chatId, previewState.draftId);
14261
- }
14262
- if (hasStreamingCards && taskAbort.signal.aborted && !taskState.streamFinalized) {
14263
- taskState.streamFinalized = await finalizeStreamFeedback(
14264
- streamFeedbackTarget,
14265
- "interrupted",
14266
- ""
14267
- );
14268
- }
14679
+ await finalizeStreamUiOnce(
14680
+ taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
14681
+ ""
14682
+ );
14269
14683
  if (taskState.mirrorSuppressionId) {
14270
- if (taskAbort.signal.aborted) {
14684
+ if (finalOutcome === "aborted") {
14271
14685
  deps.abortMirrorSuppression(binding.codepilotSessionId, taskState.mirrorSuppressionId);
14272
14686
  } else {
14273
14687
  deps.settleMirrorSuppression(binding.codepilotSessionId, taskState.mirrorSuppressionId);
@@ -14275,17 +14689,16 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14275
14689
  taskState.mirrorSuppressionId = null;
14276
14690
  }
14277
14691
  if (shouldRecordHealthEnd) {
14278
- if (taskAbort.signal.aborted) {
14692
+ if (taskAbort.signal.aborted && !externalTerminalRequest) {
14279
14693
  finalOutcome = "aborted";
14280
14694
  finalOutcomeDetail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14281
14695
  }
14282
14696
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14283
14697
  }
14284
14698
  deps.releaseInteractiveTask(binding.codepilotSessionId, taskId);
14285
- if (!taskState.uiEnded) {
14286
- adapter.onMessageEnd?.(msg.address.chatId, streamKey);
14287
- taskState.uiEnded = true;
14288
- }
14699
+ deps.releaseBridgeTurn?.(binding.codepilotSessionId, taskId);
14700
+ endMessageUiOnce();
14701
+ settleExternalTerminalCompletion(taskState.streamFinalized || !hasStreamingCards);
14289
14702
  }
14290
14703
  }
14291
14704
 
@@ -14298,16 +14711,7 @@ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
14298
14711
  function isTerminalSessionHealthStatus(status) {
14299
14712
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14300
14713
  }
14301
- function buildInteractiveIdleReminderNotice() {
14302
- return [
14303
- "\u63D0\u9192\uFF1A\u8FD9\u8F6E\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5DF2\u7ECF\u8D85\u8FC7 10 \u5206\u949F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8F93\u51FA\u3002",
14304
- "\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u7EC8\u6B62\u5B83\uFF1B\u5982\u679C\u4F60\u4ECD\u5728\u5BF9\u5E94\u7EBF\u7A0B\uFF0C\u53EF\u53D1\u9001 `/stop` \u4E3B\u52A8\u505C\u6B62\uFF1B\u5982\u679C\u5DF2\u7ECF\u5207\u5230\u522B\u7684\u7EBF\u7A0B\uFF0C\u9700\u8981\u5148\u5207\u56DE\u5BF9\u5E94\u7EBF\u7A0B\u3002"
14305
- ].join("\n");
14306
- }
14307
- function shouldSkipIdleReminder(task) {
14308
- return task.adapter.provider === "feishu" && task.structuredStreamUiActive;
14309
- }
14310
- function createInteractiveRuntime(getState2, options, deps) {
14714
+ function createInteractiveRuntime(getState2, deps) {
14311
14715
  function getQueuedCount(sessionId) {
14312
14716
  return getState2().queuedCounts.get(sessionId) || 0;
14313
14717
  }
@@ -14341,7 +14745,6 @@ function createInteractiveRuntime(getState2, options, deps) {
14341
14745
  const task = getState2().activeTasks.get(sessionId);
14342
14746
  if (task?.id !== taskId) return;
14343
14747
  task.lastActivityAt = Date.now();
14344
- task.idleReminderSent = false;
14345
14748
  }
14346
14749
  function releaseInteractiveTask(sessionId, taskId) {
14347
14750
  const state = getState2();
@@ -14350,35 +14753,19 @@ function createInteractiveRuntime(getState2, options, deps) {
14350
14753
  state.activeTasks.delete(sessionId);
14351
14754
  syncSessionRuntimeState(sessionId);
14352
14755
  }
14353
- async function remindIdleInteractiveTask(task) {
14354
- if (!isCurrentInteractiveTask(task.sessionId, task.id) || task.idleReminderSent) return;
14355
- task.idleReminderSent = true;
14356
- try {
14357
- await deliverBridgeNotice(task.adapter, task.address, buildInteractiveIdleReminderNotice(), {
14358
- sessionId: task.sessionId,
14359
- replyToMessageId: task.requestMessageId
14360
- });
14361
- } catch {
14362
- }
14363
- }
14364
- async function reconcileIdleInteractiveTasks() {
14365
- const now2 = Date.now();
14366
- const tasks = Array.from(getState2().activeTasks.values());
14367
- for (const task of tasks) {
14368
- if (shouldSkipIdleReminder(task)) continue;
14369
- if (task.idleReminderSent) continue;
14370
- if (now2 - task.lastActivityAt < options.idleReminderMs) continue;
14371
- await remindIdleInteractiveTask(task);
14372
- }
14756
+ async function finalizeTerminalActiveTask(sessionId, outcome, detail, finalText) {
14757
+ const task = getState2().activeTasks.get(sessionId);
14758
+ if (!task?.finalizeFromExternalTerminal) return false;
14759
+ return task.finalizeFromExternalTerminal(outcome, detail, finalText);
14373
14760
  }
14374
- function reconcileTerminalSessionRuntimeState() {
14761
+ async function reconcileTerminalSessionRuntimeState() {
14375
14762
  const store = deps.getStore();
14376
14763
  for (const session of store.listSessions()) {
14764
+ if (!isTerminalSessionHealthStatus(session.health_status)) continue;
14765
+ if (getState2().activeTasks.has(session.id)) continue;
14377
14766
  const queuedCount = getQueuedCount(session.id);
14378
14767
  const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
14379
- const hasActiveTask = getState2().activeTasks.has(session.id);
14380
- if (hasActiveTask || queuedCount > 0) continue;
14381
- if (!isTerminalSessionHealthStatus(session.health_status)) continue;
14768
+ if (queuedCount > 0) continue;
14382
14769
  if (persistedQueuedCount === 0 && session.runtime_status !== "running" && session.runtime_status !== "queued") {
14383
14770
  continue;
14384
14771
  }
@@ -14449,7 +14836,7 @@ function createInteractiveRuntime(getState2, options, deps) {
14449
14836
  touchInteractiveTask,
14450
14837
  releaseInteractiveTask,
14451
14838
  syncSessionRuntimeState,
14452
- reconcileIdleInteractiveTasks,
14839
+ finalizeTerminalActiveTask,
14453
14840
  reconcileTerminalSessionRuntimeState,
14454
14841
  resetPersistedInteractiveRuntimeState,
14455
14842
  processWithSessionLock
@@ -14788,7 +15175,7 @@ function buildMirrorSubscriptionRegistryPlan(bindings, activeChannelTypes, exist
14788
15175
  if (binding.active === false) return false;
14789
15176
  if (!activeChannels.has(binding.channelType)) return false;
14790
15177
  const session = getSession(binding.codepilotSessionId);
14791
- return Boolean(binding.sdkSessionId || session?.sdk_session_id);
15178
+ return Boolean(session?.desktop_thread_id || (session?.thread_origin === "desktop" ? session.sdk_session_id : null));
14792
15179
  });
14793
15180
  const desiredIds = new Set(upsertBindings.map((binding) => binding.id));
14794
15181
  const removeBindingIds = Array.from(existingBindingIds).filter((bindingId) => !desiredIds.has(bindingId));
@@ -14882,11 +15269,17 @@ function createMirrorRuntime(getState2, options, deps) {
14882
15269
  function clearDanglingMirrorThread(subscription, reason) {
14883
15270
  const { store } = getBridgeContext();
14884
15271
  const session = store.getSession(subscription.sessionId);
14885
- const currentThreadId = session?.sdk_session_id || subscription.threadId;
15272
+ const currentThreadId = getExplicitDesktopThreadId(session) || subscription.threadId;
14886
15273
  console.warn(
14887
15274
  `[bridge-manager] Clearing dangling desktop thread ${currentThreadId} for session ${subscription.sessionId}: ${reason}`
14888
15275
  );
14889
15276
  store.updateSdkSessionId(subscription.sessionId, "");
15277
+ store.updateSession(subscription.sessionId, {
15278
+ sdk_session_id: "",
15279
+ codex_thread_id: void 0,
15280
+ desktop_thread_id: void 0,
15281
+ thread_origin: void 0
15282
+ });
14890
15283
  removeMirrorSubscription(subscription.bindingId);
14891
15284
  }
14892
15285
  function upsertMirrorSubscription(binding) {
@@ -14897,7 +15290,7 @@ function createMirrorRuntime(getState2, options, deps) {
14897
15290
  removeMirrorSubscription(binding.id);
14898
15291
  return;
14899
15292
  }
14900
- const threadId = binding.sdkSessionId || session.sdk_session_id || "";
15293
+ const threadId = getExplicitDesktopThreadId(session) || "";
14901
15294
  if (!threadId) {
14902
15295
  removeMirrorSubscription(binding.id);
14903
15296
  return;
@@ -15010,11 +15403,13 @@ function createMirrorRuntime(getState2, options, deps) {
15010
15403
  `[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
15011
15404
  );
15012
15405
  }
15013
- if (deliverableRecords.length > 0) {
15014
- deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, deliverableRecords);
15406
+ const routeResult = deliverableRecords.length > 0 && deps.routeDesktopRecords ? await deps.routeDesktopRecords(subscription.sessionId, subscription.threadId, deliverableRecords) : { claimed: [], unclaimed: deliverableRecords, terminalClaimed: false };
15407
+ const mirrorRecords = routeResult.unclaimed;
15408
+ if (mirrorRecords.length > 0) {
15409
+ deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, mirrorRecords);
15015
15410
  }
15016
- const blocked = getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId);
15017
- const deliveryPlan = buildMirrorDeliveryPlan(subscription, deliverableRecords, {
15411
+ const blocked = getState2().activeTasks.has(subscription.sessionId);
15412
+ const deliveryPlan = buildMirrorDeliveryPlan(subscription, mirrorRecords, {
15018
15413
  blocked,
15019
15414
  filterSuppressedRecords: deps.filterSuppressedMirrorRecords,
15020
15415
  flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
@@ -15111,6 +15506,241 @@ function createMirrorRuntime(getState2, options, deps) {
15111
15506
  };
15112
15507
  }
15113
15508
 
15509
+ // src/lib/bridge/mirror-feedback-controller.ts
15510
+ function createMirrorStreamFeedbackTarget(subscription, turnState, adapter, startMirrorStreaming) {
15511
+ return {
15512
+ adapter,
15513
+ channelType: subscription.channelType,
15514
+ chatId: subscription.chatId,
15515
+ streamKey: turnState.streamKey,
15516
+ ensureStarted: () => {
15517
+ startMirrorStreaming(subscription, turnState);
15518
+ }
15519
+ };
15520
+ }
15521
+ function createMirrorFeedbackController(deps) {
15522
+ function getMirrorStreamingAdapter(subscription) {
15523
+ const adapter = deps.getAdapter(subscription.channelType);
15524
+ if (!adapter || !adapter.isRunning()) return null;
15525
+ if (getChannelProviderKey(subscription.channelType) !== "feishu") return null;
15526
+ if (typeof adapter.onStreamText !== "function" || typeof adapter.onStreamEnd !== "function") {
15527
+ return null;
15528
+ }
15529
+ return adapter;
15530
+ }
15531
+ function getMirrorStreamingText(subscription, turnState) {
15532
+ const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
15533
+ const markdown = getFeedbackParseMode(subscription.channelType) === "Markdown";
15534
+ const rendered = formatMirrorMessage(
15535
+ title,
15536
+ turnState.userText,
15537
+ stripOutboundArtifactBlocksForStreaming(turnState.streamedText),
15538
+ markdown,
15539
+ true
15540
+ );
15541
+ return rendered || buildMirrorTitle(title, markdown);
15542
+ }
15543
+ function startMirrorStreaming(subscription, turnState) {
15544
+ const adapter = getMirrorStreamingAdapter(subscription);
15545
+ if (!adapter || turnState.streamStarted) return;
15546
+ try {
15547
+ adapter.onMirrorStreamStart?.(subscription.chatId, turnState.streamKey);
15548
+ if (!adapter.onMirrorStreamStart) {
15549
+ adapter.onStreamText?.(subscription.chatId, "", turnState.streamKey);
15550
+ }
15551
+ turnState.streamStarted = true;
15552
+ } catch {
15553
+ }
15554
+ }
15555
+ function createStreamTarget(subscription, turnState, adapter) {
15556
+ return createMirrorStreamFeedbackTarget(subscription, turnState, adapter, startMirrorStreaming);
15557
+ }
15558
+ function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
15559
+ const adapter = getMirrorStreamingAdapter(subscription);
15560
+ if (!adapter || typeof adapter.onStreamStatus !== "function") return;
15561
+ if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
15562
+ const startedAtMs = Date.parse(turnState.startedAt);
15563
+ if (!Number.isFinite(startedAtMs)) return;
15564
+ const nowMs = options.nowMs ?? Date.now();
15565
+ const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
15566
+ if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
15567
+ return;
15568
+ }
15569
+ const statusText = formatStreamRuntimeStatus(
15570
+ Math.max(0, nowMs - startedAtMs),
15571
+ options.lastResponseAgeMs,
15572
+ turnState.statusNote
15573
+ );
15574
+ if (turnState.lastStatusText === statusText) return;
15575
+ const pushed = pushStreamFeedbackStatus(
15576
+ createStreamTarget(subscription, turnState, adapter),
15577
+ statusText
15578
+ );
15579
+ if (!pushed) return;
15580
+ turnState.lastStatusText = statusText;
15581
+ turnState.lastStatusAt = nowMs;
15582
+ }
15583
+ function refreshMirrorStreamingStatus2(subscription, nowMs = Date.now(), config2) {
15584
+ const pendingTurn = subscription.pendingTurn;
15585
+ if (!pendingTurn?.streamStarted) return;
15586
+ const startedAtMs = Date.parse(pendingTurn.startedAt);
15587
+ if (!Number.isFinite(startedAtMs)) return;
15588
+ const lastContentResponseAtMs = pendingTurn.lastContentResponseAt ? Date.parse(pendingTurn.lastContentResponseAt) : pendingTurn.lastResponseAt ? Date.parse(pendingTurn.lastResponseAt) : null;
15589
+ const streamState = {
15590
+ startedAtMs,
15591
+ lastContentResponseAtMs: Number.isFinite(lastContentResponseAtMs) ? lastContentResponseAtMs : null
15592
+ };
15593
+ if (!shouldShowStreamLastContentResponseAge(streamState, nowMs, config2)) return;
15594
+ pushMirrorStreamingStatus(subscription, pendingTurn, {
15595
+ nowMs,
15596
+ lastResponseAgeMs: getStreamLastContentResponseAgeMs(streamState, nowMs),
15597
+ minIntervalMs: config2.heartbeatMs
15598
+ });
15599
+ }
15600
+ function updateMirrorStreaming(subscription, turnState) {
15601
+ const adapter = getMirrorStreamingAdapter(subscription);
15602
+ if (!adapter) return;
15603
+ pushStreamFeedbackText(
15604
+ createStreamTarget(subscription, turnState, adapter),
15605
+ getMirrorStreamingText(subscription, turnState)
15606
+ );
15607
+ pushMirrorStreamingStatus(subscription, turnState);
15608
+ }
15609
+ function updateMirrorToolProgress(subscription, turnState) {
15610
+ const adapter = getMirrorStreamingAdapter(subscription);
15611
+ if (!adapter) return;
15612
+ pushStreamFeedbackTools(
15613
+ createStreamTarget(subscription, turnState, adapter),
15614
+ Array.from(turnState.toolCalls.values())
15615
+ );
15616
+ pushMirrorStreamingStatus(subscription, turnState);
15617
+ }
15618
+ function updateMirrorTaskProgress(subscription, turnState) {
15619
+ const adapter = getMirrorStreamingAdapter(subscription);
15620
+ if (!adapter) return;
15621
+ pushStreamFeedbackTasks(
15622
+ createStreamTarget(subscription, turnState, adapter),
15623
+ turnState.taskItems
15624
+ );
15625
+ pushMirrorStreamingStatus(subscription, turnState);
15626
+ }
15627
+ function updateMirrorStatusProgress(subscription, turnState) {
15628
+ const adapter = getMirrorStreamingAdapter(subscription);
15629
+ if (!adapter) return;
15630
+ pushMirrorStreamingStatus(subscription, turnState);
15631
+ }
15632
+ function stopMirrorStreaming2(subscription, status = "interrupted") {
15633
+ const adapter = getMirrorStreamingAdapter(subscription);
15634
+ const pendingTurn = subscription.pendingTurn;
15635
+ if (!adapter || !pendingTurn?.streamStarted) return;
15636
+ void finalizeStreamFeedback(
15637
+ createStreamTarget(subscription, pendingTurn, adapter),
15638
+ status,
15639
+ getMirrorStreamingText(subscription, pendingTurn)
15640
+ );
15641
+ }
15642
+ async function deliverMirrorTurn(subscription, turn) {
15643
+ const adapter = deps.getAdapter(subscription.channelType);
15644
+ if (!adapter || !adapter.isRunning()) return;
15645
+ const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
15646
+ const responseParseMode = getFeedbackParseMode(subscription.channelType);
15647
+ const markdown = responseParseMode === "Markdown";
15648
+ const rawFinalResponse = assembleDesktopFinalResponse({ text: turn.text });
15649
+ const attachments = rawFinalResponse.attachments;
15650
+ const cleanTurnText = rawFinalResponse.text;
15651
+ const renderedTextBase = formatMirrorMessage(title, turn.userText, cleanTurnText, markdown);
15652
+ const renderedStreamTextBase = formatMirrorMessage(title, turn.userText, cleanTurnText, markdown, true);
15653
+ const renderedText = turn.timedOut ? appendMirrorTimeoutNotice(renderedTextBase || buildMirrorTitle(title, markdown), markdown) : renderedTextBase;
15654
+ const renderedStreamText = turn.timedOut ? appendMirrorTimeoutNotice(renderedStreamTextBase || buildMirrorTitle(title, markdown), markdown) : renderedStreamTextBase;
15655
+ const text2 = renderedText ? renderFeedbackText(renderedText, responseParseMode) : "";
15656
+ const streamText = renderFeedbackText(
15657
+ renderedStreamText || buildMirrorTitle(title, markdown),
15658
+ responseParseMode
15659
+ );
15660
+ const address = {
15661
+ channelType: subscription.channelType,
15662
+ chatId: subscription.chatId
15663
+ };
15664
+ if (getChannelProviderKey(subscription.channelType) === "feishu" && typeof adapter.onStreamEnd === "function") {
15665
+ try {
15666
+ const finalized = await adapter.onStreamEnd(
15667
+ subscription.chatId,
15668
+ turn.status,
15669
+ streamText,
15670
+ turn.streamKey
15671
+ );
15672
+ if (finalized) {
15673
+ if (attachments.length > 0) {
15674
+ const attachmentResult = await deliverFinalResponse(
15675
+ {
15676
+ adapter,
15677
+ address,
15678
+ sessionId: subscription.sessionId,
15679
+ deliverResponse: deps.deliverResponse
15680
+ },
15681
+ assembleDesktopFinalResponse({ attachments }),
15682
+ { skipText: true }
15683
+ );
15684
+ if (!attachmentResult.ok) {
15685
+ throw new Error(attachmentResult.error || "mirror attachment delivery failed");
15686
+ }
15687
+ }
15688
+ subscription.lastDeliveredAt = turn.timestamp || deps.nowIso();
15689
+ return;
15690
+ }
15691
+ } catch (error) {
15692
+ console.warn("[bridge-manager] Mirror stream finalize failed:", error instanceof Error ? error.message : error);
15693
+ }
15694
+ }
15695
+ const finalResponse = assembleDesktopFinalResponse({
15696
+ text: text2,
15697
+ attachments
15698
+ });
15699
+ if (!finalResponse.text && finalResponse.attachments.length === 0) return;
15700
+ const response = await deliverFinalResponse({
15701
+ adapter,
15702
+ address,
15703
+ sessionId: subscription.sessionId,
15704
+ deliverResponse: deps.deliverResponse,
15705
+ deliverText: async (messageText) => deliver(adapter, {
15706
+ address,
15707
+ text: messageText,
15708
+ parseMode: responseParseMode
15709
+ }, {
15710
+ sessionId: subscription.sessionId,
15711
+ dedupKey: `mirror:${subscription.bindingId}:${turn.signature}`
15712
+ })
15713
+ }, finalResponse);
15714
+ if (!response.ok) {
15715
+ throw new Error(response.error || "mirror delivery failed");
15716
+ }
15717
+ subscription.lastDeliveredAt = turn.timestamp || deps.nowIso();
15718
+ }
15719
+ async function deliverMirrorTurns2(subscription, turns) {
15720
+ let deliveredCount = 0;
15721
+ for (const turn of turns.slice(0, deps.eventBatchLimit)) {
15722
+ try {
15723
+ await deliverMirrorTurn(subscription, turn);
15724
+ deliveredCount += 1;
15725
+ } catch (error) {
15726
+ return { deliveredCount, error };
15727
+ }
15728
+ }
15729
+ return { deliveredCount };
15730
+ }
15731
+ return {
15732
+ hooks: {
15733
+ onStreamText: updateMirrorStreaming,
15734
+ onStatusProgress: updateMirrorStatusProgress,
15735
+ onTaskProgress: updateMirrorTaskProgress,
15736
+ onToolProgress: updateMirrorToolProgress
15737
+ },
15738
+ refreshMirrorStreamingStatus: refreshMirrorStreamingStatus2,
15739
+ stopMirrorStreaming: stopMirrorStreaming2,
15740
+ deliverMirrorTurns: deliverMirrorTurns2
15741
+ };
15742
+ }
15743
+
15114
15744
  // src/lib/bridge/session-health-process.ts
15115
15745
  import { execFile } from "node:child_process";
15116
15746
  import { promisify } from "node:util";
@@ -15203,7 +15833,6 @@ async function probeCodexThreadProcess(threadId) {
15203
15833
  var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
15204
15834
  var HEALTH_SLOW_OBSERVED_MS = 30 * 60 * 1e3;
15205
15835
  var HEALTH_PROGRESS_PERSIST_THROTTLE_MS = 15 * 1e3;
15206
- var HEALTH_PROCESS_PROBE_CACHE_MS = 30 * 1e3;
15207
15836
  var HEALTH_STREAM_UI_STALL_MS = 60 * 1e3;
15208
15837
  var RUNNING_HEALTH_STATUSES = /* @__PURE__ */ new Set([
15209
15838
  "running_active",
@@ -15342,6 +15971,7 @@ function computeBaseDiagnosis(session, nowMs) {
15342
15971
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15343
15972
  return {
15344
15973
  sessionId: session.id,
15974
+ checkedAt: null,
15345
15975
  runtimeStatus,
15346
15976
  healthStatus: fallbackStatus,
15347
15977
  healthReason: session.health_reason?.trim() || (fallbackStatus === "idle" ? "\u5F53\u524D\u6CA1\u6709\u8BB0\u5F55\u5230\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002" : "\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u8BE6\u7EC6\u8FDB\u5C55\u3002"),
@@ -15379,6 +16009,7 @@ function computeBaseDiagnosis(session, nowMs) {
15379
16009
  }
15380
16010
  return {
15381
16011
  sessionId: session.id,
16012
+ checkedAt: null,
15382
16013
  runtimeStatus,
15383
16014
  healthStatus,
15384
16015
  healthReason,
@@ -15487,7 +16118,6 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15487
16118
  // src/lib/bridge/session-health-runtime.ts
15488
16119
  function createSessionHealthRuntime(deps) {
15489
16120
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
15490
- const processProbeCache = /* @__PURE__ */ new Map();
15491
16121
  function summarizePlanUpdate(tasks) {
15492
16122
  if (!Array.isArray(tasks) || tasks.length === 0) {
15493
16123
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
@@ -15516,7 +16146,7 @@ function createSessionHealthRuntime(deps) {
15516
16146
  }
15517
16147
  }
15518
16148
  if (!changed) return;
15519
- store.updateSession(sessionId, next);
16149
+ store.updateSession(sessionId, next, { touch: options?.touch });
15520
16150
  }
15521
16151
  function maybePersistProgress(sessionId, updates, progressType) {
15522
16152
  const nowMs = Date.now();
@@ -15619,10 +16249,13 @@ function createSessionHealthRuntime(deps) {
15619
16249
  active_tool_name: void 0,
15620
16250
  active_tool_started_at: void 0,
15621
16251
  stream_ui_flush_started_at: void 0,
15622
- last_health_check_at: nowIso4
16252
+ last_stream_ui_attempt_at: void 0,
16253
+ last_stream_ui_update_at: void 0,
16254
+ last_stream_ui_error_at: void 0,
16255
+ last_stream_ui_error: void 0,
16256
+ stream_ui_consecutive_failures: void 0
15623
16257
  }, { force: true });
15624
16258
  lastProgressPersistAt.set(sessionId, Date.now());
15625
- processProbeCache.delete(sessionId);
15626
16259
  }
15627
16260
  function toIso(value) {
15628
16261
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
@@ -15638,6 +16271,12 @@ function createSessionHealthRuntime(deps) {
15638
16271
  updates.last_stream_ui_error_at = toIso(snapshot.lastErrorAt);
15639
16272
  updates.last_stream_ui_error = snapshot.lastError?.trim() || void 0;
15640
16273
  updates.stream_ui_consecutive_failures = snapshot.consecutiveFailures && snapshot.consecutiveFailures > 0 ? snapshot.consecutiveFailures : void 0;
16274
+ } else {
16275
+ updates.last_stream_ui_attempt_at = void 0;
16276
+ updates.last_stream_ui_update_at = void 0;
16277
+ updates.last_stream_ui_error_at = void 0;
16278
+ updates.last_stream_ui_error = void 0;
16279
+ updates.stream_ui_consecutive_failures = void 0;
15641
16280
  }
15642
16281
  updateSessionHealth(sessionId, updates);
15643
16282
  }
@@ -15705,23 +16344,13 @@ function createSessionHealthRuntime(deps) {
15705
16344
  updateSessionHealth(session.id, {
15706
16345
  health_status: diagnosis.healthStatus,
15707
16346
  health_reason: diagnosis.healthReason
15708
- });
16347
+ }, { touch: false });
15709
16348
  }
15710
16349
  }
15711
16350
  async function loadProcessProbe(session) {
15712
16351
  const threadId = session.sdk_session_id?.trim();
15713
16352
  if (!threadId || !deps.probeThreadProcess) return null;
15714
- const cached = processProbeCache.get(session.id);
15715
- const nowMs = Date.now();
15716
- if (cached && nowMs - cached.checkedAtMs < HEALTH_PROCESS_PROBE_CACHE_MS) {
15717
- return cached.result;
15718
- }
15719
- const result = await deps.probeThreadProcess(threadId);
15720
- processProbeCache.set(session.id, {
15721
- checkedAtMs: nowMs,
15722
- result
15723
- });
15724
- return result;
16353
+ return deps.probeThreadProcess(threadId);
15725
16354
  }
15726
16355
  async function diagnoseSessionHealth(sessionId) {
15727
16356
  const store = deps.getStore();
@@ -15729,16 +16358,10 @@ function createSessionHealthRuntime(deps) {
15729
16358
  if (!session) return null;
15730
16359
  const base = computeBaseDiagnosis(session, Date.now());
15731
16360
  const processProbe = await loadProcessProbe(session);
15732
- const diagnosis = applyStreamUiDiagnosis(
16361
+ return applyStreamUiDiagnosis(
15733
16362
  applyProcessProbeDiagnosis(base, processProbe),
15734
16363
  Date.now()
15735
16364
  );
15736
- updateSessionHealth(sessionId, {
15737
- health_status: diagnosis.healthStatus,
15738
- health_reason: diagnosis.healthReason,
15739
- last_health_check_at: deps.nowIso()
15740
- });
15741
- return diagnosis;
15742
16365
  }
15743
16366
  async function diagnoseAllActiveSessions() {
15744
16367
  const store = deps.getStore();
@@ -15759,6 +16382,100 @@ function createSessionHealthRuntime(deps) {
15759
16382
  };
15760
16383
  }
15761
16384
 
16385
+ // src/lib/bridge/turns/desktop-terminal-router.ts
16386
+ function isTerminalRecord(record) {
16387
+ return record.type === "task_complete" || record.type === "task_aborted";
16388
+ }
16389
+ function toTerminalRecord(sessionId, desktopThreadId, record) {
16390
+ return {
16391
+ sessionId,
16392
+ desktopThreadId,
16393
+ turnId: record.turnId,
16394
+ text: record.content,
16395
+ outcome: record.type === "task_aborted" ? "aborted" : "completed",
16396
+ timestamp: record.timestamp
16397
+ };
16398
+ }
16399
+ async function routeDesktopRecords(sessionId, desktopThreadId, records, coordinator) {
16400
+ let terminalRecord = null;
16401
+ for (let index = records.length - 1; index >= 0; index -= 1) {
16402
+ if (!isTerminalRecord(records[index])) continue;
16403
+ terminalRecord = records[index];
16404
+ break;
16405
+ }
16406
+ if (!terminalRecord) {
16407
+ return {
16408
+ claimed: [],
16409
+ unclaimed: records,
16410
+ terminalClaimed: false
16411
+ };
16412
+ }
16413
+ const claim = await coordinator.claimDesktopTerminal(
16414
+ toTerminalRecord(sessionId, desktopThreadId, terminalRecord)
16415
+ );
16416
+ if (!claim.claimed) {
16417
+ return {
16418
+ claimed: [],
16419
+ unclaimed: records,
16420
+ terminalClaimed: false
16421
+ };
16422
+ }
16423
+ const claimedTurnId = terminalRecord.turnId;
16424
+ const claimed = claimedTurnId ? records.filter((record) => record.turnId === claimedTurnId) : [terminalRecord];
16425
+ const claimedSet = new Set(claimed.map((record) => record.signature));
16426
+ return {
16427
+ claimed,
16428
+ unclaimed: records.filter((record) => !claimedSet.has(record.signature)),
16429
+ terminalClaimed: true
16430
+ };
16431
+ }
16432
+
16433
+ // src/lib/bridge/turns/turn-coordinator.ts
16434
+ function createTurnCoordinator(deps = {}) {
16435
+ const activeTurnsBySession = /* @__PURE__ */ new Map();
16436
+ function registerInteractiveTurn(turn) {
16437
+ activeTurnsBySession.set(turn.sessionId, turn);
16438
+ }
16439
+ function getActiveTurn(sessionId) {
16440
+ return activeTurnsBySession.get(sessionId);
16441
+ }
16442
+ async function claimDesktopTerminal(terminal) {
16443
+ const turn = activeTurnsBySession.get(terminal.sessionId);
16444
+ if (!turn || turn.kind !== "im_desktop_reuse") {
16445
+ return { claimed: false };
16446
+ }
16447
+ if (turn.desktopThreadId && turn.desktopThreadId !== terminal.desktopThreadId) {
16448
+ return { claimed: false };
16449
+ }
16450
+ const finalized = await deps.finalizeTerminalTurn?.(turn, terminal);
16451
+ return finalized ? { claimed: true, turn } : { claimed: false, turn };
16452
+ }
16453
+ function releaseTurn(turnId) {
16454
+ for (const [sessionId, turn] of activeTurnsBySession) {
16455
+ if (turn.id !== turnId) continue;
16456
+ activeTurnsBySession.delete(sessionId);
16457
+ return;
16458
+ }
16459
+ }
16460
+ function releaseSessionTurn(sessionId, turnId) {
16461
+ const turn = activeTurnsBySession.get(sessionId);
16462
+ if (!turn) return;
16463
+ if (turnId && turn.id !== turnId) return;
16464
+ activeTurnsBySession.delete(sessionId);
16465
+ }
16466
+ function clear() {
16467
+ activeTurnsBySession.clear();
16468
+ }
16469
+ return {
16470
+ registerInteractiveTurn,
16471
+ getActiveTurn,
16472
+ claimDesktopTerminal,
16473
+ releaseTurn,
16474
+ releaseSessionTurn,
16475
+ clear
16476
+ };
16477
+ }
16478
+
15762
16479
  // src/lib/bridge/bridge-manager.ts
15763
16480
  var GLOBAL_KEY = "__bridge_manager__";
15764
16481
  var DANGLING_MIRROR_THREAD_RETRY_LIMIT = 3;
@@ -15769,10 +16486,10 @@ var MIRROR_WATCH_DEBOUNCE_MS = 350;
15769
16486
  var MIRROR_EVENT_BATCH_LIMIT = 8;
15770
16487
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
15771
16488
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
15772
- var INTERACTIVE_IDLE_REMINDER_MS = 6e5;
16489
+ var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
15773
16490
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
15774
16491
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
15775
- var MIRROR_IDLE_TIMEOUT_MS = 6e5;
16492
+ var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
15776
16493
  function describeUnknownError(error) {
15777
16494
  if (error instanceof Error) {
15778
16495
  return error.stack || `${error.name}: ${error.message}`;
@@ -15854,11 +16571,26 @@ function getState() {
15854
16571
  return g[GLOBAL_KEY];
15855
16572
  }
15856
16573
  var INTERACTIVE_RUNTIME = createInteractiveRuntime(getState, {
15857
- idleReminderMs: INTERACTIVE_IDLE_REMINDER_MS
15858
- }, {
15859
16574
  getStore: () => getBridgeContext().store,
15860
16575
  nowIso: nowIso3
15861
16576
  });
16577
+ function formatDesktopTerminalDetail(terminal) {
16578
+ if (terminal.outcome === "aborted") {
16579
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
16580
+ }
16581
+ if (terminal.outcome === "failed") {
16582
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5F53\u524D\u4EFB\u52A1\u6267\u884C\u5931\u8D25\u3002";
16583
+ }
16584
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002";
16585
+ }
16586
+ var TURN_COORDINATOR = createTurnCoordinator({
16587
+ finalizeTerminalTurn: (turn, terminal) => INTERACTIVE_RUNTIME.finalizeTerminalActiveTask(
16588
+ turn.sessionId,
16589
+ terminal.outcome,
16590
+ formatDesktopTerminalDetail(terminal),
16591
+ terminal.text
16592
+ )
16593
+ });
15862
16594
  var SESSION_HEALTH_RUNTIME = createSessionHealthRuntime({
15863
16595
  getStore: () => getBridgeContext().store,
15864
16596
  nowIso: nowIso3,
@@ -15895,9 +16627,6 @@ function settleMirrorSuppression2(sessionId, suppressionId, durationMs = MIRROR_
15895
16627
  durationMs
15896
16628
  );
15897
16629
  }
15898
- function isMirrorSuppressed2(sessionId) {
15899
- return isMirrorSuppressed(getMirrorSuppressionStore(), sessionId);
15900
- }
15901
16630
  function filterSuppressedMirrorRecords2(sessionId, records) {
15902
16631
  return filterSuppressedMirrorRecords(
15903
16632
  getMirrorSuppressionStore(),
@@ -15931,28 +16660,6 @@ function syncMirrorSessionStateSafe(sessionId, context) {
15931
16660
  );
15932
16661
  }
15933
16662
  }
15934
- function getMirrorStreamingAdapter(subscription) {
15935
- const state = getState();
15936
- const adapter = state.adapters.get(subscription.channelType);
15937
- if (!adapter || !adapter.isRunning()) return null;
15938
- if (getChannelProviderKey(subscription.channelType) !== "feishu") return null;
15939
- if (typeof adapter.onStreamText !== "function" || typeof adapter.onStreamEnd !== "function") {
15940
- return null;
15941
- }
15942
- return adapter;
15943
- }
15944
- function getMirrorStreamingText(subscription, turnState) {
15945
- const title = getDesktopThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
15946
- const markdown = getFeedbackParseMode(subscription.channelType) === "Markdown";
15947
- const rendered = formatMirrorMessage(
15948
- title,
15949
- turnState.userText,
15950
- turnState.streamedText,
15951
- markdown,
15952
- true
15953
- );
15954
- return rendered || buildMirrorTitle(title, markdown);
15955
- }
15956
16663
  function getMirrorStructuredStreamStatusConfig() {
15957
16664
  const { store } = getBridgeContext();
15958
16665
  const idleStartSeconds = parseInt(store.getSetting("bridge_stream_status_idle_start_seconds") || "", 10);
@@ -15968,194 +16675,43 @@ function getMirrorStructuredStreamStatusConfig() {
15968
16675
  )
15969
16676
  };
15970
16677
  }
15971
- function startMirrorStreaming(subscription, turnState) {
15972
- const adapter = getMirrorStreamingAdapter(subscription);
15973
- if (!adapter || turnState.streamStarted) return;
15974
- try {
15975
- adapter.onMirrorStreamStart?.(subscription.chatId, turnState.streamKey);
15976
- if (!adapter.onMirrorStreamStart) {
15977
- adapter.onStreamText?.(subscription.chatId, "", turnState.streamKey);
15978
- }
15979
- turnState.streamStarted = true;
15980
- } catch {
15981
- }
15982
- }
15983
- function createMirrorStreamFeedbackTarget(subscription, turnState, adapter) {
15984
- return {
15985
- adapter,
15986
- channelType: subscription.channelType,
15987
- chatId: subscription.chatId,
15988
- streamKey: turnState.streamKey,
15989
- ensureStarted: () => {
15990
- startMirrorStreaming(subscription, turnState);
15991
- }
15992
- };
15993
- }
15994
- function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
15995
- const adapter = getMirrorStreamingAdapter(subscription);
15996
- if (!adapter || typeof adapter.onStreamStatus !== "function") return;
15997
- if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
15998
- const startedAtMs = Date.parse(turnState.startedAt);
15999
- if (!Number.isFinite(startedAtMs)) return;
16000
- const nowMs = options.nowMs ?? Date.now();
16001
- const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
16002
- if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
16003
- return;
16004
- }
16005
- const statusText = formatInteractiveRuntimeStatus(
16006
- Math.max(0, nowMs - startedAtMs),
16007
- options.lastResponseAgeMs,
16008
- turnState.statusNote
16009
- );
16010
- if (turnState.lastStatusText === statusText) return;
16011
- pushStreamFeedbackStatus(
16012
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16013
- statusText
16014
- );
16015
- turnState.lastStatusText = statusText;
16016
- turnState.lastStatusAt = nowMs;
16017
- }
16678
+ var MIRROR_FEEDBACK = createMirrorFeedbackController({
16679
+ getAdapter: (channelType) => getState().adapters.get(channelType) || null,
16680
+ getThreadTitle: (threadId) => getDesktopThreadTitle(threadId),
16681
+ nowIso: nowIso3,
16682
+ eventBatchLimit: MIRROR_EVENT_BATCH_LIMIT,
16683
+ deliverResponse
16684
+ });
16018
16685
  function refreshMirrorStreamingStatus(subscription, nowMs = Date.now(), config2 = getMirrorStructuredStreamStatusConfig()) {
16019
- const pendingTurn = subscription.pendingTurn;
16020
- if (!pendingTurn?.streamStarted) return;
16021
- const startedAtMs = Date.parse(pendingTurn.startedAt);
16022
- if (!Number.isFinite(startedAtMs)) return;
16023
- const elapsedMs = nowMs - startedAtMs;
16024
- if (elapsedMs < config2.idleStartMs) return;
16025
- const lastResponseAtMs = pendingTurn.lastResponseAt ? Date.parse(pendingTurn.lastResponseAt) : NaN;
16026
- const lastResponseAgeMs = Number.isFinite(lastResponseAtMs) ? nowMs - lastResponseAtMs : null;
16027
- if (lastResponseAgeMs != null && lastResponseAgeMs < config2.heartbeatMs) return;
16028
- pushMirrorStreamingStatus(subscription, pendingTurn, {
16029
- nowMs,
16030
- lastResponseAgeMs,
16031
- minIntervalMs: config2.heartbeatMs
16032
- });
16686
+ MIRROR_FEEDBACK.refreshMirrorStreamingStatus(subscription, nowMs, config2);
16033
16687
  }
16034
16688
  function refreshActiveMirrorStreamingStatuses(nowMs = Date.now()) {
16035
16689
  for (const subscription of getState().mirrorSubscriptions.values()) {
16036
16690
  refreshMirrorStreamingStatus(subscription, nowMs);
16037
16691
  }
16038
16692
  }
16039
- function updateMirrorStreaming(subscription, turnState) {
16040
- const adapter = getMirrorStreamingAdapter(subscription);
16041
- if (!adapter) return;
16042
- pushStreamFeedbackText(
16043
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16044
- getMirrorStreamingText(subscription, turnState)
16045
- );
16046
- pushMirrorStreamingStatus(subscription, turnState);
16047
- }
16048
- function updateMirrorToolProgress(subscription, turnState) {
16049
- const adapter = getMirrorStreamingAdapter(subscription);
16050
- if (!adapter) return;
16051
- pushStreamFeedbackTools(
16052
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16053
- Array.from(turnState.toolCalls.values())
16054
- );
16055
- pushMirrorStreamingStatus(subscription, turnState);
16056
- }
16057
- function updateMirrorTaskProgress(subscription, turnState) {
16058
- const adapter = getMirrorStreamingAdapter(subscription);
16059
- if (!adapter) return;
16060
- pushStreamFeedbackTasks(
16061
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16062
- turnState.taskItems
16063
- );
16064
- pushMirrorStreamingStatus(subscription, turnState);
16065
- }
16066
- function updateMirrorStatusProgress(subscription, turnState) {
16067
- const adapter = getMirrorStreamingAdapter(subscription);
16068
- if (!adapter) return;
16069
- pushMirrorStreamingStatus(subscription, turnState);
16070
- }
16071
16693
  function stopMirrorStreaming(subscription, status = "interrupted") {
16072
- const adapter = getMirrorStreamingAdapter(subscription);
16073
- const pendingTurn = subscription.pendingTurn;
16074
- if (!adapter || !pendingTurn?.streamStarted) return;
16075
- void finalizeStreamFeedback(
16076
- createMirrorStreamFeedbackTarget(subscription, pendingTurn, adapter),
16077
- status,
16078
- getMirrorStreamingText(subscription, pendingTurn)
16079
- );
16080
- }
16081
- async function deliverMirrorTurn(subscription, turn) {
16082
- const state = getState();
16083
- const adapter = state.adapters.get(subscription.channelType);
16084
- if (!adapter || !adapter.isRunning()) return;
16085
- const title = getDesktopThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
16086
- const responseParseMode = getFeedbackParseMode(subscription.channelType);
16087
- const markdown = responseParseMode === "Markdown";
16088
- const renderedTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown);
16089
- const renderedStreamTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown, true);
16090
- const renderedText = turn.timedOut ? appendMirrorTimeoutNotice(renderedTextBase || buildMirrorTitle(title, markdown), markdown) : renderedTextBase;
16091
- const renderedStreamText = turn.timedOut ? appendMirrorTimeoutNotice(renderedStreamTextBase || buildMirrorTitle(title, markdown), markdown) : renderedStreamTextBase;
16092
- const text2 = renderedText ? renderFeedbackText(renderedText, responseParseMode) : "";
16093
- const streamText = renderFeedbackText(
16094
- renderedStreamText || buildMirrorTitle(title, markdown),
16095
- responseParseMode
16096
- );
16097
- if (getChannelProviderKey(subscription.channelType) === "feishu" && typeof adapter.onStreamEnd === "function") {
16098
- try {
16099
- const finalized = await adapter.onStreamEnd(
16100
- subscription.chatId,
16101
- turn.status,
16102
- streamText,
16103
- turn.streamKey
16104
- );
16105
- if (finalized) {
16106
- subscription.lastDeliveredAt = turn.timestamp || nowIso3();
16107
- return;
16108
- }
16109
- } catch (error) {
16110
- console.warn("[bridge-manager] Mirror stream finalize failed:", error instanceof Error ? error.message : error);
16111
- }
16112
- }
16113
- if (!text2) return;
16114
- const response = await deliver(adapter, {
16115
- address: {
16116
- channelType: subscription.channelType,
16117
- chatId: subscription.chatId
16118
- },
16119
- text: text2,
16120
- parseMode: responseParseMode
16121
- }, {
16122
- sessionId: subscription.sessionId,
16123
- dedupKey: `mirror:${subscription.bindingId}:${turn.signature}`
16124
- });
16125
- if (!response.ok) {
16126
- throw new Error(response.error || "mirror delivery failed");
16127
- }
16128
- subscription.lastDeliveredAt = turn.timestamp || nowIso3();
16694
+ MIRROR_FEEDBACK.stopMirrorStreaming(subscription, status);
16129
16695
  }
16130
16696
  async function deliverMirrorTurns(subscription, turns) {
16131
- let deliveredCount = 0;
16132
- for (const turn of turns.slice(0, MIRROR_EVENT_BATCH_LIMIT)) {
16133
- try {
16134
- await deliverMirrorTurn(subscription, turn);
16135
- deliveredCount += 1;
16136
- } catch (error) {
16137
- return { deliveredCount, error };
16138
- }
16139
- }
16140
- return { deliveredCount };
16697
+ return MIRROR_FEEDBACK.deliverMirrorTurns(subscription, turns);
16141
16698
  }
16142
- var MIRROR_TURN_HOOKS = {
16143
- onStreamText: updateMirrorStreaming,
16144
- onStatusProgress: updateMirrorStatusProgress,
16145
- onTaskProgress: updateMirrorTaskProgress,
16146
- onToolProgress: updateMirrorToolProgress
16147
- };
16699
+ var MIRROR_TURN_HOOKS = MIRROR_FEEDBACK.hooks;
16148
16700
  function consumeMirrorRecords2(subscription, records) {
16149
16701
  return consumeMirrorRecords(subscription, records, MIRROR_TURN_HOOKS);
16150
16702
  }
16151
16703
  function flushTimedOutMirrorTurn2(subscription, nowMs = Date.now()) {
16152
- return flushTimedOutMirrorTurn(subscription, MIRROR_IDLE_TIMEOUT_MS, nowMs);
16704
+ if (subscription.pendingTurn?.streamStarted) {
16705
+ return null;
16706
+ }
16707
+ return flushTimedOutMirrorTurn(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs);
16153
16708
  }
16154
16709
  function hasPendingMirrorWork2(subscription) {
16155
16710
  return hasPendingMirrorWork(subscription);
16156
16711
  }
16157
16712
  function consumeBufferedMirrorTurns2(subscription, nowMs = Date.now()) {
16158
- return consumeBufferedMirrorTurns(subscription, MIRROR_IDLE_TIMEOUT_MS, nowMs, MIRROR_TURN_HOOKS);
16713
+ const timeoutMs = subscription.pendingTurn?.streamStarted ? Number.POSITIVE_INFINITY : MIRROR_TURN_BUFFER_TIMEOUT_MS;
16714
+ return consumeBufferedMirrorTurns(subscription, timeoutMs, nowMs, MIRROR_TURN_HOOKS);
16159
16715
  }
16160
16716
  var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16161
16717
  watchDebounceMs: MIRROR_WATCH_DEBOUNCE_MS,
@@ -16167,11 +16723,16 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16167
16723
  describeUnknownError,
16168
16724
  getDesktopSessionByThreadIdSafe,
16169
16725
  syncMirrorSessionStateSafe,
16170
- isMirrorSuppressed: isMirrorSuppressed2,
16171
16726
  filterSuppressedMirrorRecords: filterSuppressedMirrorRecords2,
16172
16727
  observeSessionHealthRecords: (sessionId, threadId, records) => {
16173
16728
  SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
16174
16729
  },
16730
+ routeDesktopRecords: (sessionId, threadId, records) => routeDesktopRecords(
16731
+ sessionId,
16732
+ threadId,
16733
+ records,
16734
+ TURN_COORDINATOR
16735
+ ),
16175
16736
  consumeMirrorRecords: consumeMirrorRecords2,
16176
16737
  flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
16177
16738
  hasPendingMirrorWork: hasPendingMirrorWork2,
@@ -16229,15 +16790,14 @@ async function start() {
16229
16790
  void ADAPTER_RUNTIME.syncConfiguredAdapters({ startLoops: true }).catch((err) => {
16230
16791
  console.error("[bridge-manager] Adapter reconcile failed:", err);
16231
16792
  });
16232
- void INTERACTIVE_RUNTIME.reconcileIdleInteractiveTasks().catch((err) => {
16233
- console.error("[bridge-manager] Interactive idle reminder reconcile failed:", err);
16234
- });
16235
16793
  try {
16236
16794
  SESSION_HEALTH_RUNTIME.reconcileSessionHealth();
16237
- INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState();
16238
16795
  } catch (err) {
16239
16796
  console.error("[bridge-manager] Session health reconcile failed:", describeUnknownError(err));
16240
16797
  }
16798
+ void INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState().catch((err) => {
16799
+ console.error("[bridge-manager] Terminal interactive reconcile failed:", describeUnknownError(err));
16800
+ });
16241
16801
  }, 5e3);
16242
16802
  state.mirrorPollTimer = setInterval(() => {
16243
16803
  void reconcileMirrorSubscriptions().catch((err) => {
@@ -16420,6 +16980,7 @@ async function handleMessage(adapter, msg) {
16420
16980
  try {
16421
16981
  await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
16422
16982
  registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
16983
+ registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
16423
16984
  resetMirrorSessionForInteractiveRun,
16424
16985
  isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
16425
16986
  touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
@@ -16436,8 +16997,10 @@ async function handleMessage(adapter, msg) {
16436
16997
  abortMirrorSuppression: abortMirrorSuppression2,
16437
16998
  settleMirrorSuppression: settleMirrorSuppression2,
16438
16999
  releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
17000
+ releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
16439
17001
  deliverResponse,
16440
- persistSdkSessionUpdate
17002
+ persistSdkSessionUpdate,
17003
+ desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
16441
17004
  });
16442
17005
  } finally {
16443
17006
  ack();
@@ -16749,7 +17312,7 @@ var JsonFileStore = class {
16749
17312
  findSessionBySdkSessionId(sdkSessionId) {
16750
17313
  this.reloadSessions();
16751
17314
  for (const session of this.sessions.values()) {
16752
- if (session.sdk_session_id === sdkSessionId) {
17315
+ if (session.sdk_session_id === sdkSessionId || session.codex_thread_id === sdkSessionId || session.desktop_thread_id === sdkSessionId) {
16753
17316
  return session;
16754
17317
  }
16755
17318
  }
@@ -16786,7 +17349,7 @@ var JsonFileStore = class {
16786
17349
  this.persistSessions();
16787
17350
  }
16788
17351
  }
16789
- updateSession(sessionId, updates) {
17352
+ updateSession(sessionId, updates, options) {
16790
17353
  this.reloadSessions();
16791
17354
  const session = this.sessions.get(sessionId);
16792
17355
  if (!session) return;
@@ -16794,7 +17357,7 @@ var JsonFileStore = class {
16794
17357
  ...session,
16795
17358
  ...updates,
16796
17359
  id: session.id,
16797
- updated_at: now()
17360
+ updated_at: options?.touch === false ? session.updated_at : now()
16798
17361
  };
16799
17362
  this.sessions.set(sessionId, next);
16800
17363
  this.persistSessions();
@@ -16883,6 +17446,15 @@ var JsonFileStore = class {
16883
17446
  const s = this.sessions.get(sessionId);
16884
17447
  if (s) {
16885
17448
  s.sdk_session_id = sdkSessionId;
17449
+ if (sdkSessionId) {
17450
+ s.codex_thread_id = sdkSessionId;
17451
+ s.thread_origin = s.thread_origin || "bridge";
17452
+ } else {
17453
+ delete s.codex_thread_id;
17454
+ if (s.thread_origin !== "desktop") {
17455
+ delete s.thread_origin;
17456
+ }
17457
+ }
16886
17458
  s.updated_at = now();
16887
17459
  this.persistSessions();
16888
17460
  }