codex-to-im 1.0.44 → 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
@@ -10473,6 +10473,18 @@ function readDesktopSessionEventStream(threadId) {
10473
10473
  return readDesktopSessionEventStreamByFilePath(session.filePath);
10474
10474
  }
10475
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
+
10476
10488
  // src/session-bindings.ts
10477
10489
  function asChannelProvider(value) {
10478
10490
  return value === "feishu" || value === "weixin" ? value : void 0;
@@ -10518,15 +10530,31 @@ function assertBindingTargetAvailable(store, current, opts) {
10518
10530
  function getSessionMode(store, session) {
10519
10531
  return session.preferred_mode || store.getSetting("bridge_default_mode") || "code";
10520
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
+ }
10521
10545
  function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10522
10546
  const session = store.getSession(sessionId);
10523
10547
  if (!session) return null;
10524
10548
  assertBindingTargetAvailable(
10525
10549
  store,
10526
10550
  { channelType, chatId },
10527
- { sessionId: session.id, sdkSessionId: session.sdk_session_id || void 0 }
10551
+ {
10552
+ sessionId: session.id,
10553
+ sdkSessionId: getCodexThreadId(session) || getExplicitDesktopThreadId(session)
10554
+ }
10528
10555
  );
10529
10556
  const meta = resolveChannelMeta(channelType);
10557
+ const sdkSessionId = getBindingResumeThreadId(session);
10530
10558
  return store.upsertChannelBinding({
10531
10559
  channelType,
10532
10560
  channelProvider: meta.provider,
@@ -10535,7 +10563,7 @@ function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10535
10563
  chatUserId: chatMeta?.chatUserId,
10536
10564
  chatDisplayName: chatMeta?.chatDisplayName,
10537
10565
  codepilotSessionId: session.id,
10538
- sdkSessionId: session.sdk_session_id || "",
10566
+ sdkSessionId,
10539
10567
  workingDirectory: session.working_directory,
10540
10568
  model: session.model,
10541
10569
  mode: getSessionMode(store, session)
@@ -10550,6 +10578,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10550
10578
  const meta = resolveChannelMeta(channelType);
10551
10579
  const existing = store.findSessionBySdkSessionId(sdkSessionId);
10552
10580
  if (existing) {
10581
+ markSessionAsDesktopBacked(store, existing.id, sdkSessionId);
10553
10582
  return store.upsertChannelBinding({
10554
10583
  channelType,
10555
10584
  channelProvider: meta.provider,
@@ -10574,7 +10603,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10574
10603
  workingDirectory,
10575
10604
  "code"
10576
10605
  );
10577
- store.updateSdkSessionId(session.id, sdkSessionId);
10606
+ markSessionAsDesktopBacked(store, session.id, sdkSessionId);
10578
10607
  return store.upsertChannelBinding({
10579
10608
  channelType,
10580
10609
  channelProvider: meta.provider,
@@ -10947,15 +10976,17 @@ async function deliver(adapter, message, opts) {
10947
10976
  } catch {
10948
10977
  }
10949
10978
  }
10950
- try {
10951
- store.insertAuditLog({
10952
- channelType: adapter.channelType,
10953
- chatId: message.address.chatId,
10954
- direction: "outbound",
10955
- messageId: lastMessageId || "",
10956
- summary: message.text.slice(0, 200)
10957
- });
10958
- } 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
+ }
10959
10990
  }
10960
10991
  return { ok: true, messageId: lastMessageId };
10961
10992
  }
@@ -11552,7 +11583,6 @@ function buildHealthCommandResponse(title, diagnosis, markdown = false) {
11552
11583
  title,
11553
11584
  [
11554
11585
  ["Session", diagnosis.sessionId],
11555
- ["\u68C0\u67E5\u65F6\u95F4", formatCommandTimestamp(diagnosis.checkedAt)],
11556
11586
  ["\u8FD0\u884C\u72B6\u6001", formatRuntimeStatus({ runtime_status: diagnosis.runtimeStatus, queued_count: 0 })],
11557
11587
  ["\u5065\u5EB7\u72B6\u6001", formatHealthStatusLabel(diagnosis.healthStatus)],
11558
11588
  ["\u5F53\u524D\u9636\u6BB5", currentStage],
@@ -11573,7 +11603,6 @@ function buildHealthListResponse(diagnoses, markdown = false) {
11573
11603
  diagnoses.map((diagnosis) => ({
11574
11604
  heading: diagnosis.sessionId,
11575
11605
  details: [
11576
- `\u68C0\u67E5\u65F6\u95F4\uFF1A${formatCommandTimestamp(diagnosis.checkedAt)}`,
11577
11606
  `\u5065\u5EB7\u72B6\u6001\uFF1A${formatHealthStatusLabel(diagnosis.healthStatus)}`,
11578
11607
  `\u5F53\u524D\u9636\u6BB5\uFF1A${diagnosis.activeToolName ? `\u5DE5\u5177 \xB7 ${diagnosis.activeToolName}` : diagnosis.lastProgressType || "-"}`,
11579
11608
  `\u6700\u540E\u8FDB\u5C55\uFF1A${formatCommandTimestamp(diagnosis.lastProgressAt)}`,
@@ -11681,6 +11710,8 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
11681
11710
  streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
11682
11711
  startedAt: safeTimestamp,
11683
11712
  lastActivityAt: safeTimestamp,
11713
+ lastContentResponseAt: null,
11714
+ lastResponseAt: null,
11684
11715
  lastStatusText: null,
11685
11716
  lastStatusAt: 0,
11686
11717
  statusNote: null,
@@ -11727,8 +11758,14 @@ function ensureMirrorTurnState(subscription, record) {
11727
11758
  }
11728
11759
  return subscription.pendingTurn;
11729
11760
  }
11730
- function markMirrorResponse(turnState, timestamp) {
11731
- 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;
11732
11769
  }
11733
11770
  function finalizeMirrorTurn(subscription, signature, timestamp, status, preferredText) {
11734
11771
  const pendingTurn = subscription.pendingTurn;
@@ -11801,7 +11838,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11801
11838
  if (text2) {
11802
11839
  pendingTurn.lastAssistantText = text2;
11803
11840
  appendMirrorStreamText(pendingTurn, text2);
11804
- markMirrorResponse(pendingTurn, record.timestamp);
11841
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11805
11842
  hooks.onStreamText?.(subscription, pendingTurn);
11806
11843
  }
11807
11844
  } else if (record.role === "commentary") {
@@ -11809,7 +11846,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11809
11846
  if (text2) {
11810
11847
  pendingTurn.lastCommentaryText = text2;
11811
11848
  appendMirrorStreamText(pendingTurn, text2);
11812
- markMirrorResponse(pendingTurn, record.timestamp);
11849
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11813
11850
  hooks.onStreamText?.(subscription, pendingTurn);
11814
11851
  }
11815
11852
  }
@@ -11820,14 +11857,14 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11820
11857
  const text2 = record.content.trim();
11821
11858
  if (!text2) continue;
11822
11859
  pendingTurn.statusNote = text2;
11823
- markMirrorResponse(pendingTurn, record.timestamp);
11860
+ markMirrorActivity(pendingTurn, record.timestamp);
11824
11861
  hooks.onStatusProgress?.(subscription, pendingTurn);
11825
11862
  continue;
11826
11863
  }
11827
11864
  if (record.type === "plan_update") {
11828
11865
  const pendingTurn = ensureMirrorTurnState(subscription, record);
11829
11866
  pendingTurn.taskItems = record.tasks || [];
11830
- markMirrorResponse(pendingTurn, record.timestamp);
11867
+ markMirrorActivity(pendingTurn, record.timestamp);
11831
11868
  hooks.onTaskProgress?.(subscription, pendingTurn);
11832
11869
  continue;
11833
11870
  }
@@ -11840,7 +11877,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11840
11877
  name: toolName,
11841
11878
  status: "running"
11842
11879
  });
11843
- markMirrorResponse(pendingTurn, record.timestamp);
11880
+ markMirrorActivity(pendingTurn, record.timestamp);
11844
11881
  hooks.onToolProgress?.(subscription, pendingTurn);
11845
11882
  continue;
11846
11883
  }
@@ -11853,7 +11890,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11853
11890
  name: existing?.name || record.toolName || "tool",
11854
11891
  status: record.isError ? "error" : "complete"
11855
11892
  });
11856
- markMirrorResponse(pendingTurn, record.timestamp);
11893
+ markMirrorActivity(pendingTurn, record.timestamp);
11857
11894
  hooks.onToolProgress?.(subscription, pendingTurn);
11858
11895
  continue;
11859
11896
  }
@@ -12022,9 +12059,6 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
12022
12059
  }
12023
12060
  target.until = nowMs + config2.suppressionWindowMs;
12024
12061
  }
12025
- function isMirrorSuppressed(store, sessionId, nowMs = Date.now()) {
12026
- return getMirrorSuppressionStates(store, sessionId, nowMs).length > 0;
12027
- }
12028
12062
  function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now()) {
12029
12063
  const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
12030
12064
  const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
@@ -12661,7 +12695,7 @@ function supportsOutboundArtifacts(provider) {
12661
12695
  }
12662
12696
 
12663
12697
  // src/lib/bridge/feedback-delivery.ts
12664
- async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId) {
12698
+ async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId, options) {
12665
12699
  if (!responseText.trim()) return { ok: true };
12666
12700
  const parseMode = getFeedbackParseMode(adapter.channelType);
12667
12701
  const renderedText = renderFeedbackText(responseText, parseMode);
@@ -12671,14 +12705,14 @@ async function deliverTextResponse(adapter, address, responseText, sessionId, re
12671
12705
  text: responseText,
12672
12706
  parseMode: "Markdown",
12673
12707
  replyToMessageId
12674
- }, { sessionId });
12708
+ }, { sessionId, audit: options?.audit });
12675
12709
  }
