codex-to-im 1.0.45 → 1.0.47

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
@@ -571,7 +571,8 @@ var init_codex_provider = __esm({
571
571
  usage: usage ? {
572
572
  input_tokens: usage.input_tokens ?? 0,
573
573
  output_tokens: usage.output_tokens ?? 0,
574
- cache_read_input_tokens: usage.cached_input_tokens ?? 0
574
+ cache_read_input_tokens: usage.cached_input_tokens ?? 0,
575
+ reasoning_output_tokens: usage.reasoning_output_tokens ?? 0
575
576
  } : void 0,
576
577
  ...threadId ? { session_id: threadId } : {}
577
578
  }));
@@ -1223,14 +1224,19 @@ function buildPostContent(text2) {
1223
1224
  function htmlToFeishuMarkdown(html) {
1224
1225
  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
1226
  }
1226
- function buildToolProgressMarkdown(tools) {
1227
+ function normalizeToolStatusForRender(status, options) {
1228
+ if (status !== "running" || !options.terminalStatus) return status;
1229
+ return options.terminalStatus === "completed" ? "complete" : "error";
1230
+ }
1231
+ function buildToolProgressMarkdown(tools, options = {}) {
1227
1232
  if (tools.length === 0) return "";
1228
1233
  const grouped = /* @__PURE__ */ new Map();
1229
1234
  for (const tool of tools) {
1230
1235
  const key = tool.name || "tool";
1231
1236
  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;
1237
+ const status = normalizeToolStatusForRender(tool.status, options);
1238
+ if (status === "running") bucket.running += 1;
1239
+ else if (status === "error") bucket.error += 1;
1234
1240
  else bucket.complete += 1;
1235
1241
  grouped.set(key, bucket);
1236
1242
  }
@@ -1249,11 +1255,25 @@ function buildToolProgressMarkdown(tools) {
1249
1255
  });
1250
1256
  return lines.join("\n");
1251
1257
  }
1252
- function buildTaskProgressMarkdown(tasks) {
1258
+ function getTaskProgressPresentation(task, options) {
1259
+ if (!options.terminalStatus) {
1260
+ 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" };
1261
+ }
1262
+ if (task.status === "completed") {
1263
+ return { icon: "\u2705", label: "\u5DF2\u5B8C\u6210" };
1264
+ }
1265
+ if (options.terminalStatus === "completed") {
1266
+ return { icon: "\u2705", label: "\u5DF2\u7ED3\u675F" };
1267
+ }
1268
+ if (options.terminalStatus === "interrupted") {
1269
+ return task.status === "pending" ? { icon: "\u26A0\uFE0F", label: "\u672A\u6267\u884C" } : { icon: "\u26A0\uFE0F", label: "\u5DF2\u505C\u6B62" };
1270
+ }
1271
+ return task.status === "pending" ? { icon: "\u274C", label: "\u672A\u6267\u884C" } : { icon: "\u274C", label: "\u5DF2\u4E2D\u65AD" };
1272
+ }
1273
+ function buildTaskProgressMarkdown(tasks, options = {}) {
1253
1274
  if (tasks.length === 0) return "";
1254
1275
  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";
1276
+ const { icon, label } = getTaskProgressPresentation(task, options);
1257
1277
  return `${icon} ${task.text}\uFF08${label}\uFF09`;
1258
1278
  }).join("\n");
1259
1279
  }
