codex-to-im 1.0.45 → 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
  }
@@ -13360,10 +13458,27 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
13360
13458
  }
13361
13459
  case "/stop": {
13362
13460
  const binding = resolve(msg.address);
13461
+ const session = store.getSession(binding.codepilotSessionId);
13363
13462
  const task = deps.getActiveTask(binding.codepilotSessionId);
13364
- if (task) {
13365
- task.abortController.abort();
13366
- 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`;
13367
13482
  } else {
13368
13483
  response = "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
13369
13484
  }
@@ -14537,6 +14652,22 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14537
14652
  let finalOutcome = "failed";
14538
14653
  let finalOutcomeDetail;
14539
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
+ };
14540
14671
  try {
14541
14672
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
14542
14673
  const processPromise = processMessageImpl(
@@ -14691,7 +14822,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14691
14822
  if (shouldRecordHealthEnd) {
14692
14823
  if (taskAbort.signal.aborted && !externalTerminalRequest) {
14693
14824
  finalOutcome = "aborted";
14694
- finalOutcomeDetail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14825
+ finalOutcomeDetail = finalOutcomeDetail || "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14695
14826
  }
14696
14827
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14697
14828
  }
@@ -14712,6 +14843,13 @@ function isTerminalSessionHealthStatus(status) {
14712
14843
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14713
14844
  }
14714
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
+ }
14715
14853
  function getQueuedCount(sessionId) {
14716
14854
  return getState2().queuedCounts.get(sessionId) || 0;
14717
14855
  }
@@ -14758,6 +14896,23 @@ function createInteractiveRuntime(getState2, deps) {
14758
14896
  if (!task?.finalizeFromExternalTerminal) return false;
14759
14897
  return task.finalizeFromExternalTerminal(outcome, detail, finalText);
14760
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
+ }
14761
14916
  async function reconcileTerminalSessionRuntimeState() {
14762
14917
  const store = deps.getStore();
14763
14918
  for (const session of store.listSessions()) {
@@ -14809,6 +14964,7 @@ function createInteractiveRuntime(getState2, deps) {
14809
14964
  const state = getState2();
14810
14965
  const prev = state.sessionLocks.get(sessionId) || Promise.resolve();
14811
14966
  const queued = state.sessionLocks.has(sessionId);
14967
+ const lockVersion = getSessionLockVersion(sessionId);
14812
14968
  if (queued) {
14813
14969
  incrementQueuedCount(sessionId);
14814
14970
  }
@@ -14816,6 +14972,7 @@ function createInteractiveRuntime(getState2, deps) {
14816
14972
  if (queued) {
14817
14973
  decrementQueuedCount(sessionId);
14818
14974
  }
14975
+ if (getSessionLockVersion(sessionId) !== lockVersion) return;
14819
14976
  await fn();
14820
14977
  };
14821
14978
  const current = prev.then(wrapped, wrapped);
@@ -14837,6 +14994,7 @@ function createInteractiveRuntime(getState2, deps) {
14837
14994
  releaseInteractiveTask,
14838
14995
  syncSessionRuntimeState,
14839
14996
  finalizeTerminalActiveTask,
14997
+ forceStopSession,
14840
14998
  reconcileTerminalSessionRuntimeState,
14841
14999
  resetPersistedInteractiveRuntimeState,
14842
15000
  processWithSessionLock
@@ -15967,6 +16125,27 @@ function computeBaseDiagnosis(session, nowMs) {
15967
16125
  const sdkSessionId = trimOrNull(session.sdk_session_id);
15968
16126
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
15969
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
+ }
15970
16149
  if (!lastProgressMs) {
15971
16150
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15972
16151
  return {
@@ -15999,10 +16178,10 @@ function computeBaseDiagnosis(session, nowMs) {
15999
16178
  );
16000
16179
  } else if (idleMs <= HEALTH_RECENT_PROGRESS_MS) {
16001
16180
  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";
16181
+ healthReason = activeToolName ? `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${activeToolName}\u3002` : "\u8FD1\u671F\u4ECD\u6709\u65B0\u8FDB\u5C55\u3002";
16003
16182
  } else if (idleMs <= HEALTH_SLOW_OBSERVED_MS) {
16004
16183
  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";
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";
16006
16185
  } else {
16007
16186
  healthStatus = "suspected_stall";
16008
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";
@@ -16069,9 +16248,6 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16069
16248
  return diagnosis;
16070
16249
  }
16071
16250
  const lastProgressMs = parseIsoMs(diagnosis.lastProgressAt || void 0);
16072
- if (!lastProgressMs || nowMs - lastProgressMs > HEALTH_RECENT_PROGRESS_MS) {
16073
- return diagnosis;
16074
- }
16075
16251
  const lastStreamUiUpdateMs = parseIsoMs(diagnosis.lastStreamUiUpdateAt || void 0);
16076
16252
  const lastStreamUiAttemptMs = parseIsoMs(diagnosis.lastStreamUiAttemptAt || void 0);
16077
16253
  const streamUiFlushStartedMs = parseIsoMs(diagnosis.streamUiFlushStartedAt || void 0);
@@ -16090,7 +16266,21 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16090
16266
  healthReason: details.join(" ")
16091
16267
  };
16092
16268
  }
16093
- 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) {
16094
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"];
16095
16285
  if (lastStreamUiErrorText) {
16096
16286
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -16101,7 +16291,7 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16101
16291
  healthReason: details.join(" ")
16102
16292
  };
16103
16293
  }
16104
- if (!lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
16294
+ if (lastProgressMs && !lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
16105
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"];
16106
16296
  if (lastStreamUiErrorText) {
16107
16297
  details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
@@ -16112,12 +16302,30 @@ function applyStreamUiDiagnosis(diagnosis, nowMs) {
16112
16302
  healthReason: details.join(" ")
16113
16303
  };
16114
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
+ }
16115
16317
  return diagnosis;
16116
16318
  }
16117
16319
 
16118
16320
  // src/lib/bridge/session-health-runtime.ts
16119
16321
  function createSessionHealthRuntime(deps) {
16120
16322
  const lastProgressPersistAt = /* @__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
+ }
16121
16329
  function summarizePlanUpdate(tasks) {
16122
16330
  if (!Array.isArray(tasks) || tasks.length === 0) {
16123
16331
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
@@ -16185,6 +16393,8 @@ function createSessionHealthRuntime(deps) {
16185
16393
  }
16186
16394
  function recordInteractiveProgress(sessionId, type, detail) {
16187
16395
  const nowIso4 = deps.nowIso();
16396
+ const session = deps.getStore().getSession(sessionId);
16397
+ if (!session || shouldIgnoreNonStartProgress(session)) return;
16188
16398
  maybePersistProgress(sessionId, {
16189
16399
  health_status: type === "permission_wait" ? "waiting_tool" : "running_active",
16190
16400
  health_reason: buildProgressReason(type, detail),
@@ -16197,6 +16407,7 @@ function createSessionHealthRuntime(deps) {
16197
16407
  const store = deps.getStore();
16198
16408
  const session = store.getSession(sessionId);
16199
16409
  if (!session) return;
16410
+ if (shouldIgnoreNonStartProgress(session)) return;
16200
16411
  const activeTools = new Map(
16201
16412
  parseActiveToolsJson(session.active_tools_json).map((tool) => [tool.id, tool])
16202
16413
  );
@@ -16489,7 +16700,7 @@ var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
16489
16700
  var DESKTOP_TERMINAL_FINALIZATION_TIMEOUT_MS = 3e4;
16490
16701
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
16491
16702
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
16492
- var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
16703
+ var MIRROR_TURN_BUFFER_TIMEOUT_MS = 10 * 6e4;
16493
16704
  function describeUnknownError(error) {
16494
16705
  if (error instanceof Error) {
16495
16706
  return error.stack || `${error.name}: ${error.message}`;
@@ -17009,6 +17220,8 @@ async function handleMessage(adapter, msg) {
17009
17220
  async function handleCommand(adapter, msg, text2) {
17010
17221
  await handleBridgeCommand(adapter, msg, text2, {
17011
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),
17012
17225
  diagnoseSessionHealth: (sessionId) => SESSION_HEALTH_RUNTIME.diagnoseSessionHealth(sessionId),
17013
17226
  diagnoseAllActiveSessions: () => SESSION_HEALTH_RUNTIME.diagnoseAllActiveSessions()
17014
17227
  });
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.46",
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",