12676
12710
  return deliver(adapter, {
12677
12711
  address,
12678
12712
  text: parseMode === "Markdown" ? responseText : renderedText,
12679
12713
  parseMode,
12680
12714
  replyToMessageId
12681
- }, { sessionId });
12715
+ }, { sessionId, audit: options?.audit });
12682
12716
  }
12683
12717
  async function deliverBridgeNotice(adapter, address, text2, options) {
12684
12718
  return deliverTextResponse(
@@ -12686,7 +12720,8 @@ async function deliverBridgeNotice(adapter, address, text2, options) {
12686
12720
  address,
12687
12721
  text2,
12688
12722
  options?.sessionId,
12689
- options?.replyToMessageId
12723
+ options?.replyToMessageId,
12724
+ { audit: options?.audit }
12690
12725
  );
12691
12726
  }
12692
12727
  async function deliverResponse(adapter, address, responseText, sessionId, replyToMessageId, attachments = []) {
@@ -12802,6 +12837,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
12802
12837
  }
12803
12838
  let response = "";
12804
12839
  let responseParseMode = getFeedbackParseMode(adapter.channelType);
12840
+ let auditResponse = true;
12805
12841
  const currentBinding = store.getChannelBinding(msg.address.channelType, msg.address.chatId);
12806
12842
  switch (command) {
12807
12843
  case "/start":
@@ -13134,6 +13170,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13134
13170
  break;
13135
13171
  }
13136
13172
  if (!args) {
13173
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13137
13174
  const currentModel = resolveDisplayedModel(
13138
13175
  binding,
13139
13176
  session,
@@ -13145,14 +13182,14 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13145
13182
  [["\u6A21\u578B", formatDisplayedModel(currentModel)]],
13146
13183
  [
13147
13184
  getAvailableModelChoicesText(),
13148
- 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",
13149
13186
  "\u6A21\u578B\u5207\u6362\u53EA\u5F71\u54CD\u540E\u7EED\u4ECE IM \u53D1\u8D77\u7684 Codex CLI \u8BF7\u6C42\u3002"
13150
13187
  ],
13151
13188
  responseParseMode === "Markdown"
13152
13189
  );
13153
13190
  break;
13154
13191
  }
13155
- if (binding.sdkSessionId) {
13192
+ if (getExplicitDesktopThreadId(session)) {
13156
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";
13157
13194
  break;
13158
13195
  }
@@ -13203,9 +13240,32 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13203
13240
  break;
13204
13241
  }
13205
13242
  case "/status": {
13206
- 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
+ }
13207
13254
  const session = store.getSession(binding.codepilotSessionId);
13208
- 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);
13209
13269
  const sandboxMode = resolveEffectiveSandboxMode();
13210
13270
  const reasoningEffort = resolveEffectiveReasoningEffort(session);
13211
13271
  const currentModel = resolveDisplayedModel(
@@ -13229,21 +13289,25 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13229
13289
  ["\u601D\u8003\u7EA7\u522B", formatReasoningEffort(reasoningEffort)]
13230
13290
  ],
13231
13291
  [
13232
- 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"
13233
13293
  ],
13234
13294
  responseParseMode === "Markdown"
13235
13295
  );
13236
13296
  break;
13237
13297
  }
13238
13298
  case "/health": {
13299
+ auditResponse = false;
13239
13300
  if (args === "all") {
13240
13301
  const diagnoses = await deps.diagnoseAllActiveSessions();
13241
13302
  response = diagnoses.length > 0 ? buildHealthListResponse(diagnoses, responseParseMode === "Markdown") : "\u5F53\u524D\u6CA1\u6709\u68C0\u6D4B\u5230\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002";
13242
13303
  break;
13243
13304
  }
13244
- const binding = currentBinding || resolve(msg.address);
13245
13305
  const explicitTargetSessionId = args.trim();
13246
- const targetSessionId = explicitTargetSessionId || binding.codepilotSessionId;
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
+ }
13247
13311
  const diagnosis = await deps.diagnoseSessionHealth(targetSessionId);
13248
13312
  if (!diagnosis) {
13249
13313
  response = `\u6CA1\u6709\u627E\u5230\u4F1A\u8BDD ${targetSessionId}\u3002`;
@@ -13262,15 +13326,16 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13262
13326
  break;
13263
13327
  }
13264
13328
  const limit = getHistoryMessageLimit();
13265
- 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) : [];
13266
13332
  const { messages: storedMessages } = store.getMessages(currentBinding.codepilotSessionId, { limit });
13267
13333
  const messages = desktopMessages.length > 0 ? desktopMessages : storedMessages;
13268
13334
  if (messages.length === 0) {
13269
13335
  response = "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u5386\u53F2\u6D88\u606F\u3002";
13270
13336
  break;
13271
13337
  }