@@ -1274,11 +1294,12 @@ function buildStreamingToolsContent(tools) {
1274
1294
  function buildStreamingTaskContent(tasks) {
1275
1295
  return buildTaskProgressMarkdown(tasks);
1276
1296
  }
1277
- function buildFinalCardJson(text2, tasks, tools, footer) {
1297
+ function buildFinalCardJson(text2, tasks, tools, footer, terminalStatus) {
1278
1298
  const elements = [];
1279
1299
  const content = preprocessFeishuMarkdown(text2);
1280
- const taskMd = buildTaskProgressMarkdown(tasks);
1281
- const toolMd = buildToolProgressMarkdown(tools);
1300
+ const renderOptions = { terminalStatus };
1301
+ const taskMd = buildTaskProgressMarkdown(tasks, renderOptions);
1302
+ const toolMd = buildToolProgressMarkdown(tools, renderOptions);
1282
1303
  if (content) {
1283
1304
  elements.push({
1284
1305
  tag: "markdown",
@@ -1384,9 +1405,53 @@ var MAX_FILE_SIZE = 20 * 1024 * 1024;
1384
1405
  var TYPING_EMOJI = "Typing";
1385
1406
  var CARD_THROTTLE_MS = 1e3;
1386
1407
  var CARD_REQUEST_TIMEOUT_MS = 15e3;
1408
+ var CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS = 1e3;
1409
+ var CARD_FULL_REFRESH_INTERVAL_MS = 5 * 6e4;
1387
1410
  var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
1388
1411
  var EMPTY_STREAMING_TASKS = "";
1389
1412
  var EMPTY_STREAMING_TOOLS = "";
1413
+ function buildStreamingCardBody(content, tasksText, toolsText, statusText) {
1414
+ return {
1415
+ schema: "2.0",
1416
+ config: {
1417
+ streaming_mode: true,
1418
+ wide_screen_mode: true,
1419
+ summary: { content: "\u601D\u8003\u4E2D..." }
1420
+ },
1421
+ body: {
1422
+ elements: [
1423
+ {
1424
+ tag: "markdown",
1425
+ content,
1426
+ text_align: "left",
1427
+ text_size: "normal",
1428
+ element_id: "streaming_content"
1429
+ },
1430
+ {
1431
+ tag: "markdown",
1432
+ content: tasksText,
1433
+ text_align: "left",
1434
+ text_size: "normal",
1435
+ element_id: "streaming_tasks"
1436
+ },
1437
+ {
1438
+ tag: "markdown",
1439
+ content: toolsText,
1440
+ text_align: "left",
1441
+ text_size: "normal",
1442
+ element_id: "streaming_tools"
1443
+ },
1444
+ {
1445
+ tag: "markdown",
1446
+ content: statusText,
1447
+ text_align: "left",
1448
+ text_size: "notation",
1449
+ element_id: "streaming_status"
1450
+ }
1451
+ ]
1452
+ }
1453
+ };
1454
+ }
1390
1455
  var MIME_BY_TYPE = {
1391
1456
  image: "image/png",
1392
1457
  file: "application/octet-stream",
@@ -1421,6 +1486,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1421
1486
  /** Cached tenant token for upload APIs. */
1422
1487
  tenantTokenCache = null;
1423
1488
  cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
1489
+ cardFinalizeFlushWaitExtraMs = CARD_FINALIZE_FLUSH_WAIT_EXTRA_MS;
1490
+ cardFullRefreshIntervalMs = CARD_FULL_REFRESH_INTERVAL_MS;
1424
1491
  constructor(instance) {
1425
1492
  super();
1426
1493
  this.channelType = instance?.id || "feishu";
@@ -1654,46 +1721,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1654
1721
  return false;
1655
1722
  }
1656
1723
  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
- };
1724
+ const cardBody = buildStreamingCardBody(
1725
+ "\u{1F4AD} Thinking...",
1726
+ EMPTY_STREAMING_TASKS,
1727
+ EMPTY_STREAMING_TOOLS,
1728
+ INITIAL_STREAMING_STATUS
1729
+ );
1697
1730
  const createResp = await this.withFeishuRequestTimeout(cardKey, "card.create", () => cardkit.card.create({
1698
1731
  data: { type: "card_json", data: JSON.stringify(cardBody) }
1699
1732
  }));
@@ -1724,12 +1757,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1724
1757
  console.warn("[feishu-adapter] Card message send returned no message_id");
1725
1758
  return false;
1726
1759
  }
1760
+ const now2 = Date.now();
1727
1761
  this.activeCards.set(cardKey, {
1728
1762
  chatId,
1729
1763
  cardId,
1730
1764
  messageId,
1731
1765
  sequence: 0,
1732
- startTime: Date.now(),
1766
+ startTime: now2,
1733
1767
  taskItems: [],
1734
1768
  toolCalls: [],
1735
1769
  thinking: true,
@@ -1748,7 +1782,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1748
1782
  lastSuccessfulFlushAt: null,
1749
1783
  lastFlushErrorAt: null,
1750
1784
  lastFlushError: null,
1751
- consecutiveFlushFailures: 0
1785
+ consecutiveFlushFailures: 0,
1786
+ lastFullRefreshAttemptAt: now2,
1787
+ lastSuccessfulFullRefreshAt: null
1752
1788
  });
1753
1789
  console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
1754
1790
  return true;
@@ -1837,6 +1873,17 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1837
1873
  const toolsText = buildStreamingToolsContent(state.toolCalls) || EMPTY_STREAMING_TOOLS;
1838
1874
  const statusText = state.pendingStatusText || INITIAL_STREAMING_STATUS;
1839
1875
  const updates = [];
1876
+ if (this.shouldFullRefreshCard(state, Date.now())) {
1877
+ const refreshed = await this.flushFullCardRefresh(
1878
+ streamKey,
1879
+ state,
1880
+ content,
1881
+ tasksText,
1882
+ toolsText,
1883
+ statusText
1884
+ );
1885
+ if (refreshed) return;
1886
+ }
1840
1887
  if (content !== state.renderedText) {
1841
1888
  updates.push({
1842
1889
  elementId: "streaming_content",
@@ -1903,24 +1950,33 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1903
1950
  state.toolCalls = tools;
1904
1951
  this.scheduleCardFlush(cardKey);
1905
1952
  }
1906
- async awaitCardFlushCompletion(streamKey) {
1953
+ async awaitCardFlushCompletion(streamKey, timeoutMs = this.getCardRequestTimeoutMs() + Math.max(0, this.cardFinalizeFlushWaitExtraMs)) {
1954
+ const deadline = Date.now() + Math.max(0, timeoutMs);
1907
1955
  while (true) {
1908
1956
  const state = this.activeCards.get(streamKey);
1909
- if (!state) return;
1957
+ if (!state) return true;
1910
1958
  const inFlight = state.flushInFlight;
1911
1959
  if (inFlight) {
1960
+ const remainingMs = deadline - Date.now();
1961
+ if (remainingMs <= 0) return false;
1962
+ const timedOut = Symbol("flush-timeout");
1912
1963
  try {
1913
- await inFlight;
1964
+ const result = await Promise.race([
1965
+ inFlight.then(() => null),
1966
+ new Promise((resolve2) => setTimeout(() => resolve2(timedOut), remainingMs))
1967
+ ]);
1968
+ if (result === timedOut) return false;
1914
1969
  } catch {
1915
1970
  }
1916
1971
  continue;
1917
1972
  }
1973
+ if (Date.now() >= deadline) return false;
1918
1974
  if (state.flushQueued) {
1919
1975
  state.flushQueued = false;
1920
1976
  this.enqueueCardFlush(streamKey);
1921
1977
  continue;
1922
1978
  }
1923
- return;
1979
+ return true;
1924
1980
  }
1925
1981
  }
1926
1982
  /**
@@ -1943,7 +1999,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1943
1999
  clearTimeout(state.throttleTimer);
1944
2000
  state.throttleTimer = null;
1945
2001
  }
1946
- await this.awaitCardFlushCompletion(cardKey);
2002
+ const flushed = await this.awaitCardFlushCompletion(cardKey);
2003
+ if (!flushed) {
2004
+ console.warn(`[feishu-adapter] Card finalize proceeding after flush wait timeout: streamKey=${cardKey}`);
2005
+ state.flushInFlight = null;
2006
+ state.flushQueued = false;
2007
+ }
1947
2008
  try {
1948
2009
  state.sequence++;
1949
2010
  await this.withFeishuRequestTimeout(cardKey, "card.settings", () => cardkit.card.settings({
@@ -1972,7 +2033,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1972
2033
 
1973
2034
  ${trimmedResponse}`;
1974
2035
  }
1975
- const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer);
2036
+ const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer, status);
1976
2037
  state.sequence++;
1977
2038
  await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
1978
2039
  path: { card_id: state.cardId },
@@ -2026,6 +2087,44 @@ ${trimmedResponse}`;
2026
2087
  consecutiveFailures: state.consecutiveFlushFailures
2027
2088
  };
2028
2089
  }
2090
+ shouldFullRefreshCard(state, now2) {
2091
+ const interval = Math.max(0, this.cardFullRefreshIntervalMs);
2092
+ if (interval <= 0) return false;
2093
+ if (!Number.isFinite(now2)) return false;
2094
+ return now2 - state.lastFullRefreshAttemptAt >= interval;
2095
+ }
2096
+ async flushFullCardRefresh(streamKey, state, content, tasksText, toolsText, statusText) {
2097
+ state.lastFullRefreshAttemptAt = Date.now();
2098
+ const cardkit = this.restClient?.cardkit?.v1;
2099
+ if (!cardkit?.card?.update) return false;
2100
+ try {
2101
+ state.sequence++;
2102
+ await this.withFeishuRequestTimeout(streamKey, "card.update:streaming_refresh", () => cardkit.card.update({
2103
+ path: { card_id: state.cardId },
2104
+ data: {
2105
+ card: {
2106
+ type: "card_json",
2107
+ data: JSON.stringify(buildStreamingCardBody(content, tasksText, toolsText, statusText))
2108
+ },
2109
+ sequence: state.sequence
2110
+ }
2111
+ }));
2112
+ state.renderedText = content;
2113
+ state.renderedTasksText = tasksText;
2114
+ state.renderedToolsText = toolsText;
2115
+ state.renderedStatusText = statusText;
2116
+ state.lastSuccessfulFullRefreshAt = Date.now();
2117
+ this.markCardFlushSuccess(state);
2118
+ return true;
2119
+ } catch (err) {
2120
+ this.markCardFlushFailure(state, err);
2121
+ console.warn(
2122
+ "[feishu-adapter] card.update streaming refresh failed:",
2123
+ err instanceof Error ? err.message : err
2124
+ );
2125
+ return false;
2126
+ }
2127
+ }
2029
2128
  getCardRequestTimeoutMs() {
2030
2129
  return Math.max(1, this.cardRequestTimeoutMs);
2031
2130
  }
@@ -9973,6 +10072,13 @@ function parseUpdatePlanTasks(argumentsJson) {
9973
10072
  if (!parsed || typeof parsed !== "object") return [];
9974
10073
  return parseTaskProgressItems(parsed.plan ?? parsed.tasks);
9975
10074
  }
10075
+ function extractReasoningSummary(payload) {
10076
+ for (const value of [payload.summary, payload.content, payload.text, payload.message]) {
10077
+ const text2 = extractNormalizedStructuredText(value);
10078
+ if (text2) return text2;
10079
+ }
10080
+ return "";
10081
+ }
9976
10082
  function extractToolOutputText(value) {
9977
10083
  if (typeof value !== "string") return extractNormalizedFreeText(value);
9978
10084
  const trimmed = value.trim();
@@ -9986,6 +10092,39 @@ function extractToolOutputText(value) {
9986
10092
  }
9987
10093
  return extractNormalizedFreeText(value);
9988
10094
  }
10095
+ function summarizePatchChanges(value) {
10096
+ if (!value || typeof value !== "object") return "";
10097
+ return Object.entries(value).map(([filePath, detail]) => {
10098
+ const kind = detail && typeof detail === "object" ? extractNormalizedFreeText(detail.type ?? detail.kind) : "";
10099
+ return kind ? `${kind}: ${filePath}` : filePath;
10100
+ }).filter(Boolean).join("\n");
10101
+ }
10102
+ function summarizeToolSearchOutput(value) {
10103
+ if (!Array.isArray(value)) return "";
10104
+ let count = 0;
10105
+ const names = [];
10106
+ for (const entry of value) {
10107
+ if (!entry || typeof entry !== "object") continue;
10108
+ const namespaceName = extractNormalizedFreeText(entry.name);
10109
+ if (namespaceName) names.push(namespaceName);
10110
+ const tools = entry.tools;
10111
+ if (Array.isArray(tools)) count += tools.length;
10112
+ }
10113
+ const prefix = count > 0 ? `Found ${count} tools` : "";
10114
+ const suffix = names.length > 0 ? names.slice(0, 5).join(", ") : "";
10115
+ return [prefix, suffix].filter(Boolean).join(": ");
10116
+ }
10117
+ function getDynamicToolCallId(payload) {
10118
+ return extractNormalizedFreeText(payload.call_id ?? payload.callId);
10119
+ }
10120
+ function formatDesktopToolName(namespaceValue, nameValue) {
10121
+ const name = extractNormalizedFreeText(nameValue);
10122
+ if (!name) return "";
10123
+ const namespace = extractNormalizedFreeText(namespaceValue);
10124
+ if (!namespace) return name;
10125
+ if (name.startsWith(namespace)) return name;
10126
+ return namespace.endsWith("__") || namespace.endsWith("/") || namespace.endsWith(".") ? `${namespace}${name}` : `${namespace}__${name}`;
10127
+ }
9989
10128
  function createDesktopEventSignature(rawLine) {
9990
10129
  return crypto4.createHash("sha1").update(rawLine).digest("hex");
9991
10130
  }
@@ -10017,7 +10156,28 @@ function isSessionMessageLine(line) {
10017
10156
  function isTurnContextLine(line) {
10018
10157
  return line.type === "turn_context";
10019
10158
  }
10159
+ var IGNORED_EVENT_MSG_TYPES = /* @__PURE__ */ new Set([
10160
+ "context_compacted",
10161
+ "thread_name_updated",
10162
+ "thread_rolled_back",
10163
+ "token_count"
10164
+ ]);
10165
+ var IGNORED_RESPONSE_ITEM_TYPES = /* @__PURE__ */ new Set([
10166
+ "web_search_call"
10167
+ ]);
10168
+ function isIgnoredMirrorLineKind(line) {
10169
+ if (isSessionEventLine(line)) {
10170
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
10171
+ return IGNORED_EVENT_MSG_TYPES.has(payloadType);
10172
+ }
10173
+ if (isSessionMessageLine(line)) {
10174
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
10175
+ return IGNORED_RESPONSE_ITEM_TYPES.has(payloadType);
10176
+ }
10177
+ return false;
10178
+ }
10020
10179
  function describeUnhandledMirrorLineKind(line) {
10180
+ if (isIgnoredMirrorLineKind(line)) return null;
10021
10181
  if (isSessionEventLine(line)) {
10022
10182
  const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
10023
10183
  return `event_msg:${payloadType || "<unknown>"}`;
@@ -10094,6 +10254,20 @@ function pushDesktopSessionEvent(events, parsed, rawLine) {
10094
10254
  });
10095
10255
  return;
10096
10256
  }
10257
+ if (isSessionEventLine(parsed) && parsed.payload?.type === "agent_message") {
10258
+ const text2 = extractNormalizedStructuredText(parsed.payload.message);
10259
+ if (!text2) return;
10260
+ const role = parsed.payload.phase === "commentary" ? "commentary" : "assistant";
10261
+ const lastEvent = events[events.length - 1];
10262
+ if (lastEvent?.role === role && lastEvent.content === text2) return;
10263
+ events.push({
10264
+ signature: createDesktopEventSignature(rawLine),
10265
+ role,
10266
+ content: text2,
10267
+ timestamp: parsed.timestamp || ""
10268
+ });
10269
+ return;
10270
+ }
10097
10271
  if (isSessionEventLine(parsed) && parsed.payload?.type === "task_complete") {
10098
10272
  const text2 = extractNormalizedStructuredText(parsed.payload.last_agent_message);
10099
10273
  if (!text2) return;
@@ -10112,10 +10286,14 @@ function pushDesktopSessionEvent(events, parsed, rawLine) {
10112
10286
  if (isSessionMessageLine(parsed) && parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
10113
10287
  const text2 = extractDesktopMessageText(parsed);
10114
10288
  if (!text2) return;
10289
+ const role = parsed.payload.phase === "commentary" ? "commentary" : "assistant";
10290
+ const content = parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2;
10291
+ const lastEvent = events[events.length - 1];
10292
+ if (lastEvent?.role === role && lastEvent.content === content) return;
10115
10293
  events.push({
10116
10294
  signature: createDesktopEventSignature(rawLine),
10117
- role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
10118
- content: parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2,
10295
+ role,
10296
+ content,
10119
10297
  timestamp: parsed.timestamp || ""
10120
10298
  });
10121
10299
  }
@@ -10152,6 +10330,22 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
10152
10330
  });
10153
10331
  return true;
10154
10332
  }
10333
+ if (isIgnoredMirrorLineKind(parsed)) {
10334
+ return true;
10335
+ }
10336
+ if (parsed.payload?.type === "agent_message") {
10337
+ const text2 = extractNormalizedStructuredText(parsed.payload.message);
10338
+ if (!text2) return true;
10339
+ records.push({
10340
+ signature,
10341
+ type: "message",
10342
+ role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
10343
+ content: text2,
10344
+ timestamp,
10345
+ ...activeTurnId ? { turnId: activeTurnId } : {}
10346
+ });
10347
+ return true;
10348
+ }
10155
10349
  if (parsed.payload?.type === "agent_reasoning") {
10156
10350
  const text2 = extractNormalizedStructuredText(parsed.payload.text);
10157
10351
  if (!text2) return true;
@@ -10194,6 +10388,68 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
10194
10388
  });
10195
10389
  return true;
10196
10390
  }
10391
+ if (parsed.payload?.type === "exec_command_end") {
10392
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10393
+ const exitCode = typeof parsed.payload.exit_code === "number" ? parsed.payload.exit_code : null;
10394
+ const status = extractNormalizedFreeText(parsed.payload.status).toLowerCase();
10395
+ records.push({
10396
+ signature,
10397
+ type: "tool_finished",
10398
+ content: extractToolOutputText(
10399
+ parsed.payload.aggregated_output ?? parsed.payload.formatted_output ?? parsed.payload.stdout ?? parsed.payload.stderr ?? parsed.payload.command
10400
+ ),
10401
+ timestamp,
10402
+ ...parsed.payload.turn_id || activeTurnId ? { turnId: parsed.payload.turn_id || activeTurnId || void 0 } : {},
10403
+ toolId,
10404
+ toolName: "Bash",
10405
+ isError: status === "failed" || exitCode != null && exitCode !== 0
10406
+ });
10407
+ return true;
10408
+ }
10409
+ if (parsed.payload?.type === "patch_apply_end") {
10410
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10411
+ const status = extractNormalizedFreeText(parsed.payload.status).toLowerCase();
10412
+ records.push({
10413
+ signature,
10414
+ type: "tool_finished",
10415
+ content: summarizePatchChanges(parsed.payload.changes) || extractToolOutputText(parsed.payload.stdout ?? parsed.payload.stderr),
10416
+ timestamp,
10417
+ ...parsed.payload.turn_id || activeTurnId ? { turnId: parsed.payload.turn_id || activeTurnId || void 0 } : {},
10418
+ toolId,
10419
+ toolName: "apply_patch",
10420
+ isError: parsed.payload.success === false || status === "failed"
10421
+ });
10422
+ return true;
10423
+ }
10424
+ if (parsed.payload?.type === "dynamic_tool_call_request") {
10425
+ const toolId = getDynamicToolCallId(parsed.payload) || signature;
10426
+ const toolName = extractNormalizedFreeText(parsed.payload.tool) || "tool";
10427
+ records.push({
10428
+ signature,
10429
+ type: "tool_started",
10430
+ content: "",
10431
+ timestamp,
10432
+ ...parsed.payload.turnId || activeTurnId ? { turnId: parsed.payload.turnId || activeTurnId || void 0 } : {},
10433
+ toolId,
10434
+ toolName
10435
+ });
10436
+ return true;
10437
+ }
10438
+ if (parsed.payload?.type === "dynamic_tool_call_response") {
10439
+ const toolId = getDynamicToolCallId(parsed.payload) || signature;
10440
+ const toolName = extractNormalizedFreeText(parsed.payload.tool) || "tool";
10441
+ records.push({
10442
+ signature,
10443
+ type: "tool_finished",
10444
+ content: extractToolOutputText(parsed.payload.content_items ?? parsed.payload.error),
10445
+ timestamp,
10446
+ ...parsed.payload.turn_id || activeTurnId ? { turnId: parsed.payload.turn_id || activeTurnId || void 0 } : {},
10447
+ toolId,
10448
+ toolName,
10449
+ isError: parsed.payload.success === false
10450
+ });
10451
+ return true;
10452
+ }
10197
10453
  if (parsed.payload?.type === "user_message") {
10198
10454
  const text2 = extractNormalizedStructuredText(parsed.payload.message);
10199
10455
  if (!text2) return true;
@@ -10223,6 +10479,21 @@ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
10223
10479
  function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
10224
10480
  const signature = createDesktopEventSignature(rawLine);
10225
10481
  const timestamp = parsed.timestamp || "";
10482
+ if (isIgnoredMirrorLineKind(parsed)) {
10483
+ return true;
10484
+ }
10485
+ if (parsed.payload?.type === "reasoning") {
10486
+ const text2 = extractReasoningSummary(parsed.payload);
10487
+ if (!text2) return true;
10488
+ records.push({
10489
+ signature,
10490
+ type: "reasoning",
10491
+ content: text2,
10492
+ timestamp,
10493
+ ...activeTurnId ? { turnId: activeTurnId } : {}
10494
+ });
10495
+ return true;
10496
+ }
10226
10497
  if (parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
10227
10498
  const text2 = extractDesktopMessageText(parsed);
10228
10499
  if (!text2) return true;
@@ -10236,8 +10507,36 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
10236
10507
  });
10237
10508
  return true;
10238
10509
  }
10510
+ if (parsed.payload?.type === "tool_search_call") {
10511
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10512
+ records.push({
10513
+ signature,
10514
+ type: "tool_started",
10515
+ content: "",
10516
+ timestamp,
10517
+ ...activeTurnId ? { turnId: activeTurnId } : {},
10518
+ toolId,
10519
+ toolName: "tool_search"
10520
+ });
10521
+ return true;
10522
+ }
10523
+ if (parsed.payload?.type === "tool_search_output") {
10524
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10525
+ const status = extractNormalizedFreeText(parsed.payload.status).toLowerCase();
10526
+ records.push({
10527
+ signature,
10528
+ type: "tool_finished",
10529
+ content: summarizeToolSearchOutput(parsed.payload.tools),
10530
+ timestamp,
10531
+ ...activeTurnId ? { turnId: activeTurnId } : {},
10532
+ toolId,
10533
+ toolName: "tool_search",
10534
+ isError: status === "failed"
10535
+ });
10536
+ return true;
10537
+ }
10239
10538
  if (parsed.payload?.type === "function_call") {
10240
- const toolName = extractNormalizedFreeText(parsed.payload.name);
10539
+ const toolName = formatDesktopToolName(parsed.payload.namespace, parsed.payload.name);
10241
10540
  const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10242
10541
  if (!toolName) return true;
10243
10542
  if (toolName === "update_plan") {
@@ -10265,7 +10564,7 @@ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId,
10265
10564
  return true;
10266
10565
  }
10267
10566
  if (parsed.payload?.type === "custom_tool_call") {
10268
- const toolName = extractNormalizedFreeText(parsed.payload.name);
10567
+ const toolName = formatDesktopToolName(parsed.payload.namespace, parsed.payload.name);
10269
10568
  const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
10270
10569
  if (!toolName) return true;
10271
10570
  if (toolName === "update_plan") {
@@ -11703,6 +12002,7 @@ ${notice}` : notice;
11703
12002
  function nowIso2() {
11704
12003
  return (/* @__PURE__ */ new Date()).toISOString();
11705
12004
  }
12005
+ var MIRROR_DUPLICATE_TEXT_WINDOW_MS = 2e3;
11706
12006
  function createMirrorTurnState(sessionId, timestamp, turnId) {
11707
12007
  const safeTimestamp = timestamp || nowIso2();
11708
12008
  return {
@@ -11767,6 +12067,14 @@ function markMirrorContentResponse(turnState, timestamp) {
11767
12067
  turnState.lastContentResponseAt = responseAt;
11768
12068
  turnState.lastResponseAt = responseAt;
11769
12069
  }
12070
+ function isNearDuplicateMirrorText(previousText, nextText, previousTimestamp, nextTimestamp) {
12071
+ if (previousText !== nextText) return false;
12072
+ if (!previousTimestamp || !nextTimestamp) return true;
12073
+ const previousMs = Date.parse(previousTimestamp);
12074
+ const nextMs = Date.parse(nextTimestamp);
12075
+ if (!Number.isFinite(previousMs) || !Number.isFinite(nextMs)) return true;
12076
+ return Math.abs(nextMs - previousMs) <= MIRROR_DUPLICATE_TEXT_WINDOW_MS;
12077
+ }
11770
12078
  function finalizeMirrorTurn(subscription, signature, timestamp, status, preferredText) {
11771
12079
  const pendingTurn = subscription.pendingTurn;
11772
12080
  subscription.pendingTurn = null;
@@ -11836,7 +12144,14 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11836
12144
  if (record.role === "assistant") {
11837
12145
  const text2 = record.content.trim();
11838
12146
  if (text2) {
12147
+ if (isNearDuplicateMirrorText(
12148
+ pendingTurn.lastAssistantText,
12149
+ text2,
12150
+ pendingTurn.lastAssistantTextAt ?? null,
12151
+ record.timestamp
12152
+ )) continue;
11839
12153
  pendingTurn.lastAssistantText = text2;
12154
+ pendingTurn.lastAssistantTextAt = record.timestamp || nowIso2();
11840
12155
  appendMirrorStreamText(pendingTurn, text2);
11841
12156
  markMirrorContentResponse(pendingTurn, record.timestamp);
11842
12157
  hooks.onStreamText?.(subscription, pendingTurn);
@@ -11844,7 +12159,14 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
11844
12159
  } else if (record.role === "commentary") {
11845
12160
  const text2 = record.content.trim();
11846
12161
  if (text2) {
12162
+ if (isNearDuplicateMirrorText(
12163
+ pendingTurn.lastCommentaryText,
12164
+ text2,
12165
+ pendingTurn.lastCommentaryTextAt ?? null,
12166
+ record.timestamp
12167
+ )) continue;
11847
12168
  pendingTurn.lastCommentaryText = text2;
12169
+ pendingTurn.lastCommentaryTextAt = record.timestamp || nowIso2();
11848
12170
  appendMirrorStreamText(pendingTurn, text2);
11849
12171
  markMirrorContentResponse(pendingTurn, record.timestamp);
11850
12172
  hooks.onStreamText?.(subscription, pendingTurn);
@@ -13360,10 +13682,27 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13360
13682
  }
13361
13683
  case "/stop": {
13362
13684
  const binding = resolve(msg.address);
13685
+ const session = store.getSession(binding.codepilotSessionId);
13363
13686
  const task = deps.getActiveTask(binding.codepilotSessionId);
13364
- if (task) {
13365
- task.abortController.abort();
13366
- response = "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u4EFB\u52A1...";
13687
+ const runningHealthStatuses = /* @__PURE__ */ new Set([
13688
+ "running_active",
13689
+ "waiting_tool",
13690
+ "slow_observed",
13691
+ "suspected_stall",
13692
+ "suspected_stream_ui_stall",
13693
+ "suspected_detached"
13694
+ ]);
13695
+ const looksRunning = session?.runtime_status === "running" || session?.runtime_status === "queued" || runningHealthStatuses.has(session?.health_status || "");
13696
+ if (task || looksRunning) {
13697
+ const taskName = getSessionDisplayName(session, binding.workingDirectory);
13698
+ const detail = "\u7528\u6237\u6267\u884C /stop\uFF0C\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
13699
+ if (deps.forceStopSession) {
13700
+ await deps.forceStopSession(binding.codepilotSessionId, detail);
13701
+ } else if (task) {
13702
+ task.abortController.abort();
13703
+ }
13704
+ deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", detail);
13705
+ 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`;
13367
13706
  } else {
13368
13707
  response = "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
13369
13708
  }
@@ -14128,6 +14467,10 @@ function shouldShowStreamLastContentResponseAge(state, nowMs, config2) {
14128
14467
  const ageMs = getStreamLastContentResponseAgeMs(state, nowMs);
14129
14468
  return ageMs != null && ageMs >= Math.max(1e3, config2.heartbeatMs);
14130
14469
  }
14470
+ function getVisibleStreamLastContentResponseAgeMs(state, nowMs, config2) {
14471
+ if (!shouldShowStreamLastContentResponseAge(state, nowMs, config2)) return null;
14472
+ return getStreamLastContentResponseAgeMs(state, nowMs);
14473
+ }
14131
14474
  function buildStreamRuntimeStatus(state, nowMs, options = {}) {
14132
14475
  return formatStreamRuntimeStatus(
14133
14476
  Math.max(0, nowMs - state.startedAtMs),
@@ -14364,11 +14707,20 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14364
14707
  if (!snapshot) return;
14365
14708
  deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, snapshot);
14366
14709
  };
14710
+ const getVisibleLastResponseAgeMs = () => getVisibleStreamLastContentResponseAgeMs(
14711
+ streamState,
14712
+ nowMs(),
14713
+ {
14714
+ idleStartMs: streamStatusIdleDetectionStartMs,
14715
+ heartbeatMs: streamStatusHeartbeatMs
14716
+ }
14717
+ );
14367
14718
  const pushRunningStatus = (lastResponseAgeMs) => {
14368
14719
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
14720
+ const effectiveLastResponseAgeMs = lastResponseAgeMs === void 0 ? getVisibleLastResponseAgeMs() : lastResponseAgeMs;
14369
14721
  pushStreamFeedbackStatus(
14370
14722
  streamFeedbackTarget,
14371
- lastResponseAgeMs == null ? buildStreamRuntimeStatus(streamState, nowMs()) : formatStreamRuntimeStatus(nowMs() - taskStartedAt, lastResponseAgeMs, streamState.statusNote)
14723
+ effectiveLastResponseAgeMs == null ? buildStreamRuntimeStatus(streamState, nowMs()) : formatStreamRuntimeStatus(nowMs() - taskStartedAt, effectiveLastResponseAgeMs, streamState.statusNote)
14372
14724
  );
14373
14725
  syncStructuredStreamUiSnapshot();
14374
14726
  };
@@ -14455,7 +14807,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14455
14807
  if (hasStreamingCards) {
14456
14808
  pushStreamFeedbackTools(streamFeedbackTarget, Array.from(toolCallTracker.values()));
14457
14809
  }
14458
- pushRunningStatus(null);
14810
+ pushRunningStatus();
14459
14811
  syncStructuredStreamUiSnapshot();
14460
14812
  };
14461
14813
  const onTaskEvent = (tasks) => {
@@ -14465,14 +14817,14 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14465
14817
  if (hasStreamingCards) {
14466
14818
  pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
14467
14819
  }
14468
- pushRunningStatus(null);
14820
+ pushRunningStatus();
14469
14821
  syncStructuredStreamUiSnapshot();
14470
14822
  };
14471
14823
  const onStatusNote = (note) => {
14472
14824
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14473
14825
  updateStreamStatusNote(streamState, note, nowMs());
14474
14826
  if (streamState.statusNote) markActivity();
14475
- pushRunningStatus(null);
14827
+ pushRunningStatus();
14476
14828
  syncStructuredStreamUiSnapshot();
14477
14829
  };
14478
14830
  const onPartialText = (fullText) => {
@@ -14483,7 +14835,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14483
14835
  deps.recordInteractiveHealthProgress(binding.codepilotSessionId, "text");
14484
14836
  previewOnPartialText?.(fullText);
14485
14837
  onStreamCardText?.(fullText);
14486
- pushRunningStatus(null);
14838
+ pushRunningStatus();
14487
14839
  syncStructuredStreamUiSnapshot();
14488
14840
  };
14489
14841
  const waitForDesktopTerminalFinalization = async () => {
@@ -14537,6 +14889,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14537
14889
  let finalOutcome = "failed";
14538
14890
  let finalOutcomeDetail;
14539
14891
  let shouldRecordHealthEnd = true;
14892
+ let forceStopStarted = false;
14893
+ taskState.forceStop = async (detail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002") => {
14894
+ if (forceStopStarted) return true;
14895
+ forceStopStarted = true;
14896
+ finalOutcome = "aborted";
14897
+ finalOutcomeDetail = detail;
14898
+ taskAbort.abort();
14899
+ stopStructuredStreamStatusUpdates();
14900
+ endPreviewOnce();
14901
+ try {
14902
+ await finalizeStreamUiOnce("interrupted", detail);
14903
+ } catch {
14904
+ }
14905
+ endMessageUiOnce();
14906
+ return true;
14907
+ };
14540
14908
  try {
14541
14909
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
14542
14910
  const processPromise = processMessageImpl(
@@ -14559,7 +14927,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14559
14927
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
14560
14928
  );
14561
14929
  markActivity();
14562
- pushRunningStatus(null);
14930
+ pushRunningStatus();
14563
14931
  syncStructuredStreamUiSnapshot();
14564
14932
  },
14565
14933
  taskAbort.signal,
@@ -14691,7 +15059,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14691
15059
  if (shouldRecordHealthEnd) {
14692
15060
  if (taskAbort.signal.aborted && !externalTerminalRequest) {
14693
15061
  finalOutcome = "aborted";
14694
- finalOutcomeDetail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
15062
+ finalOutcomeDetail = finalOutcomeDetail || "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14695
15063
  }
14696
15064
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14697
15065
  }
@@ -14712,6 +15080,13 @@ function isTerminalSessionHealthStatus(status) {
14712
15080
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14713
15081
  }
14714
15082
  function createInteractiveRuntime(getState2, deps) {
15083
+ const sessionLockVersions = /* @__PURE__ */ new Map();
15084
+ function getSessionLockVersion(sessionId) {
15085
+ return sessionLockVersions.get(sessionId) || 0;
15086
+ }
15087
+ function invalidateSessionLockQueue(sessionId) {
15088
+ sessionLockVersions.set(sessionId, getSessionLockVersion(sessionId) + 1);
15089
+ }
14715
15090
  function getQueuedCount(sessionId) {
14716
15091
  return getState2().queuedCounts.get(sessionId) || 0;
14717
15092
  }
@@ -14758,6 +15133,23 @@ function createInteractiveRuntime(getState2, deps) {
14758
15133
  if (!task?.finalizeFromExternalTerminal) return false;
14759
15134
  return task.finalizeFromExternalTerminal(outcome, detail, finalText);
14760
15135
  }
15136
+ async function forceStopSession(sessionId, detail) {
15137
+ const state = getState2();
15138
+ const task = state.activeTasks.get(sessionId);
15139
+ let handled = false;
15140
+ if (task?.forceStop) {
15141
+ handled = await task.forceStop(detail);
15142
+ } else if (task) {
15143
+ task.abortController.abort();
15144
+ handled = true;
15145
+ }
15146
+ state.activeTasks.delete(sessionId);
15147
+ state.queuedCounts.delete(sessionId);
15148
+ state.sessionLocks.delete(sessionId);
15149
+ invalidateSessionLockQueue(sessionId);
15150
+ syncSessionRuntimeState(sessionId);
15151
+ return handled;
15152
+ }
14761
15153
  async function reconcileTerminalSessionRuntimeState() {
14762
15154
  const store = deps.getStore();
14763
15155
  for (const session of store.listSessions()) {
@@ -14809,6 +15201,7 @@ function createInteractiveRuntime(getState2, deps) {
14809
15201
  const state = getState2();
14810
15202
  const prev = state.sessionLocks.get(sessionId) || Promise.resolve();
14811
15203
  const queued = state.sessionLocks.has(sessionId);
15204
+ const lockVersion = getSessionLockVersion(sessionId);
14812
15205
  if (queued) {
14813
15206
  incrementQueuedCount(sessionId);
14814
15207
  }
@@ -14816,6 +15209,7 @@ function createInteractiveRuntime(getState2, deps) {
14816
15209
  if (queued) {
14817
15210
  decrementQueuedCount(sessionId);
14818
15211
  }
15212
+ if (getSessionLockVersion(sessionId) !== lockVersion) return;
14819
15213
  await fn();
14820
15214
  };
14821
15215
  const current = prev.then(wrapped, wrapped);
@@ -14837,6 +15231,7 @@ function createInteractiveRuntime(getState2, deps) {
14837
15231
  releaseInteractiveTask,
14838
15232
  syncSessionRuntimeState,
14839
15233
  finalizeTerminalActiveTask,
15234
+ forceStopSession,
14840
15235
  reconcileTerminalSessionRuntimeState,
14841
15236
  resetPersistedInteractiveRuntimeState,
14842
15237
  processWithSessionLock
@@ -15566,9 +15961,16 @@ function createMirrorFeedbackController(deps) {
15566
15961
  if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
15567
15962
  return;
15568
15963
  }
15964
+ const lastContentResponseAtMs = turnState.lastContentResponseAt ? Date.parse(turnState.lastContentResponseAt) : turnState.lastResponseAt ? Date.parse(turnState.lastResponseAt) : null;
15965
+ const streamState = {
15966
+ startedAtMs,
15967
+ lastContentResponseAtMs: Number.isFinite(lastContentResponseAtMs) ? lastContentResponseAtMs : null
15968
+ };
15969
+ const statusConfig = deps.getStructuredStreamStatusConfig?.();
15970
+ const effectiveLastResponseAgeMs = Object.prototype.hasOwnProperty.call(options, "lastResponseAgeMs") ? options.lastResponseAgeMs : statusConfig ? getVisibleStreamLastContentResponseAgeMs(streamState, nowMs, statusConfig) : null;
15569
15971
  const statusText = formatStreamRuntimeStatus(
15570
15972
  Math.max(0, nowMs - startedAtMs),
15571
- options.lastResponseAgeMs,
15973
+ effectiveLastResponseAgeMs,
15572
15974
  turnState.statusNote
15573
15975
  );
15574
15976
  if (turnState.lastStatusText === statusText) return;
@@ -15967,6 +16369,27 @@ function computeBaseDiagnosis(session, nowMs) {
15967
16369
  const sdkSessionId = trimOrNull(session.sdk_session_id);
15968
16370
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
15969
16371
  const previousStatus = session.health_status || "idle";
16372
+ if (!isRunningRuntimeStatus(runtimeStatus) && isRunningHealthStatus(previousStatus)) {
16373
+ return {
16374
+ sessionId: session.id,
16375
+ checkedAt: null,
16376
+ runtimeStatus,
16377
+ healthStatus: "idle",
16378
+ healthReason: "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002",
16379
+ lastProgressAt,
16380
+ lastProgressType,
16381
+ activeToolName,
16382
+ activeToolStartedAt,
16383
+ lastToolFinishedAt,
16384
+ lastStreamUiAttemptAt,
16385
+ lastStreamUiUpdateAt,
16386
+ streamUiFlushStartedAt,
16387
+ lastStreamUiErrorAt,
16388
+ lastStreamUiError,
16389
+ streamUiConsecutiveFailures,
16390
+ sdkSessionId
16391
+ };
16392
+ }
15970
16393
  if (!lastProgressMs) {
15971
16394
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15972
16395
  return {
@@ -15999,10 +16422,10 @@ function computeBaseDiagnosis(session, nowMs) {
15999
16422
  );
16000
16423
  } else if (idleMs <= HEALTH_RECENT_PROGRESS_MS) {
16001
16424
  healthStatus = activeToolName ? "waiting_tool" : "running_active";
16002
- healthReason = activeToolName ? `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${activeToolName}\u3002` : "\u6700\u8FD1 10 \u5206\u949F\u5185\u4ECD\u6709\u65B0\u8FDB\u5C55\u3002";
16425
+ healthReason = activeToolName ? `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${activeToolName}\u3002` : "\u8FD1\u671F\u4ECD\u6709\u65B0\u8FDB\u5C55\u3002";
16003
16426
  } else if (idleMs <= HEALTH_SLOW_OBSERVED_MS) {
16004
16427
  healthStatus = activeToolName ? "waiting_tool" : "slow_observed";
16005
- 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";
16428
+ 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";
16006
16429
  } else {
16007
16430
  healthStatus = "suspected_stall";
16008
16431
  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";
@@ -16069,9 +16492,6 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16069
16492
  return diagnosis;
16070
16493
  }
16071
16494
  const lastProgressMs = parseIsoMs(diagnosis.lastProgressAt || void 0);
16072
- if (!lastProgressMs || nowMs - lastProgressMs > HEALTH_RECENT_PROGRESS_MS) {
16073
- return diagnosis;
16074
- }
16075
16495
  const lastStreamUiUpdateMs = parseIsoMs(diagnosis.lastStreamUiUpdateAt || void 0);
16076
16496
  const lastStreamUiAttemptMs = parseIsoMs(diagnosis.lastStreamUiAttemptAt || void 0);
16077
16497
  const streamUiFlushStartedMs = parseIsoMs(diagnosis.streamUiFlushStartedAt || void 0);
@@ -16090,7 +16510,21 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16090
16510
  healthReason: details.join(" ")
16091
16511
  };
16092
16512
  }
16093
- if (lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
16513
+ if (lastStreamUiAttemptMs && (!lastStreamUiUpdateMs || lastStreamUiAttemptMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS)) {
16514
+ 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"];
16515
+ if (diagnosis.streamUiConsecutiveFailures > 0) {
16516
+ details.push(`\u6700\u8FD1\u8FDE\u7EED\u5931\u8D25 ${diagnosis.streamUiConsecutiveFailures} \u6B21\u3002`);
16517
+ }
16518
+ if (lastStreamUiErrorText) {
16519
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
16520
+ }
16521
+ return {
16522
+ ...diagnosis,
16523
+ healthStatus: "suspected_stream_ui_stall",
16524
+ healthReason: details.join(" ")
16525
+ };
16526
+ }
16527
+ if (lastProgressMs && lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
16094
16528
  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"];
16095
16529
  if (lastStreamUiErrorText) {
16096
16530
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -16101,7 +16535,7 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16101
16535
  healthReason: details.join(" ")
16102
16536
  };
16103
16537
  }
16104
- if (!lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
16538
+ if (lastProgressMs && !lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
16105
16539
  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"];
16106
16540
  if (lastStreamUiErrorText) {
16107
16541
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -16112,12 +16546,30 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16112
16546
  healthReason: details.join(" ")
16113
16547
  };
16114
16548
  }
16549
+ const lastStreamUiActivityMs = Math.max(
16550
+ lastStreamUiAttemptMs || 0,
16551
+ lastStreamUiUpdateMs || 0,
16552
+ streamUiFlushStartedMs || 0
16553
+ );
16554
+ if (lastStreamUiActivityMs > 0 && nowMs - lastStreamUiActivityMs >= HEALTH_STREAM_UI_STALL_MS) {
16555
+ return {
16556
+ ...diagnosis,
16557
+ healthStatus: "suspected_stream_ui_stall",
16558
+ 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"
16559
+ };
16560
+ }
16115
16561
  return diagnosis;
16116
16562
  }
16117
16563
 
16118
16564
  // src/lib/bridge/session-health-runtime.ts
16119
16565
  function createSessionHealthRuntime(deps) {
16120
16566
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
16567
+ function isTerminalHealthStatus(status) {
16568
+ return status === "completed" || status === "failed" || status === "aborted";
16569
+ }
16570
+ function shouldIgnoreNonStartProgress(session) {
16571
+ return isTerminalHealthStatus(session.health_status);
16572
+ }
16121
16573
  function summarizePlanUpdate(tasks) {
16122
16574
  if (!Array.isArray(tasks) || tasks.length === 0) {
16123
16575
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
@@ -16185,6 +16637,8 @@ function createSessionHealthRuntime(deps) {
16185
16637
  }
16186
16638
  function recordInteractiveProgress(sessionId, type, detail) {
16187
16639
  const nowIso4 = deps.nowIso();
16640
+ const session = deps.getStore().getSession(sessionId);
16641
+ if (!session || shouldIgnoreNonStartProgress(session)) return;
16188
16642
  maybePersistProgress(sessionId, {
16189
16643
  health_status: type === "permission_wait" ? "waiting_tool" : "running_active",
16190
16644
  health_reason: buildProgressReason(type, detail),
@@ -16197,6 +16651,7 @@ function createSessionHealthRuntime(deps) {
16197
16651
  const store = deps.getStore();
16198
16652
  const session = store.getSession(sessionId);
16199
16653
  if (!session) return;
16654
+ if (shouldIgnoreNonStartProgress(session)) return;
16200
16655
  const activeTools = new Map(
16201
16656
  parseActiveToolsJson(session.active_tools_json).map((tool) => [tool.id, tool])
16202
16657
  );
@@ -16489,7 +16944,7 @@ var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
16489
16944
  var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
16490
16945
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
16491
16946
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
16492
- var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
16947
+ var MIRROR_TURN_BUFFER_TIMEOUT_MS = 10 * 6e4;
16493
16948
  function describeUnknownError(error) {
16494
16949
  if (error instanceof Error) {
16495
16950
  return error.stack || `${error.name}: ${error.message}`;
@@ -16678,6 +17133,7 @@ function getMirrorStructuredStreamStatusConfig() {
16678
17133
  var MIRROR_FEEDBACK = createMirrorFeedbackController({
16679
17134
  getAdapter: (channelType) => getState().adapters.get(channelType) || null,
16680
17135
  getThreadTitle: (threadId) => getDesktopThreadTitle(threadId),
17136
+ getStructuredStreamStatusConfig: getMirrorStructuredStreamStatusConfig,
16681
17137
  nowIso: nowIso3,
16682
17138
  eventBatchLimit: MIRROR_EVENT_BATCH_LIMIT,
16683
17139
  deliverResponse
@@ -17009,6 +17465,8 @@ async function handleMessage(adapter, msg) {
17009
17465
  async function handleCommand(adapter, msg, text2) {
17010
17466
  await handleBridgeCommand(adapter, msg, text2, {
17011
17467
  getActiveTask: (sessionId) => INTERACTIVE_RUNTIME.getActiveTask(sessionId),
17468
+ forceStopSession: (sessionId, detail) => INTERACTIVE_RUNTIME.forceStopSession(sessionId, detail),
17469
+ recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
17012
17470
  diagnoseSessionHealth: (sessionId) => SESSION_HEALTH_RUNTIME.diagnoseSessionHealth(sessionId),
17013
17471
  diagnoseAllActiveSessions: () => SESSION_HEALTH_RUNTIME.diagnoseAllActiveSessions()
17014
17472
  });
package/docs/dev-plan.md CHANGED
@@ -18,10 +18,15 @@
18
18
 
19
19
  ## 当前进度
20
20
 
21
- 更新时间:2026-04-27
21
+ 更新时间:2026-04-28
22
22
 
23
23
  已完成:
24
24
 
25
+ - 追加恢复任务已完成代码实现:`/stop` 改为线程级强制恢复,不重启 bridge。
26
+ - 追加恢复任务已完成代码实现:强制停止会释放 active task、queued count、session lock,并让旧队列任务失效。
27
+ - 追加恢复任务已完成代码实现:飞书卡片 finalize 不再无限等待卡住的流式 flush。
28
+ - 追加恢复任务已完成代码实现:终态 session 不会再被迟到的正文/工具进展覆盖回 running。
29
+ - 追加恢复任务已完成代码实现:健康诊断对 `runtime_status=idle` 但 `health_status=running_*` 的陈旧状态按 idle 展示,仍保持只读。
25
30
  - 阶段 1 已完成:新增 turn 类型与 turn 分类器。
26
31
  - 阶段 2 已完成主路径:新增 `TurnCoordinator` 与 Desktop terminal router,并接入 mirror runtime。
27
32
  - 阶段 3 已完成主路径:新增 `ResponseAssembler` 与 `DeliveryPipeline`,并接入 interactive final 和 mirror final。
@@ -63,10 +68,36 @@
63
68
 
64
69
  下一步:
65
70
 
66
- - 阶段 5 已完成;当前不建议继续大拆。
67
- - 后续建议进入上线前审查、全量验证、提交发布准备。
71
+ - 运行 `npm run typecheck`、`npm test`、必要时 `npm run build`。
72
+ - 验证通过后进入上线前审查、提交发布准备。
68
73
  - 后续任何清理仍必须保持 health/status 查询只读,不能把诊断命令当作运行态修复入口。
69
74
 
75
+ ## 追加开发任务:线程级停止与卡住恢复
76
+
77
+ 状态:已完成(2026-04-28)
78
+
79
+ 任务清单:
80
+
81
+ - `/stop` 不重启 bridge,只针对当前绑定 session 执行强制停止。
82
+ - `/stop` 对内存中没有 active task、但持久化 health 仍显示 running/stall 的 session 也能恢复。
83
+ - 强制停止需要释放 `activeTasks`、`queuedCounts`、`sessionLocks`,并让停止前排队的旧 work 不再继续执行。
84
+ - 强制停止需要写入 `health_status=aborted`,避免出现 `runtime_status=idle` 但 health 仍是 running/stall。
85
+ - 飞书流式卡片收尾前等待 in-flight flush,但等待必须有上限;超时后继续 finalize,避免 IM 卡片永久不结束。
86
+ - 健康检查和线程状态查询继续保持只读,不承担任何修复动作。
87
+
88
+ 测试清单:
89
+
90
+ - `session-health-runtime.test.ts`:终态 session 不被迟到 progress/tool 覆盖;陈旧 idle+running health 诊断为 idle 且不落盘。
91
+ - `interactive-runtime.test.ts`:`forceStopSession` 清理运行态;旧排队 work 在 force stop 后不再执行。
92
+ - `command-dispatch.test.ts`:`/stop` 能恢复没有 active task 的疑似卡住 session。
93
+ - `feishu-adapter.test.ts`:卡片 finalize 不会被卡住的 flush 永久阻塞。
94
+
95
+ 验证结果:
96
+
97
+ - `npm run typecheck` 通过。
98
+ - `npm test -- --test-name-pattern="session-health-runtime|interactive-runtime|command-dispatch|feishu-adapter|bridge-manager stop handling"` 实际执行全量 359 个测试,全部通过。
99
+ - `npm run build` 通过。
100
+
70
101
  ## 目标
71
102
 
72
103
  - 明确建模三类 turn:`im_sdk`、`im_desktop_reuse`、`desktop_mirror`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@larksuiteoapi/node-sdk": "^1.59.0",
44
- "@openai/codex-sdk": "^0.124.0",
44
+ "@openai/codex-sdk": "^0.130.0",
45
45
  "markdown-it": "^14.1.1",
46
46
  "qrcode": "^1.5.4",
47
47
  "ws": "^8.18.0"