codex-to-im 1.0.44 → 1.0.46

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
@@ -1223,14 +1223,19 @@ function buildPostContent(text2) {
1223
1223
  function htmlToFeishuMarkdown(html) {
1224
1224
  return html.replace(/<b>(.*?)<\/b>/gi, "**$1**").replace(/<strong>(.*?)<\/strong>/gi, "**$1**").replace(/<i>(.*?)<\/i>/gi, "*$1*").replace(/<em>(.*?)<\/em>/gi, "*$1*").replace(/<code>(.*?)<\/code>/gi, "`$1`").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\n{3,}/g, "\n\n").trim();
1225
1225
  }
1226
- function buildToolProgressMarkdown(tools) {
1226
+ function normalizeToolStatusForRender(status, options) {
1227
+ if (status !== "running" || !options.terminalStatus) return status;
1228
+ return options.terminalStatus === "completed" ? "complete" : "error";
1229
+ }
1230
+ function buildToolProgressMarkdown(tools, options = {}) {
1227
1231
  if (tools.length === 0) return "";
1228
1232
  const grouped = /* @__PURE__ */ new Map();
1229
1233
  for (const tool of tools) {
1230
1234
  const key = tool.name || "tool";
1231
1235
  const bucket = grouped.get(key) || { running: 0, complete: 0, error: 0 };
1232
- if (tool.status === "running") bucket.running += 1;
1233
- else if (tool.status === "error") bucket.error += 1;
1236
+ const status = normalizeToolStatusForRender(tool.status, options);
1237
+ if (status === "running") bucket.running += 1;
1238
+ else if (status === "error") bucket.error += 1;
1234
1239
  else bucket.complete += 1;
1235
1240
  grouped.set(key, bucket);
1236
1241
  }
@@ -1249,11 +1254,25 @@ function buildToolProgressMarkdown(tools) {
1249
1254
  });
1250
1255
  return lines.join("\n");
1251
1256
  }