13272
- const threadTitle = getDesktopThreadTitle(currentBinding.sdkSessionId);
13273
- const session = store.getSession(currentBinding.codepilotSessionId);
13338
+ const threadTitle = getDesktopThreadTitle(desktopThreadId);
13274
13339
  const header = buildCommandFields(
13275
13340
  "\u6700\u8FD1\u5BF9\u8BDD\uFF08raw\uFF09",
13276
13341
  [
@@ -13402,7 +13467,8 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13402
13467
  }
13403
13468
  if (response) {
13404
13469
  await deliverBridgeNotice(adapter, msg.address, response, {
13405
- replyToMessageId: msg.messageId
13470
+ replyToMessageId: msg.messageId,
13471
+ audit: auditResponse
13406
13472
  });
13407
13473
  }
13408
13474
  }
@@ -13414,6 +13480,39 @@ import path13 from "node:path";
13414
13480
  import fs8 from "fs";
13415
13481
  import path12 from "path";
13416
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
13417
13516
  init_runtime_options();
13418
13517
 
13419
13518
  // src/lib/bridge/sse-stream-decoder.ts
@@ -13792,8 +13891,8 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13792
13891
  if (contentBlocks.length > 0) {
13793
13892
  for (const block2 of contentBlocks) {
13794
13893
  if (block2.type !== "text") continue;
13795
- const parsed = parseOutboundArtifacts(block2.text);
13796
- block2.text = parsed.cleanText;
13894
+ const parsed = collectFinalResponseArtifacts(block2.text);
13895
+ block2.text = parsed.text;
13797
13896
  outboundAttachments.push(...parsed.attachments);
13798
13897
  }
13799
13898
  const hasToolBlocks = contentBlocks.some(
@@ -13807,7 +13906,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13807
13906
  const responseText = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
13808
13907
  return {
13809
13908
  responseText,
13810
- outboundAttachments,
13909
+ outboundAttachments: dedupeOutboundAttachments(outboundAttachments),
13811
13910
  tokenUsage,
13812
13911
  hasError,
13813
13912
  errorMessage,
@@ -13840,6 +13939,42 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13840
13939
  }
13841
13940
  }
13842
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
+
13843
13978
  // src/lib/bridge/stream-feedback-controller.ts
13844
13979
  function pushStreamFeedbackText(target, text2) {
13845
13980
  if (typeof target.adapter.onStreamText !== "function") return;
@@ -13868,13 +14003,15 @@ function pushStreamFeedbackTasks(target, tasks) {
13868
14003
  }
13869
14004
  }
13870
14005
  function pushStreamFeedbackStatus(target, text2) {
13871
- if (typeof target.adapter.onStreamStatus !== "function") return;
14006
+ if (typeof target.adapter.onStreamStatus !== "function") return false;
13872
14007
  target.ensureStarted?.();
13873
14008
  const rendered = renderFeedbackTextForChannel(target.channelType, text2);
13874
- if (!rendered) return;
14009
+ if (!rendered) return false;
13875
14010
  try {
13876
14011
  target.adapter.onStreamStatus(target.chatId, rendered, target.streamKey);
14012
+ return true;
13877
14013
  } catch {
14014
+ return false;
13878
14015
  }
13879
14016
  }
13880
14017
  async function finalizeStreamFeedback(target, status, text2) {
@@ -13887,6 +14024,118 @@ async function finalizeStreamFeedback(target, status, text2) {
13887
14024
  }
13888
14025
  }
13889
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
+
13890
14139
  // src/lib/bridge/interactive-message-runner.ts
13891
14140
  function generateDraftId() {
13892
14141
  return Math.floor(Math.random() * 2147483646) + 1;
@@ -13930,18 +14179,6 @@ function flushPreview(adapter, state, config2) {
13930
14179
  }).catch(() => {
13931
14180
  });
13932
14181
  }
13933
- function formatRuntimeDuration(ms) {
13934
- const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
13935
- const seconds = totalSeconds % 60;
13936
- const totalMinutes = Math.floor(totalSeconds / 60);
13937
- const minutes = totalMinutes % 60;
13938
- const hours = Math.floor(totalMinutes / 60);
13939
- const parts = [];
13940
- if (hours > 0) parts.push(`${hours}\u5C0F\u65F6`);
13941
- if (minutes > 0) parts.push(`${minutes}\u5206`);
13942
- if (seconds > 0 || parts.length === 0) parts.push(`${seconds}\u79D2`);
13943
- return parts.join("");
13944
- }
13945
14182
  function pathBaseName(value) {
13946
14183
  return value.includes("\\") ? path13.win32.basename(value) : path13.basename(value);
13947
14184
  }
@@ -13963,18 +14200,10 @@ function buildStaleTaskCompletionNotice(address, binding) {
13963
14200
  const taskName = formatTaskDisplayName(binding);
13964
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`;
13965
14202
  }
13966
- function formatInteractiveRuntimeStatus(elapsedMs, lastResponseAgeMs, statusNote) {
13967
- const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
13968
- if (typeof lastResponseAgeMs === "number" && lastResponseAgeMs >= 0) {
13969
- parts.push(`\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA ${formatRuntimeDuration(lastResponseAgeMs)}`);
13970
- }
13971
- const runtimeText = parts.join("\uFF0C");
13972
- const note = (statusNote || "").trim();
13973
- return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
13974
- ${runtimeText}` : runtimeText;
13975
- }
13976
14203
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13977
14204
  const binding = resolve(msg.address);
14205
+ const initialSession = getBridgeContext().store.getSession(binding.codepilotSessionId);
14206
+ const desktopThreadId = getExplicitDesktopThreadId(initialSession);
13978
14207
  const streamKey = buildInteractiveStreamKey(binding.codepilotSessionId, msg.messageId);
13979
14208
  const nowMs = deps.nowMs ?? (() => Date.now());
13980
14209
  const setIntervalFn = deps.setIntervalFn ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
@@ -14000,11 +14229,14 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14000
14229
  const taskAbort = new AbortController();
14001
14230
  const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
14002
14231
  const taskStartedAt = nowMs();
14232
+ const streamState = createStreamState(taskStartedAt);
14003
14233
  let externalTerminalRequest = null;
14234
+ let desktopTerminalFinalExpected = false;
14004
14235
  let resolveExternalTerminal = null;
14005
14236
  const externalTerminalPromise = new Promise((resolve2) => {
14006
14237
  resolveExternalTerminal = resolve2;
14007
14238
  });
14239
+ let processResultSettled = false;
14008
14240
  let resolveExternalTerminalCompletion = null;
14009
14241
  let externalTerminalCompletionSettled = false;
14010
14242
  const externalTerminalCompletion = new Promise((resolve2) => {
@@ -14028,6 +14260,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14028
14260
  structuredStreamUiActive: false,
14029
14261
  lastActivityAt: taskStartedAt,
14030
14262
  lastResponseAt: null,
14263
+ lastContentResponseAt: null,
14031
14264
  streamFinalized: false,
14032
14265
  uiEnded: false,
14033
14266
  mirrorSuppressionId: null,
@@ -14036,13 +14269,26 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14036
14269
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return false;
14037
14270
  externalTerminalRequest = { outcome, detail, finalText };
14038
14271
  resolveExternalTerminal?.(externalTerminalRequest);
14039
- if (!taskAbort.signal.aborted) {
14272
+ if (!processResultSettled && !taskAbort.signal.aborted) {
14040
14273
  taskAbort.abort();
14041
14274
  }
14042
14275
  return externalTerminalCompletion;
14043
14276
  }
14044
14277
  };
14045
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
+ });
14046
14292
  deps.recordInteractiveHealthStart(binding.codepilotSessionId);
14047
14293
  let previewState = null;
14048
14294
  const caps = adapter.getPreviewCapabilities?.(msg.address.chatId) ?? null;
@@ -14062,7 +14308,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14062
14308
  const ps = previewState;
14063
14309
  const cfg = streamCfg;
14064
14310
  if (ps.degraded) return;
14065
- const sanitizedText = stripOutboundArtifactBlocksForStreaming(fullText);
14311
+ const sanitizedText = stripFinalOnlyBlocksForStreaming(fullText);
14066
14312
  ps.pendingText = sanitizedText.length > cfg.maxChars ? sanitizedText.slice(0, cfg.maxChars) + "..." : sanitizedText;
14067
14313
  const delta = ps.pendingText.length - ps.lastSentText.length;
14068
14314
  const elapsed = Date.now() - ps.lastSentAt;
@@ -14104,7 +14350,6 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14104
14350
  };
14105
14351
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
14106
14352
  const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
14107
- let latestStatusNote = null;
14108
14353
  let latestTasks = [];
14109
14354
  const syncStructuredStreamUiState = () => {
14110
14355
  if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
@@ -14123,12 +14368,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14123
14368
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
14124
14369
  pushStreamFeedbackStatus(
14125
14370
  streamFeedbackTarget,
14126
- formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, latestStatusNote)
14371
+ lastResponseAgeMs == null ? buildStreamRuntimeStatus(streamState, nowMs()) : formatStreamRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, streamState.statusNote)
14127
14372
  );
14128
14373
  syncStructuredStreamUiSnapshot();
14129
14374
  };
14130
- const markSuccessfulResponse = () => {
14131
- 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;
14132
14387
  deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
14133
14388
  };
14134
14389
  let streamStatusHeartbeat = null;
@@ -14167,10 +14422,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14167
14422
  endPreviewOnce();
14168
14423
  if (hasStreamingCards && !streamUiFinalizeAttempted) {
14169
14424
  streamUiFinalizeAttempted = true;
14170
- taskState.streamFinalized = await finalizeStreamFeedback(
14425
+ taskState.streamFinalized = await finalizeStreamingUi(
14171
14426
  streamFeedbackTarget,
14172
14427
  status,
14173
- responseText
14428
+ assembleDesktopFinalResponse({ text: responseText })
14174
14429
  );
14175
14430
  }
14176
14431
  return taskState.streamFinalized;
@@ -14184,12 +14439,12 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14184
14439
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14185
14440
  pushStreamFeedbackText(
14186
14441
  streamFeedbackTarget,
14187
- stripOutboundArtifactBlocksForStreaming(fullText)
14442
+ stripFinalOnlyBlocksForStreaming(fullText)
14188
14443
  );
14189
14444
  } : void 0;
14190
14445
  const onToolEvent = (toolId, toolName, status) => {
14191
14446
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14192
- markSuccessfulResponse();
14447
+ markActivity();
14193
14448
  deps.recordInteractiveHealthTool(binding.codepilotSessionId, toolId, toolName, status);
14194
14449
  if (toolName) {
14195
14450
  toolCallTracker.set(toolId, { id: toolId, name: toolName, status });
@@ -14205,7 +14460,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14205
14460
  };
14206
14461
  const onTaskEvent = (tasks) => {
14207
14462
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14208
- markSuccessfulResponse();
14463
+ markActivity();
14209
14464
  latestTasks = tasks;
14210
14465
  if (hasStreamingCards) {
14211
14466
  pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
@@ -14215,17 +14470,15 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14215
14470
  };
14216
14471
  const onStatusNote = (note) => {
14217
14472
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14218
- latestStatusNote = (note || "").trim() || null;
14219
- if (latestStatusNote) {
14220
- markSuccessfulResponse();
14221
- }
14473
+ updateStreamStatusNote(streamState, note, nowMs());
14474
+ if (streamState.statusNote) markActivity();
14222
14475
  pushRunningStatus(null);
14223
14476
  syncStructuredStreamUiSnapshot();
14224
14477
  };
14225
14478
  const onPartialText = (fullText) => {
14226
14479
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14227
14480
  if (fullText.trim()) {
14228
- markSuccessfulResponse();
14481
+ markContentResponse();
14229
14482
  }
14230
14483
  deps.recordInteractiveHealthProgress(binding.codepilotSessionId, "text");
14231
14484
  previewOnPartialText?.(fullText);
@@ -14233,6 +14486,34 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14233
14486
  pushRunningStatus(null);
14234
14487
  syncStructuredStreamUiSnapshot();
14235
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
+ };
14236
14517
  if (supportsStructuredStreamUi) {
14237
14518
  pushRunningStatus(null);
14238
14519
  streamStatusHeartbeat = setIntervalFn(() => {
@@ -14245,8 +14526,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14245
14526
  return;
14246
14527
  }
14247
14528
  const elapsedMs = nowMs() - taskStartedAt;
14248
- const lastResponseAgeMs = taskState.lastResponseAt == null ? null : nowMs() - taskState.lastResponseAt;
14249
- 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;
14250
14533
  pushRunningStatus(showLastResponseAge);
14251
14534
  syncStructuredStreamUiSnapshot();
14252
14535
  }, streamStatusHeartbeatMs);
@@ -14275,7 +14558,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14275
14558
  "permission_wait",
14276
14559
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
14277
14560
  );
14278
- markSuccessfulResponse();
14561
+ markActivity();
14279
14562
  pushRunningStatus(null);
14280
14563
  syncStructuredStreamUiSnapshot();
14281
14564
  },
@@ -14286,7 +14569,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14286
14569
  onTaskEvent,
14287
14570
  onStatusNote,
14288
14571
  (preparedPrompt) => {
14289
- if (!taskState.mirrorSuppressionId) {
14572
+ if (desktopThreadId) {
14573
+ desktopTerminalFinalExpected = true;
14574
+ }
14575
+ if (desktopThreadId && !taskState.mirrorSuppressionId) {
14290
14576
  taskState.mirrorSuppressionId = deps.beginMirrorSuppression(binding.codepilotSessionId, preparedPrompt);
14291
14577
  }
14292
14578
  }
@@ -14308,73 +14594,87 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14308
14594
  finalOutcomeDetail = raced.terminal.detail;
14309
14595
  const streamEndStatus = raced.terminal.outcome === "completed" ? "completed" : raced.terminal.outcome === "aborted" ? "interrupted" : "error";
14310
14596
  const staleTaskNotice2 = buildStaleTaskCompletionNotice(msg.address, binding);
14311
- const terminalText = staleTaskNotice2 || raced.terminal.finalText || "";
14312
- const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalText);
14313
- if (!cardFinalized2 && terminalText) {
14314
- await deps.deliverResponse(
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({
14315
14603
  adapter,
14316
- msg.address,
14317
- terminalText,
14318
- binding.codepilotSessionId,
14319
- msg.messageId,
14320
- []
14321
- );
14604
+ address: msg.address,
14605
+ sessionId: binding.codepilotSessionId,
14606
+ replyToMessageId: msg.messageId,
14607
+ deliverResponse: deps.deliverResponse
14608
+ }, terminalResponse2, { skipText: cardFinalized2 });
14322
14609
  }
14323
14610
  return;
14324
14611
  }
14325
14612
  const result = raced.result;
14613
+ processResultSettled = true;
14326
14614
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
14327
14615
  shouldRecordHealthEnd = false;
14328
14616
  return;
14329
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;
14330
14630
  let cardFinalized = false;
14331
14631
  const staleTaskNotice = buildStaleTaskCompletionNotice(msg.address, binding);