1252
- function buildTaskProgressMarkdown(tasks) {
1257
+ function getTaskProgressPresentation(task, options) {
1258
+ if (!options.terminalStatus) {
1259
+ return task.status === "completed" ? { icon: "\u2705", label: "\u5DF2\u5B8C\u6210" } : task.status === "in_progress" ? { icon: "\u{1F504}", label: "\u6267\u884C\u4E2D" } : { icon: "\u23F3", label: "\u7B49\u5F85\u4E2D" };
1260
+ }
1261
+ if (task.status === "completed") {
1262
+ return { icon: "\u2705", label: "\u5DF2\u5B8C\u6210" };
1263
+ }
1264
+ if (options.terminalStatus === "completed") {
1265
+ return { icon: "\u2705", label: "\u5DF2\u7ED3\u675F" };
1266
+ }
1267
+ if (options.terminalStatus === "interrupted") {
1268
+ return task.status === "pending" ? { icon: "\u26A0\uFE0F", label: "\u672A\u6267\u884C" } : { icon: "\u26A0\uFE0F", label: "\u5DF2\u505C\u6B62" };
1269
+ }
1270
+ return task.status === "pending" ? { icon: "\u274C", label: "\u672A\u6267\u884C" } : { icon: "\u274C", label: "\u5DF2\u4E2D\u65AD" };
1271
+ }
1272
+ function buildTaskProgressMarkdown(tasks, options = {}) {
1253
1273
  if (tasks.length === 0) return "";
1254
1274
  return tasks.map((task) => {
1255
- const icon = task.status === "completed" ? "\u2705" : task.status === "in_progress" ? "\u{1F504}" : "\u23F3";
1256
- const label = task.status === "completed" ? "\u5DF2\u5B8C\u6210" : task.status === "in_progress" ? "\u6267\u884C\u4E2D" : "\u7B49\u5F85\u4E2D";
1275
+ const { icon, label } = getTaskProgressPresentation(task, options);
1257
1276
  return `${icon} ${task.text}\uFF08${label}\uFF09`;
1258
1277
  }).join("\n");
1259
1278
  }
@@ -1274,11 +1293,12 @@ function buildStreamingToolsContent(tools) {
1274
1293
  function buildStreamingTaskContent(tasks) {
1275
1294
  return buildTaskProgressMarkdown(tasks);
1276
1295
  }
1277
- function buildFinalCardJson(text2, tasks, tools, footer) {
1296
+ function buildFinalCardJson(text2, tasks, tools, footer, terminalStatus) {
1278
1297
  const elements = [];
1279
1298
  const content = preprocessFeishuMarkdown(text2);
1280
- const taskMd = buildTaskProgressMarkdown(tasks);
1281
- const toolMd = buildToolProgressMarkdown(tools);
1299
+ const renderOptions = { terminalStatus };
1300
+ const taskMd = buildTaskProgressMarkdown(tasks, renderOptions);
1301
+ const toolMd = buildToolProgressMarkdown(tools, renderOptions);
1282
1302
  if (content) {
1283
1303
  elements.push({
1284
1304
  tag: "markdown",
@@ -1384,9 +1404,53 @@ var MAX_FILE_SIZE = 20 * 1024 * 1024;
1384
1404
  var TYPING_EMOJI = "Typing";
1385
1405
  var CARD_THROTTLE_MS = 1e3;
1386
1406
  var CARD_REQUEST_TIMEOUT_MS = 15e3;
1407
+ var CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS = 1e3;
1408
+ var CARD_FULL_REFRESH_INTERVAL_MS = 5 * 6e4;
1387
1409
  var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
1388
1410
  var EMPTY_STREAMING_TASKS = "";
1389
1411
  var EMPTY_STREAMING_TOOLS = "";
1412
+ function buildStreamingCardBody(content, tasksText, toolsText, statusText) {
1413
+ return {
1414
+ schema: "2.0",
1415
+ config: {
1416
+ streaming_mode: true,
1417
+ wide_screen_mode: true,
1418
+ summary: { content: "\u601D\u8003\u4E2D..." }
1419
+ },
1420
+ body: {
1421
+ elements: [
1422
+ {
1423
+ tag: "markdown",
1424
+ content,
1425
+ text_align: "left",
1426
+ text_size: "normal",
1427
+ element_id: "streaming_content"
1428
+ },
1429
+ {
1430
+ tag: "markdown",
1431
+ content: tasksText,
1432
+ text_align: "left",
1433
+ text_size: "normal",
1434
+ element_id: "streaming_tasks"
1435
+ },
1436
+ {
1437
+ tag: "markdown",
1438
+ content: toolsText,
1439
+ text_align: "left",
1440
+ text_size: "normal",
1441
+ element_id: "streaming_tools"
1442
+ },
1443
+ {
1444
+ tag: "markdown",
1445
+ content: statusText,
1446
+ text_align: "left",
1447
+ text_size: "notation",
1448
+ element_id: "streaming_status"
1449
+ }
1450
+ ]
1451
+ }
1452
+ };
1453
+ }
1390
1454
  var MIME_BY_TYPE = {
1391
1455
  image: "image/png",
1392
1456
  file: "application/octet-stream",
@@ -1421,6 +1485,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1421
1485
  /** Cached tenant token for upload APIs. */
1422
1486
  tenantTokenCache = null;
1423
1487
  cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
1488
+ cardFinalizeFlushWaitExtraMs = CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS;
1489
+ cardFullRefreshIntervalMs = CARD_FULL_REFRESH_INTERVAL_MS;
1424
1490
  constructor(instance) {
1425
1491
  super();
1426
1492
  this.channelType = instance?.id || "feishu";
@@ -1654,46 +1720,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1654
1720
  return false;
1655
1721
  }
1656
1722
  try {
1657
- const cardBody = {
1658
- schema: "2.0",
1659
- config: {
1660
- streaming_mode: true,
1661
- wide_screen_mode: true,
1662
- summary: { content: "\u601D\u8003\u4E2D..." }
1663
- },
1664
- body: {
1665
- elements: [
1666
- {
1667
- tag: "markdown",
1668
- content: "\u{1F4AD} Thinking...",
1669
- text_align: "left",
1670
- text_size: "normal",
1671
- element_id: "streaming_content"
1672
- },
1673
- {
1674
- tag: "markdown",
1675
- content: EMPTY_STREAMING_TASKS,
1676
- text_align: "left",
1677
- text_size: "normal",
1678
- element_id: "streaming_tasks"
1679
- },
1680
- {
1681
- tag: "markdown",
1682
- content: EMPTY_STREAMING_TOOLS,
1683
- text_align: "left",
1684
- text_size: "normal",
1685
- element_id: "streaming_tools"
1686
- },
1687
- {
1688
- tag: "markdown",
1689
- content: INITIAL_STREAMING_STATUS,
1690
- text_align: "left",
1691
- text_size: "notation",
1692
- element_id: "streaming_status"
1693
- }
1694
- ]
1695
- }
1696
- };
1723
+ const cardBody = buildStreamingCardBody(
1724
+ "\u{1F4AD} Thinking...",
1725
+ EMPTY_STREAMING_TASKS,
1726
+ EMPTY_STREAMING_TOOLS,
1727
+ INITIAL_STREAMING_STATUS
1728
+ );
1697
1729
  const createResp = await this.withFeishuRequestTimeout(cardKey, "card.create", () => cardkit.card.create({
1698
1730
  data: { type: "card_json", data: JSON.stringify(cardBody) }
1699
1731
  }));
@@ -1724,12 +1756,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1724
1756
  console.warn("[feishu-adapter] Card message send returned no message_id");
1725
1757
  return false;
1726
1758
  }
1759
+ const now2 = Date.now();
1727
1760
  this.activeCards.set(cardKey, {
1728
1761
  chatId,
1729
1762
  cardId,
1730
1763
  messageId,
1731
1764
  sequence: 0,
1732
- startTime: Date.now(),
1765
+ startTime: now2,
1733
1766
  taskItems: [],
1734
1767
  toolCalls: [],
1735
1768
  thinking: true,
@@ -1748,7 +1781,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1748
1781
  lastSuccessfulFlushAt: null,
1749
1782
  lastFlushErrorAt: null,
1750
1783
  lastFlushError: null,
1751
- consecutiveFlushFailures: 0
1784
+ consecutiveFlushFailures: 0,
1785
+ lastFullRefreshAttemptAt: now2,
1786
+ lastSuccessfulFullRefreshAt: null
1752
1787
  });
1753
1788
  console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
1754
1789
  return true;
@@ -1837,6 +1872,17 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1837
1872
  const toolsText = buildStreamingToolsContent(state.toolCalls) || EMPTY_STREAMING_TOOLS;
1838
1873
  const statusText = state.pendingStatusText || INITIAL_STREAMING_STATUS;
1839
1874
  const updates = [];
1875
+ if (this.shouldFullRefreshCard(state, Date.now())) {
1876
+ const refreshed = await this.flushFullCardRefresh(
1877
+ streamKey,
1878
+ state,
1879
+ content,
1880
+ tasksText,
1881
+ toolsText,
1882
+ statusText
1883
+ );
1884
+ if (refreshed) return;
1885
+ }
1840
1886
  if (content !== state.renderedText) {
1841
1887
  updates.push({
1842
1888
  elementId: "streaming_content",
@@ -1903,24 +1949,33 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1903
1949
  state.toolCalls = tools;
1904
1950
  this.scheduleCardFlush(cardKey);
1905
1951
  }
1906
- async awaitCardFlushCompletion(streamKey) {
1952
+ async awaitCardFlushCompletion(streamKey, timeoutMs = this.getCardRequestTimeoutMs() + Math.max(0, this.cardFinalizeFlushWaitExtraMs)) {
1953
+ const deadline = Date.now() + Math.max(0, timeoutMs);
1907
1954
  while (true) {
1908
1955
  const state = this.activeCards.get(streamKey);
1909
- if (!state) return;
1956
+ if (!state) return true;
1910
1957
  const inFlight = state.flushInFlight;
1911
1958
  if (inFlight) {
1959
+ const remainingMs = deadline - Date.now();
1960
+ if (remainingMs <= 0) return false;
1961
+ const timedOut = Symbol("flush-timeout");
1912
1962
  try {
1913
- await inFlight;
1963
+ const result = await Promise.race([
1964
+ inFlight.then(() => null),
1965
+ new Promise((resolve2) => setTimeout(() => resolve2(timedOut), remainingMs))
1966
+ ]);
1967
+ if (result === timedOut) return false;
1914
1968
  } catch {
1915
1969
  }
1916
1970
  continue;
1917
1971
  }
1972
+ if (Date.now() >= deadline) return false;
1918
1973
  if (state.flushQueued) {
1919
1974
  state.flushQueued = false;
1920
1975
  this.enqueueCardFlush(streamKey);
1921
1976
  continue;
1922
1977
  }
1923
- return;
1978
+ return true;
1924
1979
  }
1925
1980
  }
1926
1981
  /**
@@ -1943,7 +1998,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1943
1998
  clearTimeout(state.throttleTimer);
1944
1999
  state.throttleTimer = null;
1945
2000
  }
1946
- await this.awaitCardFlushCompletion(cardKey);
2001
+ const flushed = await this.awaitCardFlushCompletion(cardKey);
2002
+ if (!flushed) {
2003
+ console.warn(`[feishu-adapter] Card finalize proceeding after flush wait timeout: streamKey=${cardKey}`);
2004
+ state.flushInFlight = null;
2005
+ state.flushQueued = false;
2006
+ }
1947
2007
  try {
1948
2008
  state.sequence++;
1949
2009
  await this.withFeishuRequestTimeout(cardKey, "card.settings", () => cardkit.card.settings({
@@ -1972,7 +2032,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1972
2032
 
1973
2033
  ${trimmedResponse}`;
1974
2034
  }
1975
- const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer);
2035
+ const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer, status);
1976
2036
  state.sequence++;
1977
2037
  await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
1978
2038
  path: { card_id: state.cardId },
@@ -2026,6 +2086,44 @@ ${trimmedResponse}`;
2026
2086
  consecutiveFailures: state.consecutiveFlushFailures
2027
2087
  };
2028
2088
  }
2089
+ shouldFullRefreshCard(state, now2) {
2090
+ const interval = Math.max(0, this.cardFullRefreshIntervalMs);
2091
+ if (interval <= 0) return false;
2092
+ if (!Number.isFinite(now2)) return false;
2093
+ return now2 - state.lastFullRefreshAttemptAt >= interval;
2094
+ }
2095
+ async flushFullCardRefresh(streamKey, state, content, tasksText, toolsText, statusText) {
2096
+ state.lastFullRefreshAttemptAt = Date.now();
2097
+ const cardkit = this.restClient?.cardkit?.v1;
2098
+ if (!cardkit?.card?.update) return false;
2099
+ try {
2100
+ state.sequence++;
2101
+ await this.withFeishuRequestTimeout(streamKey, "card.update:streaming_refresh", () => cardkit.card.update({
2102
+ path: { card_id: state.cardId },
2103
+ data: {
2104
+ card: {
2105
+ type: "card_json",
2106
+ data: JSON.stringify(buildStreamingCardBody(content, tasksText, toolsText, statusText))
2107
+ },
2108
+ sequence: state.sequence
2109
+ }
2110
+ }));
2111
+ state.renderedText = content;
2112
+ state.renderedTasksText = tasksText;
2113
+ state.renderedToolsText = toolsText;
2114
+ state.renderedStatusText = statusText;
2115
+ state.lastSuccessfulFullRefreshAt = Date.now();
2116
+ this.markCardFlushSuccess(state);
2117
+ return true;
2118
+ } catch (err) {
2119
+ this.markCardFlushFailure(state, err);
2120
+ console.warn(
2121
+ "[feishu-adapter] card.update streaming refresh failed:",
2122
+ err instanceof Error ? err.message : err
2123
+ );
2124
+ return false;
2125
+ }
2126
+ }
2029
2127
  getCardRequestTimeoutMs() {
2030
2128
  return Math.max(1, this.cardRequestTimeoutMs);
2031
2129
  }
@@ -10473,6 +10571,18 @@ function readDesktopSessionEventStream(threadId) {
10473
10571
  return readDesktopSessionEventStreamByFilePath(session.filePath);
10474
10572
  }
10475
10573
 
10574
+ // src/lib/bridge/turns/turn-classifier.ts
10575
+ function normalizeThreadId(value) {
10576
+ const normalized = value?.trim();
10577
+ return normalized || void 0;
10578
+ }
10579
+ function getCodexThreadId(session, binding) {
10580
+ return normalizeThreadId(session?.codex_thread_id) || normalizeThreadId(binding?.sdkSessionId) || normalizeThreadId(session?.sdk_session_id);
10581
+ }
10582
+ function getExplicitDesktopThreadId(session) {
10583
+ return normalizeThreadId(session?.desktop_thread_id) || (session?.thread_origin === "desktop" ? normalizeThreadId(session.sdk_session_id) : void 0);
10584
+ }
10585
+
10476
10586
  // src/session-bindings.ts
10477
10587
  function asChannelProvider(value) {
10478
10588
  return value === "feishu" || value === "weixin" ? value : void 0;
@@ -10518,15 +10628,31 @@ function assertBindingTargetAvailable(store, current, opts) {
10518
10628
  function getSessionMode(store, session) {
10519
10629
  return session.preferred_mode || store.getSetting("bridge_default_mode") || "code";
10520
10630
  }
10631
+ function getBindingResumeThreadId(session) {
10632
+ return getCodexThreadId(session) || "";
10633
+ }
10634
+ function markSessionAsDesktopBacked(store, sessionId, desktopThreadId) {
10635
+ store.updateSdkSessionId(sessionId, desktopThreadId);
10636
+ store.updateSession(sessionId, {
10637
+ sdk_session_id: desktopThreadId,
10638
+ codex_thread_id: desktopThreadId,
10639
+ desktop_thread_id: desktopThreadId,
10640
+ thread_origin: "desktop"
10641
+ });
10642
+ }
10521
10643
  function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10522
10644
  const session = store.getSession(sessionId);
10523
10645
  if (!session) return null;
10524
10646
  assertBindingTargetAvailable(
10525
10647
  store,
10526
10648
  { channelType, chatId },
10527
- { sessionId: session.id, sdkSessionId: session.sdk_session_id || void 0 }
10649
+ {
10650
+ sessionId: session.id,
10651
+ sdkSessionId: getCodexThreadId(session) || getExplicitDesktopThreadId(session)
10652
+ }
10528
10653
  );
10529
10654
  const meta = resolveChannelMeta(channelType);
10655
+ const sdkSessionId = getBindingResumeThreadId(session);
10530
10656
  return store.upsertChannelBinding({
10531
10657
  channelType,
10532
10658
  channelProvider: meta.provider,
@@ -10535,7 +10661,7 @@ function bindStoreToSession(store, channelType, chatId, sessionId, chatMeta) {
10535
10661
  chatUserId: chatMeta?.chatUserId,
10536
10662
  chatDisplayName: chatMeta?.chatDisplayName,
10537
10663
  codepilotSessionId: session.id,
10538
- sdkSessionId: session.sdk_session_id || "",
10664
+ sdkSessionId,
10539
10665
  workingDirectory: session.working_directory,
10540
10666
  model: session.model,
10541
10667
  mode: getSessionMode(store, session)
@@ -10550,6 +10676,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10550
10676
  const meta = resolveChannelMeta(channelType);
10551
10677
  const existing = store.findSessionBySdkSessionId(sdkSessionId);
10552
10678
  if (existing) {
10679
+ markSessionAsDesktopBacked(store, existing.id, sdkSessionId);
10553
10680
  return store.upsertChannelBinding({
10554
10681
  channelType,
10555
10682
  channelProvider: meta.provider,
@@ -10574,7 +10701,7 @@ function bindStoreToSdkSession(store, channelType, chatId, sdkSessionId, opts) {
10574
10701
  workingDirectory,
10575
10702
  "code"
10576
10703
  );
10577
- store.updateSdkSessionId(session.id, sdkSessionId);
10704
+ markSessionAsDesktopBacked(store, session.id, sdkSessionId);
10578
10705
  return store.upsertChannelBinding({
10579
10706
  channelType,
10580
10707
  channelProvider: meta.provider,
@@ -10947,15 +11074,17 @@ async function deliver(adapter, message, opts) {
10947
11074
  } catch {
10948
11075
  }
10949
11076
  }
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 {
11077
+ if (opts?.audit !== false) {
11078
+ try {
11079
+ store.insertAuditLog({
11080
+ channelType: adapter.channelType,
11081
+ chatId: message.address.chatId,
11082
+ direction: "outbound",
11083
+ messageId: lastMessageId || "",
11084
+ summary: message.text.slice(0, 200)
11085
+ });
11086
+ } catch {
11087
+ }
10959
11088
  }
10960
11089
  return { ok: true, messageId: lastMessageId };
10961
11090
  }
@@ -11552,7 +11681,6 @@ function buildHealthCommandResponse(title, diagnosis, markdown = false) {
11552
11681
  title,
11553
11682
  [
11554
11683
  ["Session", diagnosis.sessionId],
11555
- ["\u68C0\u67E5\u65F6\u95F4", formatCommandTimestamp(diagnosis.checkedAt)],
11556
11684
  ["\u8FD0\u884C\u72B6\u6001", formatRuntimeStatus({ runtime_status: diagnosis.runtimeStatus, queued_count: 0 })],
11557
11685
  ["\u5065\u5EB7\u72B6\u6001", formatHealthStatusLabel(diagnosis.healthStatus)],
11558
11686
  ["\u5F53\u524D\u9636\u6BB5", currentStage],
@@ -11573,7 +11701,6 @@ function buildHealthListResponse(diagnoses, markdown = false) {
11573
11701
  diagnoses.map((diagnosis) => ({
11574
11702
  heading: diagnosis.sessionId,
11575
11703
  details: [
11576
- `\u68C0\u67E5\u65F6\u95F4\uFF1A${formatCommandTimestamp(diagnosis.checkedAt)}`,
11577
11704
  `\u5065\u5EB7\u72B6\u6001\uFF1A${formatHealthStatusLabel(diagnosis.healthStatus)}`,
11578
11705
  `\u5F53\u524D\u9636\u6BB5\uFF1A${diagnosis.activeToolName ? `\u5DE5\u5177 \xB7 ${diagnosis.activeToolName}` : diagnosis.lastProgressType || "-"}`,
11579
11706
  `\u6700\u540E\u8FDB\u5C55\uFF1A${formatCommandTimestamp(diagnosis.lastProgressAt)}`,
@@ -11681,6 +11808,8 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
11681
11808
  streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
11682
11809
  startedAt: safeTimestamp,
11683
11810
  lastActivityAt: safeTimestamp,
11811
+ lastContentResponseAt: null,
11812
+ lastResponseAt: null,
11684
11813
  lastStatusText: null,
11685
11814
  lastStatusAt: 0,
11686
11815
  statusNote: null,
@@ -11727,8 +11856,14 @@ function ensureMirrorTurnState(subscription, record) {
11727
11856
  }
11728
11857
  return subscription.pendingTurn;
11729
11858
  }
11730
- function markMirrorResponse(turnState, timestamp) {
11731
- turnState.lastResponseAt = timestamp || nowIso2();
11859
+ function markMirrorActivity(turnState, timestamp) {
11860
+ turnState.lastActivityAt = timestamp || nowIso2();
11861
+ }
11862
+ function markMirrorContentResponse(turnState, timestamp) {
11863
+ const responseAt = timestamp || nowIso2();
11864
+ markMirrorActivity(turnState, responseAt);
11865
+ turnState.lastContentResponseAt = responseAt;
11866
+ turnState.lastResponseAt = responseAt;
11732
11867
  }
11733
11868
  function finalizeMirrorTurn(subscription, signature, timestamp, status, preferredText) {
11734
11869
  const pendingTurn = subscription.pendingTurn;
@@ -11801,7 +11936,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11801
11936
  if (text2) {
11802
11937
  pendingTurn.lastAssistantText = text2;
11803
11938
  appendMirrorStreamText(pendingTurn, text2);
11804
- markMirrorResponse(pendingTurn, record.timestamp);
11939
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11805
11940
  hooks.onStreamText?.(subscription, pendingTurn);
11806
11941
  }
11807
11942
  } else if (record.role === "commentary") {
@@ -11809,7 +11944,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11809
11944
  if (text2) {
11810
11945
  pendingTurn.lastCommentaryText = text2;
11811
11946
  appendMirrorStreamText(pendingTurn, text2);
11812
- markMirrorResponse(pendingTurn, record.timestamp);
11947
+ markMirrorContentResponse(pendingTurn, record.timestamp);
11813
11948
  hooks.onStreamText?.(subscription, pendingTurn);
11814
11949
  }
11815
11950
  }
@@ -11820,14 +11955,14 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11820
11955
  const text2 = record.content.trim();
11821
11956
  if (!text2) continue;
11822
11957
  pendingTurn.statusNote = text2;
11823
- markMirrorResponse(pendingTurn, record.timestamp);
11958
+ markMirrorActivity(pendingTurn, record.timestamp);
11824
11959
  hooks.onStatusProgress?.(subscription, pendingTurn);
11825
11960
  continue;
11826
11961
  }
11827
11962
  if (record.type === "plan_update") {
11828
11963
  const pendingTurn = ensureMirrorTurnState(subscription, record);
11829
11964
  pendingTurn.taskItems = record.tasks || [];
11830
- markMirrorResponse(pendingTurn, record.timestamp);
11965
+ markMirrorActivity(pendingTurn, record.timestamp);
11831
11966
  hooks.onTaskProgress?.(subscription, pendingTurn);
11832
11967
  continue;
11833
11968
  }
@@ -11840,7 +11975,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11840
11975
  name: toolName,
11841
11976
  status: "running"
11842
11977
  });
11843
- markMirrorResponse(pendingTurn, record.timestamp);
11978
+ markMirrorActivity(pendingTurn, record.timestamp);
11844
11979
  hooks.onToolProgress?.(subscription, pendingTurn);
11845
11980
  continue;
11846
11981
  }
@@ -11853,7 +11988,7 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11853
11988
  name: existing?.name || record.toolName || "tool",
11854
11989
  status: record.isError ? "error" : "complete"
11855
11990
  });
11856
- markMirrorResponse(pendingTurn, record.timestamp);
11991
+ markMirrorActivity(pendingTurn, record.timestamp);
11857
11992
  hooks.onToolProgress?.(subscription, pendingTurn);
11858
11993
  continue;
11859
11994
  }
@@ -12022,9 +12157,6 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
12022
12157
  }
12023
12158
  target.until = nowMs + config2.suppressionWindowMs;
12024
12159
  }
12025
- function isMirrorSuppressed(store, sessionId, nowMs = Date.now()) {
12026
- return getMirrorSuppressionStates(store, sessionId, nowMs).length > 0;
12027
- }
12028
12160
  function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now()) {
12029
12161
  const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
12030
12162
  const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
@@ -12661,7 +12793,7 @@ function supportsOutboundArtifacts(provider) {
12661
12793
  }
12662
12794
 
12663
12795
  // src/lib/bridge/feedback-delivery.ts
12664
- async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId) {
12796
+ async function deliverTextResponse(adapter, address, responseText, sessionId, replyToMessageId, options) {
12665
12797
  if (!responseText.trim()) return { ok: true };
12666
12798
  const parseMode = getFeedbackParseMode(adapter.channelType);
12667
12799
  const renderedText = renderFeedbackText(responseText, parseMode);
@@ -12671,14 +12803,14 @@ async function deliverTextResponse(adapter, address, responseText, sessionId, re
12671
12803
  text: responseText,
12672
12804
  parseMode: "Markdown",
12673
12805
  replyToMessageId
12674
- }, { sessionId });
12806
+ }, { sessionId, audit: options?.audit });
12675
12807
  }
12676
12808
  return deliver(adapter, {
12677
12809
  address,
12678
12810
  text: parseMode === "Markdown" ? responseText : renderedText,
12679
12811
  parseMode,
12680
12812
  replyToMessageId
12681
- }, { sessionId });
12813
+ }, { sessionId, audit: options?.audit });
12682
12814
  }
12683
12815
  async function deliverBridgeNotice(adapter, address, text2, options) {
12684
12816
  return deliverTextResponse(
@@ -12686,7 +12818,8 @@ async function deliverBridgeNotice(adapter, address, text2, options) {
12686
12818
  address,
12687
12819
  text2,
12688
12820
  options?.sessionId,
12689
- options?.replyToMessageId
12821
+ options?.replyToMessageId,
12822
+ { audit: options?.audit }
12690
12823
  );
12691
12824
  }
12692
12825
  async function deliverResponse(adapter, address, responseText, sessionId, replyToMessageId, attachments = []) {
@@ -12802,6 +12935,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
12802
12935
  }
12803
12936
  let response = "";
12804
12937
  let responseParseMode = getFeedbackParseMode(adapter.channelType);
12938
+ let auditResponse = true;
12805
12939
  const currentBinding = store.getChannelBinding(msg.address.channelType, msg.address.chatId);
12806
12940
  switch (command) {
12807
12941
  case "/start":
@@ -13134,6 +13268,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13134
13268
  break;
13135
13269
  }
13136
13270
  if (!args) {
13271
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13137
13272
  const currentModel = resolveDisplayedModel(
13138
13273
  binding,
13139
13274
  session,
@@ -13145,14 +13280,14 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13145
13280
  [["\u6A21\u578B", formatDisplayedModel(currentModel)]],
13146
13281
  [
13147
13282
  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",
13283
+ 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
13284
  "\u6A21\u578B\u5207\u6362\u53EA\u5F71\u54CD\u540E\u7EED\u4ECE IM \u53D1\u8D77\u7684 Codex CLI \u8BF7\u6C42\u3002"
13150
13285
  ],
13151
13286
  responseParseMode === "Markdown"
13152
13287
  );
13153
13288
  break;
13154
13289
  }
13155
- if (binding.sdkSessionId) {
13290
+ if (getExplicitDesktopThreadId(session)) {
13156
13291
  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
13292
  break;
13158
13293
  }
@@ -13203,9 +13338,32 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13203
13338
  break;
13204
13339
  }
13205
13340
  case "/status": {
13206
- const binding = resolve(msg.address);
13341
+ auditResponse = false;
13342
+ const binding = currentBinding;
13343
+ if (!binding) {
13344
+ response = buildCommandFields(
13345
+ "\u5F53\u524D\u4F1A\u8BDD",
13346
+ [],
13347
+ ["\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"],
13348
+ responseParseMode === "Markdown"
13349
+ );
13350
+ break;
13351
+ }
13207
13352
  const session = store.getSession(binding.codepilotSessionId);
13208
- const threadTitle = getDesktopThreadTitle(binding.sdkSessionId);
13353
+ if (!session) {
13354
+ response = buildCommandFields(
13355
+ "\u5F53\u524D\u4F1A\u8BDD",
13356
+ [
13357
+ ["Session", binding.codepilotSessionId],
13358
+ ["\u76EE\u5F55", formatCommandPath(binding.workingDirectory)]
13359
+ ],
13360
+ ["\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"],
13361
+ responseParseMode === "Markdown"
13362
+ );
13363
+ break;
13364
+ }
13365
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13366
+ const threadTitle = getDesktopThreadTitle(desktopThreadId);
13209
13367
  const sandboxMode = resolveEffectiveSandboxMode();
13210
13368
  const reasoningEffort = resolveEffectiveReasoningEffort(session);
13211
13369
  const currentModel = resolveDisplayedModel(
@@ -13229,21 +13387,25 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13229
13387
  ["\u601D\u8003\u7EA7\u522B", formatReasoningEffort(reasoningEffort)]
13230
13388
  ],
13231
13389
  [
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"
13390
+ 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
13391
  ],
13234
13392
  responseParseMode === "Markdown"
13235
13393
  );
13236
13394
  break;
13237
13395
  }
13238
13396
  case "/health": {
13397
+ auditResponse = false;
13239
13398
  if (args === "all") {
13240
13399
  const diagnoses = await deps.diagnoseAllActiveSessions();
13241
13400
  response = diagnoses.length > 0 ? buildHealthListResponse(diagnoses, responseParseMode === "Markdown") : "\u5F53\u524D\u6CA1\u6709\u68C0\u6D4B\u5230\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u3002";
13242
13401
  break;
13243
13402
  }
13244
- const binding = currentBinding || resolve(msg.address);
13245
13403
  const explicitTargetSessionId = args.trim();
13246
- const targetSessionId = explicitTargetSessionId || binding.codepilotSessionId;
13404
+ const targetSessionId = explicitTargetSessionId || currentBinding?.codepilotSessionId;
13405
+ if (!targetSessionId) {
13406
+ 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";
13407
+ break;
13408
+ }
13247
13409
  const diagnosis = await deps.diagnoseSessionHealth(targetSessionId);
13248
13410
  if (!diagnosis) {
13249
13411
  response = `\u6CA1\u6709\u627E\u5230\u4F1A\u8BDD ${targetSessionId}\u3002`;
@@ -13262,15 +13424,16 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13262
13424
  break;
13263
13425
  }
13264
13426
  const limit = getHistoryMessageLimit();
13265
- const desktopMessages = currentBinding.sdkSessionId ? readDesktopSessionMessages(currentBinding.sdkSessionId, limit) : [];
13427
+ const session = store.getSession(currentBinding.codepilotSessionId);
13428
+ const desktopThreadId = getExplicitDesktopThreadId(session);
13429
+ const desktopMessages = desktopThreadId ? readDesktopSessionMessages(desktopThreadId, limit) : [];
13266
13430
  const { messages: storedMessages } = store.getMessages(currentBinding.codepilotSessionId, { limit });
13267
13431
  const messages = desktopMessages.length > 0 ? desktopMessages : storedMessages;
13268
13432
  if (messages.length === 0) {
13269
13433
  response = "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u5386\u53F2\u6D88\u606F\u3002";
13270
13434
  break;
13271
13435
  }
13272
- const threadTitle = getDesktopThreadTitle(currentBinding.sdkSessionId);
13273
- const session = store.getSession(currentBinding.codepilotSessionId);
13436
+ const threadTitle = getDesktopThreadTitle(desktopThreadId);
13274
13437
  const header = buildCommandFields(
13275
13438
  "\u6700\u8FD1\u5BF9\u8BDD\uFF08raw\uFF09",
13276
13439
  [
@@ -13295,10 +13458,27 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13295
13458
  }
13296
13459
  case "/stop": {
13297
13460
  const binding = resolve(msg.address);
13461
+ const session = store.getSession(binding.codepilotSessionId);
13298
13462
  const task = deps.getActiveTask(binding.codepilotSessionId);
13299
- if (task) {
13300
- task.abortController.abort();
13301
- response = "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u4EFB\u52A1...";
13463
+ const runningHealthStatuses = /* @__PURE__ */ new Set([
13464
+ "running_active",
13465
+ "waiting_tool",
13466
+ "slow_observed",
13467
+ "suspected_stall",
13468
+ "suspected_stream_ui_stall",
13469
+ "suspected_detached"
13470
+ ]);
13471
+ const looksRunning = session?.runtime_status === "running" || session?.runtime_status === "queued" || runningHealthStatuses.has(session?.health_status || "");
13472
+ if (task || looksRunning) {
13473
+ const taskName = getSessionDisplayName(session, binding.workingDirectory);
13474
+ const detail = "\u7528\u6237\u6267\u884C /stop\uFF0C\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
13475
+ if (deps.forceStopSession) {
13476
+ await deps.forceStopSession(binding.codepilotSessionId, detail);
13477
+ } else if (task) {
13478
+ task.abortController.abort();
13479
+ }
13480
+ deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", detail);
13481
+ response = `\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u4EFB\u52A1\u5DF2\u505C\u6B62\uFF0C\u53EF\u7EE7\u7EED\u53D1\u9001\u6D88\u606F\u6062\u590D\u8BE5\u7EBF\u7A0B\u3002`;
13302
13482
  } else {
13303
13483
  response = "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
13304
13484
  }
@@ -13402,7 +13582,8 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13402
13582
  }
13403
13583
  if (response) {
13404
13584
  await deliverBridgeNotice(adapter, msg.address, response, {
13405
- replyToMessageId: msg.messageId
13585
+ replyToMessageId: msg.messageId,
13586
+ audit: auditResponse
13406
13587
  });
13407
13588
  }
13408
13589
  }
@@ -13414,6 +13595,39 @@ import path13 from "node:path";
13414
13595
  import fs8 from "fs";
13415
13596
  import path12 from "path";
13416
13597
  import crypto5 from "crypto";
13598
+
13599
+ // src/lib/bridge/turns/final-response-artifacts.ts
13600
+ function attachmentKey(attachment) {
13601
+ return [
13602
+ attachment.kind,
13603
+ attachment.path,
13604
+ attachment.caption || "",
13605
+ attachment.name || ""
13606
+ ].join("\0");
13607
+ }
13608
+ function dedupeOutboundAttachments(attachments) {
13609
+ const seen = /* @__PURE__ */ new Set();
13610
+ const deduped = [];
13611
+ for (const attachment of attachments) {
13612
+ const key = attachmentKey(attachment);
13613
+ if (seen.has(key)) continue;
13614
+ seen.add(key);
13615
+ deduped.push(attachment);
13616
+ }
13617
+ return deduped;
13618
+ }
13619
+ function collectFinalResponseArtifacts(text2, attachments = []) {
13620
+ const parsed = parseOutboundArtifacts(text2 || "");
13621
+ return {
13622
+ text: parsed.cleanText,
13623
+ attachments: dedupeOutboundAttachments([
13624
+ ...attachments,
13625
+ ...parsed.attachments
13626
+ ])
13627
+ };
13628
+ }
13629
+
13630
+ // src/lib/bridge/conversation-engine.ts
13417
13631
  init_runtime_options();
13418
13632
 
13419
13633
  // src/lib/bridge/sse-stream-decoder.ts
@@ -13792,8 +14006,8 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13792
14006
  if (contentBlocks.length > 0) {
13793
14007
  for (const block2 of contentBlocks) {
13794
14008
  if (block2.type !== "text") continue;
13795
- const parsed = parseOutboundArtifacts(block2.text);
13796
- block2.text = parsed.cleanText;
14009
+ const parsed = collectFinalResponseArtifacts(block2.text);
14010
+ block2.text = parsed.text;
13797
14011
  outboundAttachments.push(...parsed.attachments);
13798
14012
  }
13799
14013
  const hasToolBlocks = contentBlocks.some(
@@ -13807,7 +14021,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13807
14021
  const responseText = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
13808
14022
  return {
13809
14023
  responseText,
13810
- outboundAttachments,
14024
+ outboundAttachments: dedupeOutboundAttachments(outboundAttachments),
13811
14025
  tokenUsage,
13812
14026
  hasError,
13813
14027
  errorMessage,
@@ -13840,6 +14054,42 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
13840
14054
  }
13841
14055
  }
13842
14056
 
14057
+ // src/lib/bridge/turns/response-assembler.ts
14058
+ function assembleFinalResponse(source, input) {
14059
+ const parsed = collectFinalResponseArtifacts(input.text, input.attachments);
14060
+ return {
14061
+ text: parsed.text,
14062
+ attachments: parsed.attachments,
14063
+ hasError: input.hasError,
14064
+ errorMessage: input.errorMessage,
14065
+ source
14066
+ };
14067
+ }
14068
+ function assembleSdkFinalResponse(input) {
14069
+ return assembleFinalResponse("sdk_result", input);
14070
+ }
14071
+ function assembleDesktopFinalResponse(input) {
14072
+ return assembleFinalResponse("desktop_task_complete", input);
14073
+ }
14074
+ function hasFinalResponsePayload(response) {
14075
+ return Boolean(response.text || response.attachments.length > 0);
14076
+ }
14077
+ function mergeFinalResponses(primary, fallback) {
14078
+ return {
14079
+ text: primary.text || fallback.text,
14080
+ attachments: dedupeOutboundAttachments([
14081
+ ...fallback.attachments,
14082
+ ...primary.attachments
14083
+ ]),
14084
+ hasError: primary.hasError ?? fallback.hasError,
14085
+ errorMessage: primary.errorMessage || fallback.errorMessage,
14086
+ source: primary.source
14087
+ };
14088
+ }
14089
+ function stripFinalOnlyBlocksForStreaming(text2) {
14090
+ return stripOutboundArtifactBlocksForStreaming(text2);
14091
+ }
14092
+
13843
14093
  // src/lib/bridge/stream-feedback-controller.ts
13844
14094
  function pushStreamFeedbackText(target, text2) {
13845
14095
  if (typeof target.adapter.onStreamText !== "function") return;
@@ -13868,13 +14118,15 @@ function pushStreamFeedbackTasks(target, tasks) {
13868
14118
  }
13869
14119
  }
13870
14120
  function pushStreamFeedbackStatus(target, text2) {
13871
- if (typeof target.adapter.onStreamStatus !== "function") return;
14121
+ if (typeof target.adapter.onStreamStatus !== "function") return false;
13872
14122
  target.ensureStarted?.();
13873
14123
  const rendered = renderFeedbackTextForChannel(target.channelType, text2);
13874
- if (!rendered) return;
14124
+ if (!rendered) return false;
13875
14125
  try {
13876
14126
  target.adapter.onStreamStatus(target.chatId, rendered, target.streamKey);
14127
+ return true;
13877
14128
  } catch {
14129
+ return false;
13878
14130
  }
13879
14131
  }
13880
14132
  async function finalizeStreamFeedback(target, status, text2) {
@@ -13887,6 +14139,118 @@ async function finalizeStreamFeedback(target, status, text2) {
13887
14139
  }
13888
14140
  }
13889
14141
 
14142
+ // src/lib/bridge/turns/delivery-pipeline.ts
14143
+ function normalizeUnknownSendResult(result) {
14144
+ if (result && typeof result === "object" && "ok" in result) {
14145
+ return result;
14146
+ }
14147
+ return { ok: true };
14148
+ }
14149
+ async function deliverFinalResponse(context, response, options = {}) {
14150
+ let lastResult = { ok: true };
14151
+ const deliverResponse2 = context.deliverResponse || deliverResponse;
14152
+ if (!options.skipText && response.text.trim()) {
14153
+ if (context.deliverText) {
14154
+ lastResult = await context.deliverText(response.text);
14155
+ } else {
14156
+ lastResult = normalizeUnknownSendResult(await deliverResponse2(
14157
+ context.adapter,
14158
+ context.address,
14159
+ response.text,
14160
+ context.sessionId,
14161
+ context.replyToMessageId,
14162
+ []
14163
+ ));
14164
+ }
14165
+ if (!lastResult.ok) return lastResult;
14166
+ }
14167
+ if (response.attachments.length > 0) {
14168
+ lastResult = normalizeUnknownSendResult(await deliverResponse2(
14169
+ context.adapter,
14170
+ context.address,
14171
+ "",
14172
+ context.sessionId,
14173
+ context.replyToMessageId,
14174
+ response.attachments
14175
+ ));
14176
+ if (!lastResult.ok) return lastResult;
14177
+ }
14178
+ return lastResult;
14179
+ }
14180
+ async function finalizeStreamingUi(target, status, response) {
14181
+ return finalizeStreamFeedback(target, status, response.text);
14182
+ }
14183
+
14184
+ // src/lib/bridge/turns/stream-state.ts
14185
+ function createStreamState(startedAtMs) {
14186
+ const safeStartedAtMs = Number.isFinite(startedAtMs) ? startedAtMs : Date.now();
14187
+ return {
14188
+ startedAtMs: safeStartedAtMs,
14189
+ lastActivityAtMs: safeStartedAtMs,
14190
+ lastContentResponseAtMs: null,
14191
+ statusNote: null,
14192
+ lastStatusText: null,
14193
+ lastStatusAtMs: 0
14194
+ };
14195
+ }
14196
+ function recordStreamActivity(state, nowMs) {
14197
+ if (!Number.isFinite(nowMs)) return;
14198
+ state.lastActivityAtMs = Math.max(state.lastActivityAtMs, nowMs);
14199
+ }
14200
+ function recordStreamContentResponse(state, nowMs) {
14201
+ if (!Number.isFinite(nowMs)) return;
14202
+ recordStreamActivity(state, nowMs);
14203
+ state.lastContentResponseAtMs = nowMs;
14204
+ }
14205
+ function updateStreamStatusNote(state, note, nowMs) {
14206
+ state.statusNote = (note || "").trim() || null;
14207
+ if (state.statusNote) {
14208
+ recordStreamActivity(state, nowMs);
14209
+ }
14210
+ }
14211
+ function formatRuntimeDuration(ms) {
14212
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
14213
+ const seconds = totalSeconds % 60;
14214
+ const totalMinutes = Math.floor(totalSeconds / 60);
14215
+ const minutes = totalMinutes % 60;
14216
+ const hours = Math.floor(totalMinutes / 60);
14217
+ const parts = [];
14218
+ if (hours > 0) parts.push(`${hours}\u5C0F\u65F6`);
14219
+ if (minutes > 0) parts.push(`${minutes}\u5206`);
14220
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}\u79D2`);
14221
+ return parts.join("");
14222
+ }
14223
+ function formatStreamRuntimeStatus(elapsedMs, lastContentResponseAgeMs, statusNote) {
14224
+ const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
14225
+ if (typeof lastContentResponseAgeMs === "number" && lastContentResponseAgeMs >= 0) {
14226
+ parts.push(`\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA ${formatRuntimeDuration(lastContentResponseAgeMs)}`);
14227
+ }
14228
+ const runtimeText = parts.join("\uFF0C");
14229
+ const note = (statusNote || "").trim();
14230
+ return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
14231
+ ${runtimeText}` : runtimeText;
14232
+ }
14233
+ function getStreamLastContentResponseAgeMs(state, nowMs, options = {}) {
14234
+ const fallbackToStart = options.fallbackToStart !== false;
14235
+ const base = state.lastContentResponseAtMs ?? (fallbackToStart ? state.startedAtMs : null);
14236
+ if (base == null || !Number.isFinite(base) || !Number.isFinite(nowMs)) return null;
14237
+ return Math.max(0, nowMs - base);
14238
+ }
14239
+ function shouldShowStreamLastContentResponseAge(state, nowMs, config2) {
14240
+ if (!Number.isFinite(nowMs)) return false;
14241
+ const elapsedMs = nowMs - state.startedAtMs;
14242
+ if (elapsedMs < Math.max(0, config2.idleStartMs)) return false;
14243
+ const ageMs = getStreamLastContentResponseAgeMs(state, nowMs);
14244
+ return ageMs != null && ageMs >= Math.max(1e3, config2.heartbeatMs);
14245
+ }
14246
+ function buildStreamRuntimeStatus(state, nowMs, options = {}) {
14247
+ return formatStreamRuntimeStatus(
14248
+ Math.max(0, nowMs - state.startedAtMs),
14249
+ options.includeLastContentResponseAge ? getStreamLastContentResponseAgeMs(state, nowMs) : null,
14250
+ state.statusNote
14251
+ );
14252
+ }
14253
+
13890
14254
  // src/lib/bridge/interactive-message-runner.ts
13891
14255
  function generateDraftId() {
13892
14256
  return Math.floor(Math.random() * 2147483646) + 1;
@@ -13930,18 +14294,6 @@ function flushPreview(adapter, state, config2) {
13930
14294
  }).catch(() => {
13931
14295
  });
13932
14296
  }
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
14297
  function pathBaseName(value) {
13946
14298
  return value.includes("\\") ? path13.win32.basename(value) : path13.basename(value);
13947
14299
  }
@@ -13963,18 +14315,10 @@ function buildStaleTaskCompletionNotice(address, binding) {
13963
14315
  const taskName = formatTaskDisplayName(binding);
13964
14316
  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
14317
  }
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
14318
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13977
14319
  const binding = resolve(msg.address);
14320
+ const initialSession = getBridgeContext().store.getSession(binding.codepilotSessionId);
14321
+ const desktopThreadId = getExplicitDesktopThreadId(initialSession);
13978
14322
  const streamKey = buildInteractiveStreamKey(binding.codepilotSessionId, msg.messageId);
13979
14323
  const nowMs = deps.nowMs ?? (() => Date.now());
13980
14324
  const setIntervalFn = deps.setIntervalFn ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
@@ -14000,11 +14344,14 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14000
14344
  const taskAbort = new AbortController();
14001
14345
  const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
14002
14346
  const taskStartedAt = nowMs();
14347
+ const streamState = createStreamState(taskStartedAt);
14003
14348
  let externalTerminalRequest = null;
14349
+ let desktopTerminalFinalExpected = false;
14004
14350
  let resolveExternalTerminal = null;
14005
14351
  const externalTerminalPromise = new Promise((resolve2) => {
14006
14352
  resolveExternalTerminal = resolve2;
14007
14353
  });
14354
+ let processResultSettled = false;
14008
14355
  let resolveExternalTerminalCompletion = null;
14009
14356
  let externalTerminalCompletionSettled = false;
14010
14357
  const externalTerminalCompletion = new Promise((resolve2) => {
@@ -14028,6 +14375,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14028
14375
  structuredStreamUiActive: false,
14029
14376
  lastActivityAt: taskStartedAt,
14030
14377
  lastResponseAt: null,
14378
+ lastContentResponseAt: null,
14031
14379
  streamFinalized: false,
14032
14380
  uiEnded: false,
14033
14381
  mirrorSuppressionId: null,
@@ -14036,13 +14384,26 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14036
14384
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return false;
14037
14385
  externalTerminalRequest = { outcome, detail, finalText };
14038
14386
  resolveExternalTerminal?.(externalTerminalRequest);
14039
- if (!taskAbort.signal.aborted) {
14387
+ if (!processResultSettled && !taskAbort.signal.aborted) {
14040
14388
  taskAbort.abort();
14041
14389
  }
14042
14390
  return externalTerminalCompletion;
14043
14391
  }
14044
14392
  };
14045
14393
  deps.registerInteractiveTask(taskState);
14394
+ deps.registerBridgeTurn?.({
14395
+ id: taskId,
14396
+ sessionId: binding.codepilotSessionId,
14397
+ kind: desktopThreadId ? "im_desktop_reuse" : "im_sdk",
14398
+ origin: "im",
14399
+ progressSource: "sdk_stream",
14400
+ finalSource: desktopThreadId ? "desktop_task_complete" : "sdk_result",
14401
+ codexThreadId: binding.sdkSessionId || initialSession?.codex_thread_id || initialSession?.sdk_session_id || void 0,
14402
+ desktopThreadId,
14403
+ requestMessageId: msg.messageId,
14404
+ streamKey,
14405
+ startedAt: taskStartedAt
14406
+ });
14046
14407
  deps.recordInteractiveHealthStart(binding.codepilotSessionId);
14047
14408
  let previewState = null;
14048
14409
  const caps = adapter.getPreviewCapabilities?.(msg.address.chatId) ?? null;
@@ -14062,7 +14423,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14062
14423
  const ps = previewState;
14063
14424
  const cfg = streamCfg;
14064
14425
  if (ps.degraded) return;
14065
- const sanitizedText = stripOutboundArtifactBlocksForStreaming(fullText);
14426
+ const sanitizedText = stripFinalOnlyBlocksForStreaming(fullText);
14066
14427
  ps.pendingText = sanitizedText.length > cfg.maxChars ? sanitizedText.slice(0, cfg.maxChars) + "..." : sanitizedText;
14067
14428
  const delta = ps.pendingText.length - ps.lastSentText.length;
14068
14429
  const elapsed = Date.now() - ps.lastSentAt;
@@ -14104,7 +14465,6 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14104
14465
  };
14105
14466
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
14106
14467
  const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
14107
- let latestStatusNote = null;
14108
14468
  let latestTasks = [];
14109
14469
  const syncStructuredStreamUiState = () => {
14110
14470
  if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
@@ -14123,12 +14483,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14123
14483
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
14124
14484
  pushStreamFeedbackStatus(
14125
14485
  streamFeedbackTarget,
14126
- formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, latestStatusNote)
14486
+ lastResponseAgeMs == null ? buildStreamRuntimeStatus(streamState, nowMs()) : formatStreamRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, streamState.statusNote)
14127
14487
  );
14128
14488
  syncStructuredStreamUiSnapshot();
14129
14489
  };
14130
- const markSuccessfulResponse = () => {
14131
- taskState.lastResponseAt = nowMs();
14490
+ const markActivity = () => {
14491
+ const now2 = nowMs();
14492
+ recordStreamActivity(streamState, now2);
14493
+ taskState.lastActivityAt = streamState.lastActivityAtMs;
14494
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
14495
+ };
14496
+ const markContentResponse = () => {
14497
+ const now2 = nowMs();
14498
+ recordStreamContentResponse(streamState, now2);
14499
+ taskState.lastActivityAt = streamState.lastActivityAtMs;
14500
+ taskState.lastResponseAt = streamState.lastContentResponseAtMs;
14501
+ taskState.lastContentResponseAt = streamState.lastContentResponseAtMs;
14132
14502
  deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
14133
14503
  };
14134
14504
  let streamStatusHeartbeat = null;
@@ -14167,10 +14537,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14167
14537
  endPreviewOnce();
14168
14538
  if (hasStreamingCards && !streamUiFinalizeAttempted) {
14169
14539
  streamUiFinalizeAttempted = true;
14170
- taskState.streamFinalized = await finalizeStreamFeedback(
14540
+ taskState.streamFinalized = await finalizeStreamingUi(
14171
14541
  streamFeedbackTarget,
14172
14542
  status,
14173
- responseText
14543
+ assembleDesktopFinalResponse({ text: responseText })
14174
14544
  );
14175
14545
  }
14176
14546
  return taskState.streamFinalized;
@@ -14184,12 +14554,12 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14184
14554
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14185
14555
  pushStreamFeedbackText(
14186
14556
  streamFeedbackTarget,
14187
- stripOutboundArtifactBlocksForStreaming(fullText)
14557
+ stripFinalOnlyBlocksForStreaming(fullText)
14188
14558
  );
14189
14559
  } : void 0;
14190
14560
  const onToolEvent = (toolId, toolName, status) => {
14191
14561
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14192
- markSuccessfulResponse();
14562
+ markActivity();
14193
14563
  deps.recordInteractiveHealthTool(binding.codepilotSessionId, toolId, toolName, status);
14194
14564
  if (toolName) {
14195
14565
  toolCallTracker.set(toolId, { id: toolId, name: toolName, status });
@@ -14205,7 +14575,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14205
14575
  };
14206
14576
  const onTaskEvent = (tasks) => {
14207
14577
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14208
- markSuccessfulResponse();
14578
+ markActivity();
14209
14579
  latestTasks = tasks;
14210
14580
  if (hasStreamingCards) {
14211
14581
  pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
@@ -14215,17 +14585,15 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14215
14585
  };
14216
14586
  const onStatusNote = (note) => {
14217
14587
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14218
- latestStatusNote = (note || "").trim() || null;
14219
- if (latestStatusNote) {
14220
- markSuccessfulResponse();
14221
- }
14588
+ updateStreamStatusNote(streamState, note, nowMs());
14589
+ if (streamState.statusNote) markActivity();
14222
14590
  pushRunningStatus(null);
14223
14591
  syncStructuredStreamUiSnapshot();
14224
14592
  };
14225
14593
  const onPartialText = (fullText) => {
14226
14594
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14227
14595
  if (fullText.trim()) {
14228
- markSuccessfulResponse();
14596
+ markContentResponse();
14229
14597
  }
14230
14598
  deps.recordInteractiveHealthProgress(binding.codepilotSessionId, "text");
14231
14599
  previewOnPartialText?.(fullText);
@@ -14233,6 +14601,34 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14233
14601
  pushRunningStatus(null);
14234
14602
  syncStructuredStreamUiSnapshot();
14235
14603
  };
14604
+ const waitForDesktopTerminalFinalization = async () => {
14605
+ if (externalTerminalRequest) return externalTerminalRequest;
14606
+ const timeoutMs = Math.max(0, deps.desktopTerminalFinalizationTimeoutMs ?? 0);
14607
+ if (!desktopThreadId || !desktopTerminalFinalExpected || timeoutMs <= 0) return null;
14608
+ if (taskAbort.signal.aborted) return null;
14609
+ return new Promise((resolve2) => {
14610
+ let settled = false;
14611
+ let timer = null;
14612
+ const finish = (terminal) => {
14613
+ if (settled) return;
14614
+ settled = true;
14615
+ if (timer) {
14616
+ clearTimeout(timer);
14617
+ timer = null;
14618
+ }
14619
+ taskAbort.signal.removeEventListener("abort", onAbort);
14620
+ resolve2(terminal);
14621
+ };
14622
+ const onAbort = () => finish(null);
14623
+ timer = setTimeout(() => finish(null), timeoutMs);
14624
+ taskAbort.signal.addEventListener("abort", onAbort, { once: true });
14625
+ externalTerminalPromise.then((terminal) => {
14626
+ finish(terminal);
14627
+ }, () => {
14628
+ finish(null);
14629
+ });
14630
+ });
14631
+ };
14236
14632
  if (supportsStructuredStreamUi) {
14237
14633
  pushRunningStatus(null);
14238
14634
  streamStatusHeartbeat = setIntervalFn(() => {
@@ -14245,8 +14641,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14245
14641
  return;
14246
14642
  }
14247
14643
  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;
14644
+ const showLastResponseAge = shouldShowStreamLastContentResponseAge(streamState, nowMs(), {
14645
+ idleStartMs: streamStatusIdleDetectionStartMs,
14646
+ heartbeatMs: streamStatusHeartbeatMs
14647
+ }) ? getStreamLastContentResponseAgeMs(streamState, nowMs()) : null;
14250
14648
  pushRunningStatus(showLastResponseAge);
14251
14649
  syncStructuredStreamUiSnapshot();
14252
14650
  }, streamStatusHeartbeatMs);
@@ -14254,6 +14652,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14254
14652
  let finalOutcome = "failed";
14255
14653
  let finalOutcomeDetail;
14256
14654
  let shouldRecordHealthEnd = true;
14655
+ let forceStopStarted = false;
14656
+ taskState.forceStop = async (detail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002") => {
14657
+ if (forceStopStarted) return true;
14658
+ forceStopStarted = true;
14659
+ finalOutcome = "aborted";
14660
+ finalOutcomeDetail = detail;
14661
+ taskAbort.abort();
14662
+ stopStructuredStreamStatusUpdates();
14663
+ endPreviewOnce();
14664
+ try {
14665
+ await finalizeStreamUiOnce("interrupted", detail);
14666
+ } catch {
14667
+ }
14668
+ endMessageUiOnce();
14669
+ return true;
14670
+ };
14257
14671
  try {
14258
14672
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
14259
14673
  const processPromise = processMessageImpl(
@@ -14275,7 +14689,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14275
14689
  "permission_wait",
14276
14690
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
14277
14691
  );
14278
- markSuccessfulResponse();
14692
+ markActivity();
14279
14693
  pushRunningStatus(null);
14280
14694
  syncStructuredStreamUiSnapshot();
14281
14695
  },
@@ -14286,7 +14700,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14286
14700
  onTaskEvent,
14287
14701
  onStatusNote,
14288
14702
  (preparedPrompt) => {
14289
- if (!taskState.mirrorSuppressionId) {
14703
+ if (desktopThreadId) {
14704
+ desktopTerminalFinalExpected = true;
14705
+ }
14706
+ if (desktopThreadId && !taskState.mirrorSuppressionId) {
14290
14707
  taskState.mirrorSuppressionId = deps.beginMirrorSuppression(binding.codepilotSessionId, preparedPrompt);
14291
14708
  }
14292
14709
  }
@@ -14308,73 +14725,87 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14308
14725
  finalOutcomeDetail = raced.terminal.detail;
14309
14726
  const streamEndStatus = raced.terminal.outcome === "completed" ? "completed" : raced.terminal.outcome === "aborted" ? "interrupted" : "error";
14310
14727
  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(
14728
+ const terminalResponse2 = assembleDesktopFinalResponse({
14729
+ text: staleTaskNotice2 || raced.terminal.finalText || ""
14730
+ });
14731
+ const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalResponse2.text);
14732
+ if (hasFinalResponsePayload(terminalResponse2)) {
14733
+ await deliverFinalResponse({
14315
14734
  adapter,
14316
- msg.address,
14317
- terminalText,
14318
- binding.codepilotSessionId,
14319
- msg.messageId,
14320
- []
14321
- );
14735
+ address: msg.address,
14736
+ sessionId: binding.codepilotSessionId,
14737
+ replyToMessageId: msg.messageId,
14738
+ deliverResponse: deps.deliverResponse
14739
+ }, terminalResponse2, { skipText: cardFinalized2 });
14322
14740
  }
14323
14741
  return;
14324
14742
  }
14325
14743
  const result = raced.result;
14744
+ processResultSettled = true;
14326
14745
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
14327
14746
  shouldRecordHealthEnd = false;
14328
14747
  return;
14329
14748
  }
14749
+ const terminalAfterProcess = await waitForDesktopTerminalFinalization();
14750
+ const terminalResponse = terminalAfterProcess?.outcome === "completed" ? assembleDesktopFinalResponse({ text: terminalAfterProcess.finalText || "" }) : null;
14751
+ const sdkResponse = assembleSdkFinalResponse({
14752
+ text: result.responseText,
14753
+ attachments: result.outboundAttachments,
14754
+ hasError: result.hasError,
14755
+ errorMessage: result.errorMessage
14756
+ });
14757
+ const terminalHasFinalPayload = Boolean(
14758
+ terminalResponse && hasFinalResponsePayload(terminalResponse)
14759
+ );
14760
+ const effectiveResponse = terminalResponse && terminalHasFinalPayload ? mergeFinalResponses(terminalResponse, sdkResponse) : sdkResponse;
14330
14761
  let cardFinalized = false;
14331
14762
  const staleTaskNotice = buildStaleTaskCompletionNotice(msg.address, binding);
14763
+ const staleResponse = staleTaskNotice ? assembleDesktopFinalResponse({ text: staleTaskNotice }) : null;
14332
14764
  if (hasStreamingCards) {
14333
- const streamEndStatus = taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14765
+ const streamEndStatus = terminalAfterProcess ? terminalAfterProcess.outcome === "completed" ? "completed" : terminalAfterProcess.outcome === "aborted" ? "interrupted" : "error" : taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14334
14766
  cardFinalized = await finalizeStreamUiOnce(
14335
14767
  streamEndStatus,
14336
- staleTaskNotice || (streamEndStatus === "interrupted" ? "" : result.responseText)
14768
+ staleResponse?.text || (streamEndStatus === "interrupted" ? "" : effectiveResponse.text)
14337
14769
  );
14338
14770
  }
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(
14771
+ if (staleResponse) {
14772
+ await deliverFinalResponse({
14364
14773
  adapter,
14365
- msg.address,
14366
- `**Error:** ${result.errorMessage}`,
14367
- binding.codepilotSessionId,
14368
- msg.messageId,
14369
- []
14774
+ address: msg.address,
14775
+ sessionId: binding.codepilotSessionId,
14776
+ replyToMessageId: msg.messageId,
14777
+ deliverResponse: deps.deliverResponse
14778
+ }, staleResponse, { skipText: cardFinalized });
14779
+ } else if (hasFinalResponsePayload(effectiveResponse)) {
14780
+ await deliverFinalResponse({
14781
+ adapter,
14782
+ address: msg.address,
14783
+ sessionId: binding.codepilotSessionId,
14784
+ replyToMessageId: msg.messageId,
14785
+ deliverResponse: deps.deliverResponse
14786
+ }, effectiveResponse, { skipText: cardFinalized });
14787
+ } else if (result.hasError && !taskAbort.signal.aborted) {
14788
+ await deliverFinalResponse(
14789
+ {
14790
+ adapter,
14791
+ address: msg.address,
14792
+ sessionId: binding.codepilotSessionId,
14793
+ replyToMessageId: msg.messageId,
14794
+ deliverResponse: deps.deliverResponse
14795
+ },
14796
+ assembleSdkFinalResponse({
14797
+ text: `**Error:** ${result.errorMessage}`,
14798
+ hasError: true,
14799
+ errorMessage: result.errorMessage
14800
+ })
14370
14801
  );
14371
14802
  }
14372
14803
  try {
14373
14804
  deps.persistSdkSessionUpdate(binding.codepilotSessionId, result.sdkSessionId, result.hasError);
14374
14805
  } catch {
14375
14806
  }
14376
- finalOutcome = result.hasError ? "failed" : "completed";
14377
- finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
14807
+ finalOutcome = terminalAfterProcess?.outcome || (result.hasError ? "failed" : "completed");
14808
+ finalOutcomeDetail = terminalAfterProcess?.detail || (result.hasError ? result.errorMessage?.trim() || void 0 : void 0);
14378
14809
  } finally {
14379
14810
  await finalizeStreamUiOnce(
14380
14811
  taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
@@ -14391,11 +14822,12 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14391
14822
  if (shouldRecordHealthEnd) {
14392
14823
  if (taskAbort.signal.aborted && !externalTerminalRequest) {
14393
14824
  finalOutcome = "aborted";
14394
- finalOutcomeDetail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14825
+ finalOutcomeDetail = finalOutcomeDetail || "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14395
14826
  }
14396
14827
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14397
14828
  }
14398
14829
  deps.releaseInteractiveTask(binding.codepilotSessionId, taskId);
14830
+ deps.releaseBridgeTurn?.(binding.codepilotSessionId, taskId);
14399
14831
  endMessageUiOnce();
14400
14832
  settleExternalTerminalCompletion(taskState.streamFinalized || !hasStreamingCards);
14401
14833
  }
@@ -14410,13 +14842,14 @@ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
14410
14842
  function isTerminalSessionHealthStatus(status) {
14411
14843
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14412
14844
  }
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
14845
  function createInteractiveRuntime(getState2, deps) {
14846
+ const sessionLockVersions = /* @__PURE__ */ new Map();
14847
+ function getSessionLockVersion(sessionId) {
14848
+ return sessionLockVersions.get(sessionId) || 0;
14849
+ }
14850
+ function invalidateSessionLockQueue(sessionId) {
14851
+ sessionLockVersions.set(sessionId, getSessionLockVersion(sessionId) + 1);
14852
+ }
14420
14853
  function getQueuedCount(sessionId) {
14421
14854
  return getState2().queuedCounts.get(sessionId) || 0;
14422
14855
  }
@@ -14463,18 +14896,28 @@ function createInteractiveRuntime(getState2, deps) {
14463
14896
  if (!task?.finalizeFromExternalTerminal) return false;
14464
14897
  return task.finalizeFromExternalTerminal(outcome, detail, finalText);
14465
14898
  }
14899
+ async function forceStopSession(sessionId, detail) {
14900
+ const state = getState2();
14901
+ const task = state.activeTasks.get(sessionId);
14902
+ let handled = false;
14903
+ if (task?.forceStop) {
14904
+ handled = await task.forceStop(detail);
14905
+ } else if (task) {
14906
+ task.abortController.abort();
14907
+ handled = true;
14908
+ }
14909
+ state.activeTasks.delete(sessionId);
14910
+ state.queuedCounts.delete(sessionId);
14911
+ state.sessionLocks.delete(sessionId);
14912
+ invalidateSessionLockQueue(sessionId);
14913
+ syncSessionRuntimeState(sessionId);
14914
+ return handled;
14915
+ }
14466
14916
  async function reconcileTerminalSessionRuntimeState() {
14467
14917
  const store = deps.getStore();
14468
14918
  for (const session of store.listSessions()) {
14469
14919
  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
- }
14920
+ if (getState2().activeTasks.has(session.id)) continue;
14478
14921
  const queuedCount = getQueuedCount(session.id);
14479
14922
  const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
14480
14923
  if (queuedCount > 0) continue;
@@ -14521,6 +14964,7 @@ function createInteractiveRuntime(getState2, deps) {
14521
14964
  const state = getState2();
14522
14965
  const prev = state.sessionLocks.get(sessionId) || Promise.resolve();
14523
14966
  const queued = state.sessionLocks.has(sessionId);
14967
+ const lockVersion = getSessionLockVersion(sessionId);
14524
14968
  if (queued) {
14525
14969
  incrementQueuedCount(sessionId);
14526
14970
  }
@@ -14528,6 +14972,7 @@ function createInteractiveRuntime(getState2, deps) {
14528
14972
  if (queued) {
14529
14973
  decrementQueuedCount(sessionId);
14530
14974
  }
14975
+ if (getSessionLockVersion(sessionId) !== lockVersion) return;
14531
14976
  await fn();
14532
14977
  };
14533
14978
  const current = prev.then(wrapped, wrapped);
@@ -14549,6 +14994,7 @@ function createInteractiveRuntime(getState2, deps) {
14549
14994
  releaseInteractiveTask,
14550
14995
  syncSessionRuntimeState,
14551
14996
  finalizeTerminalActiveTask,
14997
+ forceStopSession,
14552
14998
  reconcileTerminalSessionRuntimeState,
14553
14999
  resetPersistedInteractiveRuntimeState,
14554
15000
  processWithSessionLock
@@ -14887,7 +15333,7 @@ function buildMirrorSubscriptionRegistryPlan(bindings, activeChannelTypes, exist
14887
15333
  if (binding.active === false) return false;
14888
15334
  if (!activeChannels.has(binding.channelType)) return false;
14889
15335
  const session = getSession(binding.codepilotSessionId);
14890
- return Boolean(binding.sdkSessionId || session?.sdk_session_id);
15336
+ return Boolean(session?.desktop_thread_id || (session?.thread_origin === "desktop" ? session.sdk_session_id : null));
14891
15337
  });
14892
15338
  const desiredIds = new Set(upsertBindings.map((binding) => binding.id));
14893
15339
  const removeBindingIds = Array.from(existingBindingIds).filter((bindingId) => !desiredIds.has(bindingId));
@@ -14981,11 +15427,17 @@ function createMirrorRuntime(getState2, options, deps) {
14981
15427
  function clearDanglingMirrorThread(subscription, reason) {
14982
15428
  const { store } = getBridgeContext();
14983
15429
  const session = store.getSession(subscription.sessionId);
14984
- const currentThreadId = session?.sdk_session_id || subscription.threadId;
15430
+ const currentThreadId = getExplicitDesktopThreadId(session) || subscription.threadId;
14985
15431
  console.warn(
14986
15432
  `[bridge-manager] Clearing dangling desktop thread ${currentThreadId} for session ${subscription.sessionId}: ${reason}`
14987
15433
  );
14988
15434
  store.updateSdkSessionId(subscription.sessionId, "");
15435
+ store.updateSession(subscription.sessionId, {
15436
+ sdk_session_id: "",
15437
+ codex_thread_id: void 0,
15438
+ desktop_thread_id: void 0,
15439
+ thread_origin: void 0
15440
+ });
14989
15441
  removeMirrorSubscription(subscription.bindingId);
14990
15442
  }
14991
15443
  function upsertMirrorSubscription(binding) {
@@ -14996,7 +15448,7 @@ function createMirrorRuntime(getState2, options, deps) {
14996
15448
  removeMirrorSubscription(binding.id);
14997
15449
  return;
14998
15450
  }
14999
- const threadId = binding.sdkSessionId || session.sdk_session_id || "";
15451
+ const threadId = getExplicitDesktopThreadId(session) || "";
15000
15452
  if (!threadId) {
15001
15453
  removeMirrorSubscription(binding.id);
15002
15454
  return;
@@ -15109,11 +15561,13 @@ function createMirrorRuntime(getState2, options, deps) {
15109
15561
  `[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
15110
15562
  );
15111
15563
  }
15112
- if (deliverableRecords.length > 0) {
15113
- deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, deliverableRecords);
15564
+ const routeResult = deliverableRecords.length > 0 && deps.routeDesktopRecords ? await deps.routeDesktopRecords(subscription.sessionId, subscription.threadId, deliverableRecords) : { claimed: [], unclaimed: deliverableRecords, terminalClaimed: false };
15565
+ const mirrorRecords = routeResult.unclaimed;
15566
+ if (mirrorRecords.length > 0) {
15567
+ deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, mirrorRecords);
15114
15568
  }
15115
- const blocked = getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId);
15116
- const deliveryPlan = buildMirrorDeliveryPlan(subscription, deliverableRecords, {
15569
+ const blocked = getState2().activeTasks.has(subscription.sessionId);
15570
+ const deliveryPlan = buildMirrorDeliveryPlan(subscription, mirrorRecords, {
15117
15571
  blocked,
15118
15572
  filterSuppressedRecords: deps.filterSuppressedMirrorRecords,
15119
15573
  flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
@@ -15210,6 +15664,241 @@ function createMirrorRuntime(getState2, options, deps) {
15210
15664
  };
15211
15665
  }
15212
15666
 
15667
+ // src/lib/bridge/mirror-feedback-controller.ts
15668
+ function createMirrorStreamFeedbackTarget(subscription, turnState, adapter, startMirrorStreaming) {
15669
+ return {
15670
+ adapter,
15671
+ channelType: subscription.channelType,
15672
+ chatId: subscription.chatId,
15673
+ streamKey: turnState.streamKey,
15674
+ ensureStarted: () => {
15675
+ startMirrorStreaming(subscription, turnState);
15676
+ }
15677
+ };
15678
+ }
15679
+ function createMirrorFeedbackController(deps) {
15680
+ function getMirrorStreamingAdapter(subscription) {
15681
+ const adapter = deps.getAdapter(subscription.channelType);
15682
+ if (!adapter || !adapter.isRunning()) return null;
15683
+ if (getChannelProviderKey(subscription.channelType) !== "feishu") return null;
15684
+ if (typeof adapter.onStreamText !== "function" || typeof adapter.onStreamEnd !== "function") {
15685
+ return null;
15686
+ }
15687
+ return adapter;
15688
+ }
15689
+ function getMirrorStreamingText(subscription, turnState) {
15690
+ const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
15691
+ const markdown = getFeedbackParseMode(subscription.channelType) === "Markdown";
15692
+ const rendered = formatMirrorMessage(
15693
+ title,
15694
+ turnState.userText,
15695
+ stripOutboundArtifactBlocksForStreaming(turnState.streamedText),
15696
+ markdown,
15697
+ true
15698
+ );
15699
+ return rendered || buildMirrorTitle(title, markdown);
15700
+ }
15701
+ function startMirrorStreaming(subscription, turnState) {
15702
+ const adapter = getMirrorStreamingAdapter(subscription);
15703
+ if (!adapter || turnState.streamStarted) return;
15704
+ try {
15705
+ adapter.onMirrorStreamStart?.(subscription.chatId, turnState.streamKey);
15706
+ if (!adapter.onMirrorStreamStart) {
15707
+ adapter.onStreamText?.(subscription.chatId, "", turnState.streamKey);
15708
+ }
15709
+ turnState.streamStarted = true;
15710
+ } catch {
15711
+ }
15712
+ }
15713
+ function createStreamTarget(subscription, turnState, adapter) {
15714
+ return createMirrorStreamFeedbackTarget(subscription, turnState, adapter, startMirrorStreaming);
15715
+ }
15716
+ function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
15717
+ const adapter = getMirrorStreamingAdapter(subscription);
15718
+ if (!adapter || typeof adapter.onStreamStatus !== "function") return;
15719
+ if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
15720
+ const startedAtMs = Date.parse(turnState.startedAt);
15721
+ if (!Number.isFinite(startedAtMs)) return;
15722
+ const nowMs = options.nowMs ?? Date.now();
15723
+ const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
15724
+ if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
15725
+ return;
15726
+ }
15727
+ const statusText = formatStreamRuntimeStatus(
15728
+ Math.max(0, nowMs - startedAtMs),
15729
+ options.lastResponseAgeMs,
15730
+ turnState.statusNote
15731
+ );
15732
+ if (turnState.lastStatusText === statusText) return;
15733
+ const pushed = pushStreamFeedbackStatus(
15734
+ createStreamTarget(subscription, turnState, adapter),
15735
+ statusText
15736
+ );
15737
+ if (!pushed) return;
15738
+ turnState.lastStatusText = statusText;
15739
+ turnState.lastStatusAt = nowMs;
15740
+ }
15741
+ function refreshMirrorStreamingStatus2(subscription, nowMs = Date.now(), config2) {
15742
+ const pendingTurn = subscription.pendingTurn;
15743
+ if (!pendingTurn?.streamStarted) return;
15744
+ const startedAtMs = Date.parse(pendingTurn.startedAt);
15745
+ if (!Number.isFinite(startedAtMs)) return;
15746
+ const lastContentResponseAtMs = pendingTurn.lastContentResponseAt ? Date.parse(pendingTurn.lastContentResponseAt) : pendingTurn.lastResponseAt ? Date.parse(pendingTurn.lastResponseAt) : null;
15747
+ const streamState = {
15748
+ startedAtMs,
15749
+ lastContentResponseAtMs: Number.isFinite(lastContentResponseAtMs) ? lastContentResponseAtMs : null
15750
+ };
15751
+ if (!shouldShowStreamLastContentResponseAge(streamState, nowMs, config2)) return;
15752
+ pushMirrorStreamingStatus(subscription, pendingTurn, {
15753
+ nowMs,
15754
+ lastResponseAgeMs: getStreamLastContentResponseAgeMs(streamState, nowMs),
15755
+ minIntervalMs: config2.heartbeatMs
15756
+ });
15757
+ }
15758
+ function updateMirrorStreaming(subscription, turnState) {
15759
+ const adapter = getMirrorStreamingAdapter(subscription);
15760
+ if (!adapter) return;
15761
+ pushStreamFeedbackText(
15762
+ createStreamTarget(subscription, turnState, adapter),
15763
+ getMirrorStreamingText(subscription, turnState)
15764
+ );
15765
+ pushMirrorStreamingStatus(subscription, turnState);
15766
+ }
15767
+ function updateMirrorToolProgress(subscription, turnState) {
15768
+ const adapter = getMirrorStreamingAdapter(subscription);
15769
+ if (!adapter) return;
15770
+ pushStreamFeedbackTools(
15771
+ createStreamTarget(subscription, turnState, adapter),
15772
+ Array.from(turnState.toolCalls.values())
15773
+ );
15774
+ pushMirrorStreamingStatus(subscription, turnState);
15775
+ }
15776
+ function updateMirrorTaskProgress(subscription, turnState) {
15777
+ const adapter = getMirrorStreamingAdapter(subscription);
15778
+ if (!adapter) return;
15779
+ pushStreamFeedbackTasks(
15780
+ createStreamTarget(subscription, turnState, adapter),
15781
+ turnState.taskItems
15782
+ );
15783
+ pushMirrorStreamingStatus(subscription, turnState);
15784
+ }
15785
+ function updateMirrorStatusProgress(subscription, turnState) {
15786
+ const adapter = getMirrorStreamingAdapter(subscription);
15787
+ if (!adapter) return;
15788
+ pushMirrorStreamingStatus(subscription, turnState);
15789
+ }
15790
+ function stopMirrorStreaming2(subscription, status = "interrupted") {
15791
+ const adapter = getMirrorStreamingAdapter(subscription);
15792
+ const pendingTurn = subscription.pendingTurn;
15793
+ if (!adapter || !pendingTurn?.streamStarted) return;
15794
+ void finalizeStreamFeedback(
15795
+ createStreamTarget(subscription, pendingTurn, adapter),
15796
+ status,
15797
+ getMirrorStreamingText(subscription, pendingTurn)
15798
+ );
15799
+ }
15800
+ async function deliverMirrorTurn(subscription, turn) {
15801
+ const adapter = deps.getAdapter(subscription.channelType);
15802
+ if (!adapter || !adapter.isRunning()) return;
15803
+ const title = deps.getThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
15804
+ const responseParseMode = getFeedbackParseMode(subscription.channelType);
15805
+ const markdown = responseParseMode === "Markdown";
15806
+ const rawFinalResponse = assembleDesktopFinalResponse({ text: turn.text });
15807
+ const attachments = rawFinalResponse.attachments;
15808
+ const cleanTurnText = rawFinalResponse.text;
15809
+ const renderedTextBase = formatMirrorMessage(title, turn.userText, cleanTurnText, markdown);
15810
+ const renderedStreamTextBase = formatMirrorMessage(title, turn.userText, cleanTurnText, markdown, true);
15811
+ const renderedText = turn.timedOut ? appendMirrorTimeoutNotice(renderedTextBase || buildMirrorTitle(title, markdown), markdown) : renderedTextBase;
15812
+ const renderedStreamText = turn.timedOut ? appendMirrorTimeoutNotice(renderedStreamTextBase || buildMirrorTitle(title, markdown), markdown) : renderedStreamTextBase;
15813
+ const text2 = renderedText ? renderFeedbackText(renderedText, responseParseMode) : "";
15814
+ const streamText = renderFeedbackText(
15815
+ renderedStreamText || buildMirrorTitle(title, markdown),
15816
+ responseParseMode
15817
+ );
15818
+ const address = {
15819
+ channelType: subscription.channelType,
15820
+ chatId: subscription.chatId
15821
+ };
15822
+ if (getChannelProviderKey(subscription.channelType) === "feishu" && typeof adapter.onStreamEnd === "function") {
15823
+ try {
15824
+ const finalized = await adapter.onStreamEnd(
15825
+ subscription.chatId,
15826
+ turn.status,
15827
+ streamText,
15828
+ turn.streamKey
15829
+ );
15830
+ if (finalized) {
15831
+ if (attachments.length > 0) {
15832
+ const attachmentResult = await deliverFinalResponse(
15833
+ {
15834
+ adapter,
15835
+ address,
15836
+ sessionId: subscription.sessionId,
15837
+ deliverResponse: deps.deliverResponse
15838
+ },
15839
+ assembleDesktopFinalResponse({ attachments }),
15840
+ { skipText: true }
15841
+ );
15842
+ if (!attachmentResult.ok) {
15843
+ throw new Error(attachmentResult.error || "mirror attachment delivery failed");
15844
+ }
15845
+ }
15846
+ subscription.lastDeliveredAt = turn.timestamp || deps.nowIso();
15847
+ return;
15848
+ }
15849
+ } catch (error) {
15850
+ console.warn("[bridge-manager] Mirror stream finalize failed:", error instanceof Error ? error.message : error);
15851
+ }
15852
+ }
15853
+ const finalResponse = assembleDesktopFinalResponse({
15854
+ text: text2,
15855
+ attachments
15856
+ });
15857
+ if (!finalResponse.text && finalResponse.attachments.length === 0) return;
15858
+ const response = await deliverFinalResponse({
15859
+ adapter,
15860
+ address,
15861
+ sessionId: subscription.sessionId,
15862
+ deliverResponse: deps.deliverResponse,
15863
+ deliverText: async (messageText) => deliver(adapter, {
15864
+ address,
15865
+ text: messageText,
15866
+ parseMode: responseParseMode
15867
+ }, {
15868
+ sessionId: subscription.sessionId,
15869
+ dedupKey: `mirror:${subscription.bindingId}:${turn.signature}`
15870
+ })
15871
+ }, finalResponse);
15872
+ if (!response.ok) {
15873
+ throw new Error(response.error || "mirror delivery failed");
15874
+ }
15875
+ subscription.lastDeliveredAt = turn.timestamp || deps.nowIso();
15876
+ }
15877
+ async function deliverMirrorTurns2(subscription, turns) {
15878
+ let deliveredCount = 0;
15879
+ for (const turn of turns.slice(0, deps.eventBatchLimit)) {
15880
+ try {
15881
+ await deliverMirrorTurn(subscription, turn);
15882
+ deliveredCount += 1;
15883
+ } catch (error) {
15884
+ return { deliveredCount, error };
15885
+ }
15886
+ }
15887
+ return { deliveredCount };
15888
+ }
15889
+ return {
15890
+ hooks: {
15891
+ onStreamText: updateMirrorStreaming,
15892
+ onStatusProgress: updateMirrorStatusProgress,
15893
+ onTaskProgress: updateMirrorTaskProgress,
15894
+ onToolProgress: updateMirrorToolProgress
15895
+ },
15896
+ refreshMirrorStreamingStatus: refreshMirrorStreamingStatus2,
15897
+ stopMirrorStreaming: stopMirrorStreaming2,
15898
+ deliverMirrorTurns: deliverMirrorTurns2
15899
+ };
15900
+ }
15901
+
15213
15902
  // src/lib/bridge/session-health-process.ts
15214
15903
  import { execFile } from "node:child_process";
15215
15904
  import { promisify } from "node:util";
@@ -15302,7 +15991,6 @@ async function probeCodexThreadProcess(threadId) {
15302
15991
  var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
15303
15992
  var HEALTH_SLOW_OBSERVED_MS = 30 * 60 * 1e3;
15304
15993
  var HEALTH_PROGRESS_PERSIST_THROTTLE_MS = 15 * 1e3;
15305
- var HEALTH_PROCESS_PROBE_CACHE_MS = 30 * 1e3;
15306
15994
  var HEALTH_STREAM_UI_STALL_MS = 60 * 1e3;
15307
15995
  var RUNNING_HEALTH_STATUSES = /* @__PURE__ */ new Set([
15308
15996
  "running_active",
@@ -15435,14 +16123,34 @@ function computeBaseDiagnosis(session, nowMs) {
15435
16123
  const lastStreamUiError = trimOrNull(session.last_stream_ui_error);
15436
16124
  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
16125
  const sdkSessionId = trimOrNull(session.sdk_session_id);
15438
- const checkedAt = trimOrNull(session.last_health_check_at);
15439
16126
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
15440
16127
  const previousStatus = session.health_status || "idle";
16128
+ if (!isRunningRuntimeStatus(runtimeStatus) && isRunningHealthStatus(previousStatus)) {
16129
+ return {
16130
+ sessionId: session.id,
16131
+ checkedAt: null,
16132
+ runtimeStatus,
16133
+ healthStatus: "idle",
16134
+ healthReason: "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002",
16135
+ lastProgressAt,
16136
+ lastProgressType,
16137
+ activeToolName,
16138
+ activeToolStartedAt,
16139
+ lastToolFinishedAt,
16140
+ lastStreamUiAttemptAt,
16141
+ lastStreamUiUpdateAt,
16142
+ streamUiFlushStartedAt,
16143
+ lastStreamUiErrorAt,
16144
+ lastStreamUiError,
16145
+ streamUiConsecutiveFailures,
16146
+ sdkSessionId
16147
+ };
16148
+ }
15441
16149
  if (!lastProgressMs) {
15442
16150
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15443
16151
  return {
15444
16152
  sessionId: session.id,
15445
- checkedAt,
16153
+ checkedAt: null,
15446
16154
  runtimeStatus,
15447
16155
  healthStatus: fallbackStatus,
15448
16156
  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"),
@@ -15470,17 +16178,17 @@ function computeBaseDiagnosis(session, nowMs) {
15470
16178
  );
15471
16179
  } else if (idleMs <= HEALTH_RECENT_PROGRESS_MS) {
15472
16180
  healthStatus = activeToolName ? "waiting_tool" : "running_active";
15473
- healthReason = activeToolName ? `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${activeToolName}\u3002` : "\u6700\u8FD1 10 \u5206\u949F\u5185\u4ECD\u6709\u65B0\u8FDB\u5C55\u3002";
16181
+ healthReason = activeToolName ? `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${activeToolName}\u3002` : "\u8FD1\u671F\u4ECD\u6709\u65B0\u8FDB\u5C55\u3002";
15474
16182
  } else if (idleMs <= HEALTH_SLOW_OBSERVED_MS) {
15475
16183
  healthStatus = activeToolName ? "waiting_tool" : "slow_observed";
15476
- healthReason = activeToolName ? `\u5DE5\u5177 ${activeToolName} \u5DF2\u8FD0\u884C\u8F83\u4E45\uFF0C\u4F46\u4ECD\u5728\u89C2\u5BDF\u7A97\u53E3\u5185\u3002` : "\u6700\u8FD1 10 \u5230 30 \u5206\u949F\u5185\u6CA1\u6709\u65B0\u8FDB\u5C55\uFF0C\u5148\u6807\u8BB0\u4E3A\u5F85\u89C2\u5BDF\u3002";
16184
+ healthReason = activeToolName ? `\u5DE5\u5177 ${activeToolName} \u5DF2\u8FD0\u884C\u8F83\u4E45\uFF0C\u4F46\u4ECD\u5728\u89C2\u5BDF\u7A97\u53E3\u5185\u3002` : "\u8FD1\u671F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8FDB\u5C55\uFF0C\u5148\u6807\u8BB0\u4E3A\u5F85\u89C2\u5BDF\u3002";
15477
16185
  } else {
15478
16186
  healthStatus = "suspected_stall";
15479
16187
  healthReason = activeToolName ? `\u5DE5\u5177 ${activeToolName} \u5DF2\u957F\u65F6\u95F4\u6CA1\u6709\u65B0\u8FDB\u5C55\uFF0C\u7591\u4F3C\u5361\u4F4F\u3002` : "\u5DF2\u7ECF\u8D85\u8FC7 30 \u5206\u949F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8FDB\u5C55\uFF0C\u7591\u4F3C\u5361\u4F4F\u3002";
15480
16188
  }
15481
16189
  return {
15482
16190
  sessionId: session.id,
15483
- checkedAt,
16191
+ checkedAt: null,
15484
16192
  runtimeStatus,
15485
16193
  healthStatus,
15486
16194
  healthReason,
@@ -15540,9 +16248,6 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15540
16248
  return diagnosis;
15541
16249
  }
15542
16250
  const lastProgressMs = parseIsoMs(diagnosis.lastProgressAt || void 0);
15543
- if (!lastProgressMs || nowMs - lastProgressMs > HEALTH_RECENT_PROGRESS_MS) {
15544
- return diagnosis;
15545
- }
15546
16251
  const lastStreamUiUpdateMs = parseIsoMs(diagnosis.lastStreamUiUpdateAt || void 0);
15547
16252
  const lastStreamUiAttemptMs = parseIsoMs(diagnosis.lastStreamUiAttemptAt || void 0);
15548
16253
  const streamUiFlushStartedMs = parseIsoMs(diagnosis.streamUiFlushStartedAt || void 0);
@@ -15561,7 +16266,21 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15561
16266
  healthReason: details.join(" ")
15562
16267
  };
15563
16268
  }
15564
- if (lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
16269
+ if (lastStreamUiAttemptMs && (!lastStreamUiUpdateMs || lastStreamUiAttemptMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS)) {
16270
+ const details = ["\u4EFB\u52A1\u4ECD\u5728\u7EE7\u7EED\uFF0C\u4F46\u6D41\u5F0F UI \u6709\u5237\u65B0\u5C1D\u8BD5\u672A\u6210\u529F\u8DDF\u8FDB\uFF0C\u7591\u4F3C\u505C\u66F4\u3002"];
16271
+ if (diagnosis.streamUiConsecutiveFailures > 0) {
16272
+ details.push(`\u6700\u8FD1\u8FDE\u7EED\u5931\u8D25 ${diagnosis.streamUiConsecutiveFailures} \u6B21\u3002`);
16273
+ }
16274
+ if (lastStreamUiErrorText) {
16275
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
16276
+ }
16277
+ return {
16278
+ ...diagnosis,
16279
+ healthStatus: "suspected_stream_ui_stall",
16280
+ healthReason: details.join(" ")
16281
+ };
16282
+ }
16283
+ if (lastProgressMs && lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
15565
16284
  const details = ["\u4EFB\u52A1\u4ECD\u5728\u7EE7\u7EED\uFF0C\u4F46\u6D41\u5F0F UI \u5DF2\u957F\u65F6\u95F4\u6CA1\u6709\u8DDF\u4E0A\u6700\u65B0\u6267\u884C\u8FDB\u5C55\uFF0C\u7591\u4F3C\u505C\u66F4\u3002"];
15566
16285
  if (lastStreamUiErrorText) {
15567
16286
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -15572,7 +16291,7 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15572
16291
  healthReason: details.join(" ")
15573
16292
  };
15574
16293
  }
15575
- if (!lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
16294
+ if (lastProgressMs && !lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
15576
16295
  const details = ["\u4EFB\u52A1\u4ECD\u5728\u7EE7\u7EED\uFF0C\u4F46\u6D41\u5F0F UI \u53EA\u6709\u53D1\u9001\u5C1D\u8BD5\u3001\u6CA1\u6709\u6210\u529F\u5237\u65B0\u8BB0\u5F55\uFF0C\u7591\u4F3C\u505C\u66F4\u3002"];
15577
16296
  if (lastStreamUiErrorText) {
15578
16297
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -15583,13 +16302,30 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
15583
16302
  healthReason: details.join(" ")
15584
16303
  };
15585
16304
  }
16305
+ const lastStreamUiActivityMs = Math.max(
16306
+ lastStreamUiAttemptMs || 0,
16307
+ lastStreamUiUpdateMs || 0,
16308
+ streamUiFlushStartedMs || 0
16309
+ );
16310
+ if (lastStreamUiActivityMs > 0 && nowMs - lastStreamUiActivityMs >= HEALTH_STREAM_UI_STALL_MS) {
16311
+ return {
16312
+ ...diagnosis,
16313
+ healthStatus: "suspected_stream_ui_stall",
16314
+ healthReason: "\u4EFB\u52A1\u4ECD\u5728\u7EE7\u7EED\uFF0C\u4F46\u6D41\u5F0F UI \u72B6\u6001\u5237\u65B0\u5DF2\u505C\u6B62\uFF0C\u7591\u4F3C\u505C\u66F4\u3002"
16315
+ };
16316
+ }
15586
16317
  return diagnosis;
15587
16318
  }
15588
16319
 
15589
16320
  // src/lib/bridge/session-health-runtime.ts
15590
16321
  function createSessionHealthRuntime(deps) {
15591
16322
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
15592
- const processProbeCache = /* @__PURE__ */ new Map();
16323
+ function isTerminalHealthStatus(status) {
16324
+ return status === "completed" || status === "failed" || status === "aborted";
16325
+ }
16326
+ function shouldIgnoreNonStartProgress(session) {
16327
+ return isTerminalHealthStatus(session.health_status);
16328
+ }
15593
16329
  function summarizePlanUpdate(tasks) {
15594
16330
  if (!Array.isArray(tasks) || tasks.length === 0) {
15595
16331
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
@@ -15657,6 +16393,8 @@ function createSessionHealthRuntime(deps) {
15657
16393
  }
15658
16394
  function recordInteractiveProgress(sessionId, type, detail) {
15659
16395
  const nowIso4 = deps.nowIso();
16396
+ const session = deps.getStore().getSession(sessionId);
16397
+ if (!session || shouldIgnoreNonStartProgress(session)) return;
15660
16398
  maybePersistProgress(sessionId, {
15661
16399
  health_status: type === "permission_wait" ? "waiting_tool" : "running_active",
15662
16400
  health_reason: buildProgressReason(type, detail),
@@ -15669,6 +16407,7 @@ function createSessionHealthRuntime(deps) {
15669
16407
  const store = deps.getStore();
15670
16408
  const session = store.getSession(sessionId);
15671
16409
  if (!session) return;
16410
+ if (shouldIgnoreNonStartProgress(session)) return;
15672
16411
  const activeTools = new Map(
15673
16412
  parseActiveToolsJson(session.active_tools_json).map((tool) => [tool.id, tool])
15674
16413
  );
@@ -15725,11 +16464,9 @@ function createSessionHealthRuntime(deps) {
15725
16464
  last_stream_ui_update_at: void 0,
15726
16465
  last_stream_ui_error_at: void 0,
15727
16466
  last_stream_ui_error: void 0,
15728
- stream_ui_consecutive_failures: void 0,
15729
- last_health_check_at: nowIso4
16467
+ stream_ui_consecutive_failures: void 0
15730
16468
  }, { force: true });
15731
16469
  lastProgressPersistAt.set(sessionId, Date.now());
15732
- processProbeCache.delete(sessionId);
15733
16470
  }
15734
16471
  function toIso(value) {
15735
16472
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
@@ -15824,17 +16561,7 @@ function createSessionHealthRuntime(deps) {
15824
16561
  async function loadProcessProbe(session) {
15825
16562
  const threadId = session.sdk_session_id?.trim();
15826
16563
  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;
16564
+ return deps.probeThreadProcess(threadId);
15838
16565
  }
15839
16566
  async function diagnoseSessionHealth(sessionId) {
15840
16567
  const store = deps.getStore();
@@ -15842,21 +16569,10 @@ function createSessionHealthRuntime(deps) {
15842
16569
  if (!session) return null;
15843
16570
  const base = computeBaseDiagnosis(session, Date.now());
15844
16571
  const processProbe = await loadProcessProbe(session);
15845
- const checkedAt = deps.nowIso();
15846
- const diagnosis = applyStreamUiDiagnosis(
16572
+ return applyStreamUiDiagnosis(
15847
16573
  applyProcessProbeDiagnosis(base, processProbe),
15848
16574
  Date.now()
15849
16575
  );
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
16576
  }
15861
16577
  async function diagnoseAllActiveSessions() {
15862
16578
  const store = deps.getStore();
@@ -15877,6 +16593,100 @@ function createSessionHealthRuntime(deps) {
15877
16593
  };
15878
16594
  }
15879
16595
 
16596
+ // src/lib/bridge/turns/desktop-terminal-router.ts
16597
+ function isTerminalRecord(record) {
16598
+ return record.type === "task_complete" || record.type === "task_aborted";
16599
+ }
16600
+ function toTerminalRecord(sessionId, desktopThreadId, record) {
16601
+ return {
16602
+ sessionId,
16603
+ desktopThreadId,
16604
+ turnId: record.turnId,
16605
+ text: record.content,
16606
+ outcome: record.type === "task_aborted" ? "aborted" : "completed",
16607
+ timestamp: record.timestamp
16608
+ };
16609
+ }
16610
+ async function routeDesktopRecords(sessionId, desktopThreadId, records, coordinator) {
16611
+ let terminalRecord = null;
16612
+ for (let index = records.length - 1; index >= 0; index -= 1) {
16613
+ if (!isTerminalRecord(records[index])) continue;
16614
+ terminalRecord = records[index];
16615
+ break;
16616
+ }
16617
+ if (!terminalRecord) {
16618
+ return {
16619
+ claimed: [],
16620
+ unclaimed: records,
16621
+ terminalClaimed: false
16622
+ };
16623
+ }
16624
+ const claim = await coordinator.claimDesktopTerminal(
16625
+ toTerminalRecord(sessionId, desktopThreadId, terminalRecord)
16626
+ );
16627
+ if (!claim.claimed) {
16628
+ return {
16629
+ claimed: [],
16630
+ unclaimed: records,
16631
+ terminalClaimed: false
16632
+ };
16633
+ }
16634
+ const claimedTurnId = terminalRecord.turnId;
16635
+ const claimed = claimedTurnId ? records.filter((record) => record.turnId === claimedTurnId) : [terminalRecord];
16636
+ const claimedSet = new Set(claimed.map((record) => record.signature));
16637
+ return {
16638
+ claimed,
16639
+ unclaimed: records.filter((record) => !claimedSet.has(record.signature)),
16640
+ terminalClaimed: true
16641
+ };
16642
+ }
16643
+
16644
+ // src/lib/bridge/turns/turn-coordinator.ts
16645
+ function createTurnCoordinator(deps = {}) {
16646
+ const activeTurnsBySession = /* @__PURE__ */ new Map();
16647
+ function registerInteractiveTurn(turn) {
16648
+ activeTurnsBySession.set(turn.sessionId, turn);
16649
+ }
16650
+ function getActiveTurn(sessionId) {
16651
+ return activeTurnsBySession.get(sessionId);
16652
+ }
16653
+ async function claimDesktopTerminal(terminal) {
16654
+ const turn = activeTurnsBySession.get(terminal.sessionId);
16655
+ if (!turn || turn.kind !== "im_desktop_reuse") {
16656
+ return { claimed: false };
16657
+ }
16658
+ if (turn.desktopThreadId && turn.desktopThreadId !== terminal.desktopThreadId) {
16659
+ return { claimed: false };
16660
+ }
16661
+ const finalized = await deps.finalizeTerminalTurn?.(turn, terminal);
16662
+ return finalized ? { claimed: true, turn } : { claimed: false, turn };
16663
+ }
16664
+ function releaseTurn(turnId) {
16665
+ for (const [sessionId, turn] of activeTurnsBySession) {
16666
+ if (turn.id !== turnId) continue;
16667
+ activeTurnsBySession.delete(sessionId);
16668
+ return;
16669
+ }
16670
+ }
16671
+ function releaseSessionTurn(sessionId, turnId) {
16672
+ const turn = activeTurnsBySession.get(sessionId);
16673
+ if (!turn) return;
16674
+ if (turnId && turn.id !== turnId) return;
16675
+ activeTurnsBySession.delete(sessionId);
16676
+ }
16677
+ function clear() {
16678
+ activeTurnsBySession.clear();
16679
+ }
16680
+ return {
16681
+ registerInteractiveTurn,
16682
+ getActiveTurn,
16683
+ claimDesktopTerminal,
16684
+ releaseTurn,
16685
+ releaseSessionTurn,
16686
+ clear
16687
+ };
16688
+ }
16689
+
15880
16690
  // src/lib/bridge/bridge-manager.ts
15881
16691
  var GLOBAL_KEY = "__bridge_manager__";
15882
16692
  var DANGLING_MIRROR_THREAD_RETRY_LIMIT = 3;
@@ -15887,9 +16697,10 @@ var MIRROR_WATCH_DEBOUNCE_MS = 350;
15887
16697
  var MIRROR_EVENT_BATCH_LIMIT = 8;
15888
16698
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
15889
16699
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
16700
+ var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
15890
16701
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
15891
16702
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
15892
- var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
16703
+ var MIRROR_TURN_BUFFER_TIMEOUT_MS = 10 * 6e4;
15893
16704
  function describeUnknownError(error) {
15894
16705
  if (error instanceof Error) {
15895
16706
  return error.stack || `${error.name}: ${error.message}`;
@@ -15974,6 +16785,23 @@ var INTERACTIVE_RUNTIME = createInteractiveRuntime(getState, {
15974
16785
  getStore: () => getBridgeContext().store,
15975
16786
  nowIso: nowIso3
15976
16787
  });
16788
+ function formatDesktopTerminalDetail(terminal) {
16789
+ if (terminal.outcome === "aborted") {
16790
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
16791
+ }
16792
+ if (terminal.outcome === "failed") {
16793
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5F53\u524D\u4EFB\u52A1\u6267\u884C\u5931\u8D25\u3002";
16794
+ }
16795
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002";
16796
+ }
16797
+ var TURN_COORDINATOR = createTurnCoordinator({
16798
+ finalizeTerminalTurn: (turn, terminal) => INTERACTIVE_RUNTIME.finalizeTerminalActiveTask(
16799
+ turn.sessionId,
16800
+ terminal.outcome,
16801
+ formatDesktopTerminalDetail(terminal),
16802
+ terminal.text
16803
+ )
16804
+ });
15977
16805
  var SESSION_HEALTH_RUNTIME = createSessionHealthRuntime({
15978
16806
  getStore: () => getBridgeContext().store,
15979
16807
  nowIso: nowIso3,
@@ -16010,9 +16838,6 @@ function settleMirrorSuppression2(sessionId, suppressionId, durationMs = MIRROR_
16010
16838
  durationMs
16011
16839
  );
16012
16840
  }
16013
- function isMirrorSuppressed2(sessionId) {
16014
- return isMirrorSuppressed(getMirrorSuppressionStore(), sessionId);
16015
- }
16016
16841
  function filterSuppressedMirrorRecords2(sessionId, records) {
16017
16842
  return filterSuppressedMirrorRecords(
16018
16843
  getMirrorSuppressionStore(),
@@ -16046,28 +16871,6 @@ function syncMirrorSessionStateSafe(sessionId, context) {
16046
16871
  );
16047
16872
  }
16048
16873
  }
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
16874
  function getMirrorStructuredStreamStatusConfig() {
16072
16875
  const { store } = getBridgeContext();
16073
16876
  const idleStartSeconds = parseInt(store.getSetting("bridge_stream_status_idle_start_seconds") || "", 10);
@@ -16083,216 +16886,43 @@ function getMirrorStructuredStreamStatusConfig() {
16083
16886
  )
16084
16887
  };
16085
16888
  }
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
- }
16889
+ var MIRROR_FEEDBACK = createMirrorFeedbackController({
16890
+ getAdapter: (channelType) => getState().adapters.get(channelType) || null,
16891
+ getThreadTitle: (threadId) => getDesktopThreadTitle(threadId),
16892
+ nowIso: nowIso3,
16893
+ eventBatchLimit: MIRROR_EVENT_BATCH_LIMIT,
16894
+ deliverResponse
16895
+ });
16133
16896
  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
- });
16897
+ MIRROR_FEEDBACK.refreshMirrorStreamingStatus(subscription, nowMs, config2);
16148
16898
  }
16149
16899
  function refreshActiveMirrorStreamingStatuses(nowMs = Date.now()) {
16150
16900
  for (const subscription of getState().mirrorSubscriptions.values()) {
16151
16901
  refreshMirrorStreamingStatus(subscription, nowMs);
16152
16902
  }
16153
16903
  }
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
16904
  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();
16905
+ MIRROR_FEEDBACK.stopMirrorStreaming(subscription, status);
16244
16906
  }
16245
16907
  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 };
16908
+ return MIRROR_FEEDBACK.deliverMirrorTurns(subscription, turns);
16256
16909
  }
16257
- var MIRROR_TURN_HOOKS = {
16258
- onStreamText: updateMirrorStreaming,
16259
- onStatusProgress: updateMirrorStatusProgress,
16260
- onTaskProgress: updateMirrorTaskProgress,
16261
- onToolProgress: updateMirrorToolProgress
16262
- };
16910
+ var MIRROR_TURN_HOOKS = MIRROR_FEEDBACK.hooks;
16263
16911
  function consumeMirrorRecords2(subscription, records) {
16264
16912
  return consumeMirrorRecords(subscription, records, MIRROR_TURN_HOOKS);
16265
16913
  }
16266
16914
  function flushTimedOutMirrorTurn2(subscription, nowMs = Date.now()) {
16915
+ if (subscription.pendingTurn?.streamStarted) {
16916
+ return null;
16917
+ }
16267
16918
  return flushTimedOutMirrorTurn(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs);
16268
16919
  }
16269
16920
  function hasPendingMirrorWork2(subscription) {
16270
16921
  return hasPendingMirrorWork(subscription);
16271
16922
  }
16272
16923
  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
- });
16924
+ const timeoutMs = subscription.pendingTurn?.streamStarted ? Number.POSITIVE_INFINITY : MIRROR_TURN_BUFFER_TIMEOUT_MS;
16925
+ return consumeBufferedMirrorTurns(subscription, timeoutMs, nowMs, MIRROR_TURN_HOOKS);
16296
16926
  }
16297
16927
  var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16298
16928
  watchDebounceMs: MIRROR_WATCH_DEBOUNCE_MS,
@@ -16304,12 +16934,16 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16304
16934
  describeUnknownError,
16305
16935
  getDesktopSessionByThreadIdSafe,
16306
16936
  syncMirrorSessionStateSafe,
16307
- isMirrorSuppressed: isMirrorSuppressed2,
16308
16937
  filterSuppressedMirrorRecords: filterSuppressedMirrorRecords2,
16309
16938
  observeSessionHealthRecords: (sessionId, threadId, records) => {
16310
16939
  SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
16311
- void finalizeInteractiveTaskFromMirrorRecords(sessionId, records);
16312
16940
  },
16941
+ routeDesktopRecords: (sessionId, threadId, records) => routeDesktopRecords(
16942
+ sessionId,
16943
+ threadId,
16944
+ records,
16945
+ TURN_COORDINATOR
16946
+ ),
16313
16947
  consumeMirrorRecords: consumeMirrorRecords2,
16314
16948
  flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
16315
16949
  hasPendingMirrorWork: hasPendingMirrorWork2,
@@ -16557,6 +17191,7 @@ async function handleMessage(adapter, msg) {
16557
17191
  try {
16558
17192
  await runInteractiveMessage(adapter, msg, text2, hasAttachments ? msg.attachments : void 0, {
16559
17193
  registerInteractiveTask: (task) => INTERACTIVE_RUNTIME.registerInteractiveTask(task),
17194
+ registerBridgeTurn: (turn) => TURN_COORDINATOR.registerInteractiveTurn(turn),
16560
17195
  resetMirrorSessionForInteractiveRun,
16561
17196
  isCurrentInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.isCurrentInteractiveTask(sessionId, taskId),
16562
17197
  touchInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.touchInteractiveTask(sessionId, taskId),
@@ -16573,8 +17208,10 @@ async function handleMessage(adapter, msg) {
16573
17208
  abortMirrorSuppression: abortMirrorSuppression2,
16574
17209
  settleMirrorSuppression: settleMirrorSuppression2,
16575
17210
  releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
17211
+ releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
16576
17212
  deliverResponse,
16577
- persistSdkSessionUpdate
17213
+ persistSdkSessionUpdate,
17214
+ desktopTerminalFinalizationTimeoutMs: DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS
16578
17215
  });
16579
17216
  } finally {
16580
17217
  ack();
@@ -16583,6 +17220,8 @@ async function handleMessage(adapter, msg) {
16583
17220
  async function handleCommand(adapter, msg, text2) {
16584
17221
  await handleBridgeCommand(adapter, msg, text2, {
16585
17222
  getActiveTask: (sessionId) => INTERACTIVE_RUNTIME.getActiveTask(sessionId),
17223
+ forceStopSession: (sessionId, detail) => INTERACTIVE_RUNTIME.forceStopSession(sessionId, detail),
17224
+ recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
16586
17225
  diagnoseSessionHealth: (sessionId) => SESSION_HEALTH_RUNTIME.diagnoseSessionHealth(sessionId),
16587
17226
  diagnoseAllActiveSessions: () => SESSION_HEALTH_RUNTIME.diagnoseAllActiveSessions()
16588
17227
  });
@@ -16886,7 +17525,7 @@ var JsonFileStore = class {
16886
17525
  findSessionBySdkSessionId(sdkSessionId) {
16887
17526
  this.reloadSessions();
16888
17527
  for (const session of this.sessions.values()) {
16889
- if (session.sdk_session_id === sdkSessionId) {
17528
+ if (session.sdk_session_id === sdkSessionId || session.codex_thread_id === sdkSessionId || session.desktop_thread_id === sdkSessionId) {
16890
17529
  return session;
16891
17530
  }
16892
17531
  }
@@ -17020,6 +17659,15 @@ var JsonFileStore = class {
17020
17659
  const s = this.sessions.get(sessionId);
17021
17660
  if (s) {
17022
17661
  s.sdk_session_id = sdkSessionId;
17662
+ if (sdkSessionId) {
17663
+ s.codex_thread_id = sdkSessionId;
17664
+ s.thread_origin = s.thread_origin || "bridge";
17665
+ } else {
17666
+ delete s.codex_thread_id;
17667
+ if (s.thread_origin !== "desktop") {
17668
+ delete s.thread_origin;
17669
+ }
17670
+ }
17023
17671
  s.updated_at = now();
17024
17672
  this.persistSessions();
17025
17673
  }