14632
+ const staleResponse = staleTaskNotice ? assembleDesktopFinalResponse({ text: staleTaskNotice }) : null;
14332
14633
  if (hasStreamingCards) {
14333
- const streamEndStatus = taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14634
+ const streamEndStatus = terminalAfterProcess ? terminalAfterProcess.outcome === "completed" ? "completed" : terminalAfterProcess.outcome === "aborted" ? "interrupted" : "error" : taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14334
14635
  cardFinalized = await finalizeStreamUiOnce(
14335
14636
  streamEndStatus,
14336
- staleTaskNotice || (streamEndStatus === "interrupted" ? "" : result.responseText)
14637
+ staleResponse?.text || (streamEndStatus === "interrupted" ? "" : effectiveResponse.text)
14337
14638
  );
14338
14639
  }
14339
- if (staleTaskNotice) {
14340
- if (!cardFinalized) {
14341
- await deps.deliverResponse(
14342
- adapter,
14343
- msg.address,
14344
- staleTaskNotice,
14345
- binding.codepilotSessionId,
14346
- msg.messageId,
14347
- []
14348
- );
14349
- }
14350
- } else if (result.responseText || result.outboundAttachments.length > 0) {
14351
- const textToDeliver = cardFinalized ? "" : result.responseText;
14352
- if (!cardFinalized || result.outboundAttachments.length > 0) {
14353
- await deps.deliverResponse(
14354
- adapter,
14355
- msg.address,
14356
- textToDeliver,
14357
- binding.codepilotSessionId,
14358
- msg.messageId,
14359
- result.outboundAttachments
14360
- );
14361
- }
14362
- } else if (result.hasError && !taskAbort.signal.aborted) {
14363
- await deps.deliverResponse(
14640
+ if (staleResponse) {
14641
+ await deliverFinalResponse({
14364
14642
  adapter,
14365
- msg.address,
14366
- `**Error:** ${result.errorMessage}`,
14367
- binding.codepilotSessionId,
14368
- msg.messageId,
14369
- []
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
+ })
14370
14670
  );
14371
14671
  }
14372
14672
  try {
14373
14673
  deps.persistSdkSessionUpdate(binding.codepilotSessionId, result.sdkSessionId, result.hasError);
14374
14674
  } catch {
14375
14675
  }
14376
- finalOutcome = result.hasError ? "failed" : "completed";
14377
- 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);
14378
14678
  } finally {
14379
14679
  await finalizeStreamUiOnce(
14380
14680
  taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
@@ -14396,6 +14696,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14396
14696
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14397
14697
  }
14398
14698
  deps.releaseInteractiveTask(binding.codepilotSessionId, taskId);
14699
+ deps.releaseBridgeTurn?.(binding.codepilotSessionId, taskId);
14399
14700
  endMessageUiOnce();
14400
14701
  settleExternalTerminalCompletion(taskState.streamFinalized || !hasStreamingCards);
14401
14702
  }
@@ -14410,12 +14711,6 @@ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
14410
14711
  function isTerminalSessionHealthStatus(status) {
14411
14712
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14412
14713
  }
14413
- function terminalOutcomeFromHealthStatus(status) {
14414
- if (status === "completed") return "completed";
14415
- if (status === "failed") return "failed";
14416
- if (status === "aborted") return "aborted";
14417
- return null;
14418
- }
14419
14714
  function createInteractiveRuntime(getState2, deps) {
14420
14715
  function getQueuedCount(sessionId) {
14421
14716
  return getState2().queuedCounts.get(sessionId) || 0;
@@ -14467,14 +14762,7 @@ function createInteractiveRuntime(getState2, deps) {
14467
14762
  const store = deps.getStore();
14468
14763
  for (const session of store.listSessions()) {
14469
14764
  if (!isTerminalSessionHealthStatus(session.health_status)) continue;
14470
- const activeTask = getState2().activeTasks.get(session.id);
14471
- if (activeTask) {
14472
- const outcome = terminalOutcomeFromHealthStatus(session.health_status);
14473
- if (outcome) {
14474
- await finalizeTerminalActiveTask(session.id, outcome, session.health_reason || void 0);
14475
- }
14476
- continue;
14477
- }
14765
+ if (getState2().activeTasks.has(session.id)) continue;
14478
14766
  const queuedCount = getQueuedCount(session.id);
14479
14767
  const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
14480
14768
  if (queuedCount > 0) continue;
@@ -14887,7 +15175,7 @@ function buildMirrorSubscriptionRegistryPlan(bindings, activeChannelTypes, exist
14887
15175
  if (binding.active === false) return false;
14888
15176
  if (!activeChannels.has(binding.channelType)) return false;
14889
15177
  const session = getSession(binding.codepilotSessionId);
14890
- return Boolean(binding.sdkSessionId || session?.sdk_session_id);
15178
+ return Boolean(session?.desktop_thread_id || (session?.thread_origin === "desktop" ? session.sdk_session_id : null));
14891
15179
  });
14892
15180
  const desiredIds = new Set(upsertBindings.map((binding) => binding.id));
14893
15181
  const removeBindingIds = Array.from(existingBindingIds).filter((bindingId) => !desiredIds.has(bindingId));
@@ -14981,11 +15269,17 @@ function createMirrorRuntime(getState2, options, deps) {
14981
15269
  function clearDanglingMirrorThread(subscription, reason) {
14982
15270
  const { store } = getBridgeContext();
14983
15271
  const session = store.getSession(subscription.sessionId);
14984
- const currentThreadId = session?.sdk_session_id || subscription.threadId;
15272
+ const currentThreadId = getExplicitDesktopThreadId(session) || subscription.threadId;
14985
15273
  console.warn(
14986
15274
  `[bridge-manager] Clearing dangling desktop thread ${currentThreadId} for session ${subscription.sessionId}: ${reason}`
14987
15275
  );
14988
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
+ });
14989
15283
  removeMirrorSubscription(subscription.bindingId);
14990
15284
  }
14991
15285
  function upsertMirrorSubscription(binding) {
@@ -14996,7 +15290,7 @@ function createMirrorRuntime(getState2, options, deps) {
14996
15290
  removeMirrorSubscription(binding.id);
14997
15291
  return;
14998
15292
  }
14999
- const threadId = binding.sdkSessionId || session.sdk_session_id || "";
15293
+ const threadId = getExplicitDesktopThreadId(session) || "";
15000
15294
  if (!threadId) {
15001
15295
  removeMirrorSubscription(binding.id);
15002
15296
  return;
@@ -15109,11 +15403,13 @@ function createMirrorRuntime(getState2, options, deps) {
15109
15403
  `[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
15110
15404
  );
15111
15405
  }
15112
- if (deliverableRecords.length > 0) {
15113
- 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);
15114
15410
  }
15115
- const blocked = getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId);
15116
- const deliveryPlan = buildMirrorDeliveryPlan(subscription, deliverableRecords, {
15411
+ const blocked = getState2().activeTasks.has(subscription.sessionId);
15412
+ const deliveryPlan = buildMirrorDeliveryPlan(subscription, mirrorRecords, {
15117
15413
  blocked,
15118
15414
  filterSuppressedRecords: deps.filterSuppressedMirrorRecords,
15119
15415
  flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
@@ -15210,6 +15506,241 @@ function createMirrorRuntime(getState2, options, deps) {
15210
15506
  };
15211
15507
  }
15212
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
+
15213
15744
  // src/lib/bridge/session-health-process.ts
15214
15745
  import { execFile } from "node:child_process";
15215
15746
  import { promisify } from "node:util";
@@ -15302,7 +15833,6 @@ async function probeCodexThreadProcess(threadId) {
15302
15833
  var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
15303
15834
  var HEALTH_SLOW_OBSERVED_MS = 30 * 60 * 1e3;
15304
15835
  var HEALTH_PROGRESS_PERSIST_THROTTLE_MS = 15 * 1e3;
15305
- var HEALTH_PROCESS_PROBE_CACHE_MS = 30 * 1e3;
15306
15836
  var HEALTH_STREAM_UI_STALL_MS = 60 * 1e3;
15307
15837
  var RUNNING_HEALTH_STATUSES = /* @__PURE__ */ new Set([
15308
15838
  "running_active",
@@ -15435,14 +15965,13 @@ function computeBaseDiagnosis(session, nowMs) {
15435
15965
  const lastStreamUiError = trimOrNull(session.last_stream_ui_error);
15436
15966
  const streamUiConsecutiveFailures = typeof session.stream_ui_consecutive_failures === "number" && Number.isFinite(session.stream_ui_consecutive_failures) && session.stream_ui_consecutive_failures > 0 ? session.stream_ui_consecutive_failures : 0;
15437
15967
  const sdkSessionId = trimOrNull(session.sdk_session_id);
15438
- const checkedAt = trimOrNull(session.last_health_check_at);
15439
15968
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
15440
15969
  const previousStatus = session.health_status || "idle";
15441
15970
  if (!lastProgressMs) {
15442
15971
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15443
15972
  return {
15444
15973
  sessionId: session.id,
15445
- checkedAt,
15974
+ checkedAt: null,
15446
15975
  runtimeStatus,
15447
15976
  healthStatus: fallbackStatus,
15448
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"),
@@ -15480,7 +16009,7 @@ function computeBaseDiagnosis(session, nowMs) {
15480
16009
  }
15481
16010
  return {
15482
16011
  sessionId: session.id,
15483
- checkedAt,
16012
+ checkedAt: null,
15484
16013
  runtimeStatus,
15485
16014
  healthStatus,
15486
16015
  healthReason,
@@ -15589,7 +16118,6 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15589
16118
  // src/lib/bridge/session-health-runtime.ts
15590
16119
  function createSessionHealthRuntime(deps) {
15591
16120
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
15592
- const processProbeCache = /* @__PURE__ */ new Map();
15593
16121
  function summarizePlanUpdate(tasks) {
15594
16122
  if (!Array.isArray(tasks) || tasks.length === 0) {
15595
16123
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
@@ -15725,11 +16253,9 @@ function createSessionHealthRuntime(deps) {
15725
16253
  last_stream_ui_update_at: void 0,
15726
16254
  last_stream_ui_error_at: void 0,
15727
16255
  last_stream_ui_error: void 0,
15728
- stream_ui_consecutive_failures: void 0,
15729
- last_health_check_at: nowIso4
16256
+ stream_ui_consecutive_failures: void 0
15730
16257
  }, { force: true });
15731
16258
  lastProgressPersistAt.set(sessionId, Date.now());
15732
- processProbeCache.delete(sessionId);
15733
16259
  }
15734
16260
  function toIso(value) {
15735
16261
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
@@ -15824,17 +16350,7 @@ function createSessionHealthRuntime(deps) {
15824
16350
  async function loadProcessProbe(session) {
15825
16351
  const threadId = session.sdk_session_id?.trim();
15826
16352
  if (!threadId || !deps.probeThreadProcess) return null;
15827
- const cached = processProbeCache.get(session.id);
15828
- const nowMs = Date.now();
15829
- if (cached && nowMs - cached.checkedAtMs < HEALTH_PROCESS_PROBE_CACHE_MS) {
15830
- return cached.result;
15831
- }
15832
- const result = await deps.probeThreadProcess(threadId);
15833
- processProbeCache.set(session.id, {
15834
- checkedAtMs: nowMs,
15835
- result
15836
- });
15837
- return result;
16353
+ return deps.probeThreadProcess(threadId);
15838
16354
  }
15839
16355
  async function diagnoseSessionHealth(sessionId) {
15840
16356
  const store = deps.getStore();
@@ -15842,21 +16358,10 @@ function createSessionHealthRuntime(deps) {
15842
16358
  if (!session) return null;
15843
16359
  const base = computeBaseDiagnosis(session, Date.now());
15844
16360
  const processProbe = await loadProcessProbe(session);
15845
- const checkedAt = deps.nowIso();
15846
- const diagnosis = applyStreamUiDiagnosis(
16361
+ return applyStreamUiDiagnosis(
15847
16362
  applyProcessProbeDiagnosis(base, processProbe),
15848
16363
  Date.now()
15849
16364
  );
15850
- const checkedDiagnosis = {
15851
- ...diagnosis,
15852
- checkedAt
15853
- };
15854
- updateSessionHealth(sessionId, {
15855
- health_status: checkedDiagnosis.healthStatus,
15856
- health_reason: checkedDiagnosis.healthReason,
15857
- last_health_check_at: checkedAt
15858
- }, { touch: false });
15859
- return checkedDiagnosis;
15860
16365
  }
15861
16366
  async function diagnoseAllActiveSessions() {
15862
16367
  const store = deps.getStore();
@@ -15877,6 +16382,100 @@ function createSessionHealthRuntime(deps) {
15877
16382
  };
15878
16383
  }
15879
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
+
15880
16479
  // src/lib/bridge/bridge-manager.ts
15881
16480
  var GLOBAL_KEY = "__bridge_manager__";
15882
16481
  var DANGLING_MIRROR_THREAD_RETRY_LIMIT = 3;
@@ -15887,6 +16486,7 @@ var MIRROR_WATCH_DEBOUNCE_MS = 350;
15887
16486
  var MIRROR_EVENT_BATCH_LIMIT = 8;
15888
16487
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
15889
16488
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
16489
+ var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
15890
16490
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
15891
16491
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
15892
16492
  var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
@@ -15974,6 +16574,23 @@ var INTERACTIVE_RUNTIME = createInteractiveRuntime(getState, {
15974
16574
  getStore: () => getBridgeContext().store,
15975
16575
  nowIso: nowIso3
15976
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
+ });
15977
16594
  var SESSION_HEALTH_RUNTIME = createSessionHealthRuntime({
15978
16595
  getStore: () => getBridgeContext().store,
15979
16596
  nowIso: nowIso3,
@@ -16010,9 +16627,6 @@ function settleMirrorSuppression2(sessionId, suppressionId, durationMs = MIRROR_
16010
16627
  durationMs
16011
16628
  );
16012
16629
  }
16013
- function isMirrorSuppressed2(sessionId) {
16014
- return isMirrorSuppressed(getMirrorSuppressionStore(), sessionId);
16015
- }
16016
16630
  function filterSuppressedMirrorRecords2(sessionId, records) {
16017
16631
  return filterSuppressedMirrorRecords(
16018
16632
  getMirrorSuppressionStore(),
@@ -16046,28 +16660,6 @@ function syncMirrorSessionStateSafe(sessionId, context) {
16046
16660
  );
16047
16661
  }
16048
16662
  }
16049
- function getMirrorStreamingAdapter(subscription) {
16050
- const state = getState();
16051
- const adapter = state.adapters.get(subscription.channelType);
16052
- if (!adapter || !adapter.isRunning()) return null;
16053
- if (getChannelProviderKey(subscription.channelType) !== "feishu") return null;
16054
- if (typeof adapter.onStreamText !== "function" || typeof adapter.onStreamEnd !== "function") {
16055
- return null;
16056
- }
16057
- return adapter;
16058
- }
16059
- function getMirrorStreamingText(subscription, turnState) {
16060
- const title = getDesktopThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
16061
- const markdown = getFeedbackParseMode(subscription.channelType) === "Markdown";
16062
- const rendered = formatMirrorMessage(
16063
- title,
16064
- turnState.userText,
16065
- turnState.streamedText,
16066
- markdown,
16067
- true
16068
- );
16069
- return rendered || buildMirrorTitle(title, markdown);
16070
- }
16071
16663
  function getMirrorStructuredStreamStatusConfig() {
16072
16664
  const { store } = getBridgeContext();
16073
16665
  const idleStartSeconds = parseInt(store.getSetting("bridge_stream_status_idle_start_seconds") || "", 10);
@@ -16083,216 +16675,43 @@ function getMirrorStructuredStreamStatusConfig() {
16083
16675
  )
16084
16676
  };
16085
16677
  }
16086
- function startMirrorStreaming(subscription, turnState) {
16087
- const adapter = getMirrorStreamingAdapter(subscription);
16088
- if (!adapter || turnState.streamStarted) return;
16089
- try {
16090
- adapter.onMirrorStreamStart?.(subscription.chatId, turnState.streamKey);
16091
- if (!adapter.onMirrorStreamStart) {
16092
- adapter.onStreamText?.(subscription.chatId, "", turnState.streamKey);
16093
- }
16094
- turnState.streamStarted = true;
16095
- } catch {
16096
- }
16097
- }
16098
- function createMirrorStreamFeedbackTarget(subscription, turnState, adapter) {
16099
- return {
16100
- adapter,
16101
- channelType: subscription.channelType,
16102
- chatId: subscription.chatId,
16103
- streamKey: turnState.streamKey,
16104
- ensureStarted: () => {
16105
- startMirrorStreaming(subscription, turnState);
16106
- }
16107
- };
16108
- }
16109
- function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
16110
- const adapter = getMirrorStreamingAdapter(subscription);
16111
- if (!adapter || typeof adapter.onStreamStatus !== "function") return;
16112
- if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
16113
- const startedAtMs = Date.parse(turnState.startedAt);
16114
- if (!Number.isFinite(startedAtMs)) return;
16115
- const nowMs = options.nowMs ?? Date.now();
16116
- const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
16117
- if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
16118
- return;
16119
- }
16120
- const statusText = formatInteractiveRuntimeStatus(
16121
- Math.max(0, nowMs - startedAtMs),
16122
- options.lastResponseAgeMs,
16123
- turnState.statusNote
16124
- );
16125
- if (turnState.lastStatusText === statusText) return;
16126
- pushStreamFeedbackStatus(
16127
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16128
- statusText
16129
- );
16130
- turnState.lastStatusText = statusText;
16131
- turnState.lastStatusAt = nowMs;
16132
- }
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
+ });
16133
16685
  function refreshMirrorStreamingStatus(subscription, nowMs = Date.now(), config2 = getMirrorStructuredStreamStatusConfig()) {
16134
- const pendingTurn = subscription.pendingTurn;
16135
- if (!pendingTurn?.streamStarted) return;
16136
- const startedAtMs = Date.parse(pendingTurn.startedAt);
16137
- if (!Number.isFinite(startedAtMs)) return;
16138
- const elapsedMs = nowMs - startedAtMs;
16139
- if (elapsedMs < config2.idleStartMs) return;
16140
- const lastResponseAtMs = pendingTurn.lastResponseAt ? Date.parse(pendingTurn.lastResponseAt) : NaN;
16141
- const lastResponseAgeMs = Number.isFinite(lastResponseAtMs) ? nowMs - lastResponseAtMs : null;
16142
- if (lastResponseAgeMs != null && lastResponseAgeMs < config2.heartbeatMs) return;
16143
- pushMirrorStreamingStatus(subscription, pendingTurn, {
16144
- nowMs,
16145
- lastResponseAgeMs,
16146
- minIntervalMs: config2.heartbeatMs
16147
- });
16686
+ MIRROR_FEEDBACK.refreshMirrorStreamingStatus(subscription, nowMs, config2);
16148
16687
  }
16149
16688
  function refreshActiveMirrorStreamingStatuses(nowMs = Date.now()) {
16150
16689
  for (const subscription of getState().mirrorSubscriptions.values()) {
16151
16690
  refreshMirrorStreamingStatus(subscription, nowMs);
16152
16691
  }
16153
16692
  }
16154
- function updateMirrorStreaming(subscription, turnState) {
16155
- const adapter = getMirrorStreamingAdapter(subscription);
16156
- if (!adapter) return;
16157
- pushStreamFeedbackText(
16158
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16159
- getMirrorStreamingText(subscription, turnState)
16160
- );
16161
- pushMirrorStreamingStatus(subscription, turnState);
16162
- }
16163
- function updateMirrorToolProgress(subscription, turnState) {
16164
- const adapter = getMirrorStreamingAdapter(subscription);
16165
- if (!adapter) return;
16166
- pushStreamFeedbackTools(
16167
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16168
- Array.from(turnState.toolCalls.values())
16169
- );
16170
- pushMirrorStreamingStatus(subscription, turnState);
16171
- }
16172
- function updateMirrorTaskProgress(subscription, turnState) {
16173
- const adapter = getMirrorStreamingAdapter(subscription);
16174
- if (!adapter) return;
16175
- pushStreamFeedbackTasks(
16176
- createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
16177
- turnState.taskItems
16178
- );
16179
- pushMirrorStreamingStatus(subscription, turnState);
16180
- }
16181
- function updateMirrorStatusProgress(subscription, turnState) {
16182
- const adapter = getMirrorStreamingAdapter(subscription);
16183
- if (!adapter) return;
16184
- pushMirrorStreamingStatus(subscription, turnState);
16185
- }
16186
16693
  function stopMirrorStreaming(subscription, status = "interrupted") {
16187
- const adapter = getMirrorStreamingAdapter(subscription);
16188
- const pendingTurn = subscription.pendingTurn;
16189
- if (!adapter || !pendingTurn?.streamStarted) return;
16190
- void finalizeStreamFeedback(
16191
- createMirrorStreamFeedbackTarget(subscription, pendingTurn, adapter),
16192
- status,
16193
- getMirrorStreamingText(subscription, pendingTurn)
16194
- );
16195
- }
16196
- async function deliverMirrorTurn(subscription, turn) {
16197
- const state = getState();
16198
- const adapter = state.adapters.get(subscription.channelType);
16199
- if (!adapter || !adapter.isRunning()) return;
16200
- const title = getDesktopThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
16201
- const responseParseMode = getFeedbackParseMode(subscription.channelType);
16202
- const markdown = responseParseMode === "Markdown";
16203
- const renderedTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown);
16204
- const renderedStreamTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown, true);
16205
- const renderedText = turn.timedOut ? appendMirrorTimeoutNotice(renderedTextBase || buildMirrorTitle(title, markdown), markdown) : renderedTextBase;
16206
- const renderedStreamText = turn.timedOut ? appendMirrorTimeoutNotice(renderedStreamTextBase || buildMirrorTitle(title, markdown), markdown) : renderedStreamTextBase;
16207
- const text2 = renderedText ? renderFeedbackText(renderedText, responseParseMode) : "";
16208
- const streamText = renderFeedbackText(
16209
- renderedStreamText || buildMirrorTitle(title, markdown),
16210
- responseParseMode
16211
- );
16212
- if (getChannelProviderKey(subscription.channelType) === "feishu" && typeof adapter.onStreamEnd === "function") {
16213
- try {
16214
- const finalized = await adapter.onStreamEnd(
16215
- subscription.chatId,
16216
- turn.status,
16217
- streamText,
16218
- turn.streamKey
16219
- );
16220
- if (finalized) {
16221
- subscription.lastDeliveredAt = turn.timestamp || nowIso3();
16222
- return;
16223
- }
16224
- } catch (error) {
16225
- console.warn("[bridge-manager] Mirror stream finalize failed:", error instanceof Error ? error.message : error);
16226
- }
16227
- }
16228
- if (!text2) return;
16229
- const response = await deliver(adapter, {
16230
- address: {
16231
- channelType: subscription.channelType,
16232
- chatId: subscription.chatId
16233
- },
16234
- text: text2,
16235
- parseMode: responseParseMode
16236
- }, {
16237
- sessionId: subscription.sessionId,
16238
- dedupKey: `mirror:${subscription.bindingId}:${turn.signature}`
16239
- });
16240
- if (!response.ok) {
16241
- throw new Error(response.error || "mirror delivery failed");
16242
- }
16243
- subscription.lastDeliveredAt = turn.timestamp || nowIso3();
16694
+ MIRROR_FEEDBACK.stopMirrorStreaming(subscription, status);
16244
16695
  }
16245
16696
  async function deliverMirrorTurns(subscription, turns) {
16246
- let deliveredCount = 0;
16247
- for (const turn of turns.slice(0, MIRROR_EVENT_BATCH_LIMIT)) {
16248
- try {
16249
- await deliverMirrorTurn(subscription, turn);
16250
- deliveredCount += 1;
16251
- } catch (error) {
16252
- return { deliveredCount, error };
16253
- }
16254
- }
16255
- return { deliveredCount };
16697
+ return MIRROR_FEEDBACK.deliverMirrorTurns(subscription, turns);
16256
16698
  }
16257
- var MIRROR_TURN_HOOKS = {
16258
- onStreamText: updateMirrorStreaming,
16259
- onStatusProgress: updateMirrorStatusProgress,
16260
- onTaskProgress: updateMirrorTaskProgress,
16261
- onToolProgress: updateMirrorToolProgress
16262
- };
16699
+ var MIRROR_TURN_HOOKS = MIRROR_FEEDBACK.hooks;
16263
16700
  function consumeMirrorRecords2(subscription, records) {
16264
16701
  return consumeMirrorRecords(subscription, records, MIRROR_TURN_HOOKS);
16265
16702
  }
16266
16703
  function flushTimedOutMirrorTurn2(subscription, nowMs = Date.now()) {
16704
+ if (subscription.pendingTurn?.streamStarted) {
16705
+ return null;
16706
+ }
16267
16707
  return flushTimedOutMirrorTurn(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs);
16268
16708
  }
16269
16709
  function hasPendingMirrorWork2(subscription) {
16270
16710
  return hasPendingMirrorWork(subscription);
16271
16711
  }
16272
16712
  function consumeBufferedMirrorTurns2(subscription, nowMs = Date.now()) {
16273
- return consumeBufferedMirrorTurns(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs, MIRROR_TURN_HOOKS);
16274
- }
16275
- function finalizeInteractiveTaskFromMirrorRecords(sessionId, records) {
16276
- let terminalRecord = null;
16277
- for (let index = records.length - 1; index >= 0; index -= 1) {
16278
- const record = records[index];
16279
- if (record.type === "task_complete" || record.type === "task_aborted") {
16280
- terminalRecord = record;
16281
- break;
16282
- }
16283
- }
16284
- if (!terminalRecord) return Promise.resolve(false);
16285
- const outcome = terminalRecord.type === "task_complete" ? "completed" : "aborted";
16286
- const detail = terminalRecord.type === "task_complete" ? "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002" : "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
16287
- return INTERACTIVE_RUNTIME.finalizeTerminalActiveTask(
16288
- sessionId,
16289
- outcome,
16290
- detail,
16291
- terminalRecord.content
16292
- ).catch((error) => {
16293
- console.error("[bridge-manager] Failed to finalize terminal interactive task:", describeUnknownError(error));
16294
- return false;
16295
- });
16713
+ const timeoutMs = subscription.pendingTurn?.streamStarted ? Number.POSITIVE_INFINITY : MIRROR_TURN_BUFFER_TIMEOUT_MS;
16714
+ return consumeBufferedMirrorTurns(subscription, timeoutMs, nowMs, MIRROR_TURN_HOOKS);
16296
16715
  }
16297
16716
  var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16298
16717
  watchDebounceMs: MIRROR_WATCH_DEBOUNCE_MS,
@@ -16304,12 +16723,16 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16304
16723
  describeUnknownError,
16305
16724
  getDesktopSessionByThreadIdSafe,
16306
16725
  syncMirrorSessionStateSafe,
16307
- isMirrorSuppressed: isMirrorSuppressed2,
16308
16726
  filterSuppressedMirrorRecords: filterSuppressedMirrorRecords2,
16309
16727
  observeSessionHealthRecords: (sessionId, threadId, records) => {
16310
16728
  SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
16311
- void finalizeInteractiveTaskFromMirrorRecords(sessionId, records);
16312
16729
  },
16730
+ routeDesktopRecords: (sessionId, threadId, records) => routeDesktopRecords(
16731
+ sessionId,
16732
+ threadId,
16733
+ records,
16734
+ TURN_COORDINATOR
16735
+ ),
16313
16736
  consumeMirrorRecords: consumeMirrorRecords2,
16314
16737
  flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
16315
16738
  hasPendingMirrorWork: hasPendingMirrorWork2,
@@ -16557,6 +16980,7 @@ async function handleMessage(adapter, msg) {
16557
16980
  try {
16558
16981
  await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
16559
16982
  registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
16983
+ registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
16560
16984
  resetMirrorSessionForInteractiveRun,
16561
16985
  isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
16562
16986
  touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
@@ -16573,8 +16997,10 @@ async function handleMessage(adapter, msg) {
16573
16997
  abortMirrorSuppression: abortMirrorSuppression2,
16574
16998
  settleMirrorSuppression: settleMirrorSuppression2,
16575
16999
  releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
17000
+ releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
16576
17001
  deliverResponse,
16577
- persistSdkSessionUpdate
17002
+ persistSdkSessionUpdate,
17003
+ desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
16578
17004
  });
16579
17005
  } finally {
16580
17006
  ack();
@@ -16886,7 +17312,7 @@ var JsonFileStore = class {
16886
17312
  findSessionBySdkSessionId(sdkSessionId) {
16887
17313
  this.reloadSessions();
16888
17314
  for (const session of this.sessions.values()) {
16889
- if (session.sdk_session_id === sdkSessionId) {
17315
+ if (session.sdk_session_id === sdkSessionId || session.codex_thread_id === sdkSessionId || session.desktop_thread_id === sdkSessionId) {
16890
17316
  return session;
16891
17317
  }
16892
17318
  }
@@ -17020,6 +17446,15 @@ var JsonFileStore = class {
17020
17446
  const s = this.sessions.get(sessionId);
17021
17447
  if (s) {
17022
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
+ }
17023
17458
  s.updated_at = now();
17024
17459
  this.persistSessions();
17025
17460
  }