codex-to-im 1.0.39 → 1.0.41

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
@@ -3959,6 +3959,55 @@ function shouldRetryFreshThread(message) {
3959
3959
  const lower = message.toLowerCase();
3960
3960
  return lower.includes("resuming session with different model") || lower.includes("no such session") || lower.includes("resume") && lower.includes("session");
3961
3961
  }
3962
+ function normalizeCodexErrorMessage(message) {
3963
+ const trimmed = (message || "").trim();
3964
+ if (!trimmed) return "Codex \u6267\u884C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
3965
+ const lower = trimmed.toLowerCase();
3966
+ if (lower.includes("timeout waiting for child process to exit") || lower.includes("reconnecting...")) {
3967
+ return "Codex \u4F1A\u8BDD\u6062\u590D\u5931\u8D25\uFF0C\u4E0A\u4E00\u8F6E\u6267\u884C\u8FDB\u7A0B\u672A\u6B63\u5E38\u9000\u51FA\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u5982\u679C\u8FDE\u7EED\u5931\u8D25\uFF0C\u8BF7\u65B0\u5F00\u7EBF\u7A0B\u6216\u5207\u6362\u5230 `/t 0`\u3002";
3968
+ }
3969
+ return trimmed;
3970
+ }
3971
+ function normalizeTaskText(value) {
3972
+ return typeof value === "string" ? value.trim() : "";
3973
+ }
3974
+ function mapTodoListItems(items) {
3975
+ if (!Array.isArray(items)) return [];
3976
+ const normalized = items.map((item) => ({
3977
+ text: normalizeTaskText(item?.text),
3978
+ completed: item?.completed === true
3979
+ })).filter((item) => item.text);
3980
+ let firstIncompleteSeen = false;
3981
+ return normalized.map((item) => {
3982
+ if (item.completed) {
3983
+ return { text: item.text, status: "completed" };
3984
+ }
3985
+ if (!firstIncompleteSeen) {
3986
+ firstIncompleteSeen = true;
3987
+ return { text: item.text, status: "in_progress" };
3988
+ }
3989
+ return { text: item.text, status: "pending" };
3990
+ });
3991
+ }
3992
+ function extractMcpContentText(value) {
3993
+ if (!Array.isArray(value)) return "";
3994
+ return value.map((block2) => {
3995
+ if (!block2 || typeof block2 !== "object") return "";
3996
+ const record = block2;
3997
+ if (typeof record.text === "string") return record.text.trim();
3998
+ if (typeof record.content === "string") return record.content.trim();
3999
+ return "";
4000
+ }).filter(Boolean).join("\n\n").trim();
4001
+ }
4002
+ function stringifyUnknown(value) {
4003
+ if (typeof value === "string") return value;
4004
+ if (value == null) return "";
4005
+ try {
4006
+ return JSON.stringify(value);
4007
+ } catch {
4008
+ return String(value);
4009
+ }
4010
+ }
3962
4011
  var MIME_EXT, CodexProvider;
3963
4012
  var init_codex_provider = __esm({
3964
4013
  "src/codex-provider.ts"() {
@@ -4047,6 +4096,7 @@ var init_codex_provider = __esm({
4047
4096
  input = params.prompt;
4048
4097
  }
4049
4098
  let retryFresh = false;
4099
+ const emittedToolStarts = /* @__PURE__ */ new Set();
4050
4100
  while (true) {
4051
4101
  let thread;
4052
4102
  if (savedThreadId) {
@@ -4059,6 +4109,7 @@ var init_codex_provider = __esm({
4059
4109
  thread = codex.startThread(threadOptions);
4060
4110
  }
4061
4111
  let sawAnyEvent = false;
4112
+ let sawTerminalEvent = false;
4062
4113
  try {
4063
4114
  const { events } = await thread.runStreamed(input, {
4064
4115
  signal: params.abortController?.signal
@@ -4077,9 +4128,19 @@ var init_codex_provider = __esm({
4077
4128
  }));
4078
4129
  break;
4079
4130
  }
4131
+ case "turn.started":
4132
+ break;
4133
+ case "item.started":
4134
+ case "item.updated":
4080
4135
  case "item.completed": {
4081
4136
  const item = event.item;
4082
- self.handleCompletedItem(controller, item);
4137
+ self.handleItemEvent(
4138
+ controller,
4139
+ item,
4140
+ event.type === "item.started" ? "started" : event.type === "item.updated" ? "updated" : "completed",
4141
+ params.sessionId,
4142
+ emittedToolStarts
4143
+ );
4083
4144
  break;
4084
4145
  }
4085
4146
  case "turn.completed": {
@@ -4093,19 +4154,33 @@ var init_codex_provider = __esm({
4093
4154
  } : void 0,
4094
4155
  ...threadId ? { session_id: threadId } : {}
4095
4156
  }));
4157
+ sawTerminalEvent = true;
4096
4158
  break;
4097
4159
  }
4098
4160
  case "turn.failed": {
4099
- const error = event.message;
4100
- controller.enqueue(sseEvent("error", error || "Turn failed"));
4161
+ const error = event.error?.message;
4162
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Turn failed")));
4163
+ sawTerminalEvent = true;
4101
4164
  break;
4102
4165
  }
4103
4166
  case "error": {
4104
4167
  const error = event.message;
4105
- controller.enqueue(sseEvent("error", error || "Thread error"));
4168
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Thread error")));
4169
+ sawTerminalEvent = true;
4170
+ break;
4171
+ }
4172
+ default: {
4173
+ const exhaustiveEvent = event;
4174
+ console.warn(
4175
+ "[codex-provider] Unhandled thread event:",
4176
+ stringifyUnknown(exhaustiveEvent)
4177
+ );
4106
4178
  break;
4107
4179
  }
4108
4180
  }
4181
+ if (sawTerminalEvent) {
4182
+ break;
4183
+ }
4109
4184
  }
4110
4185
  break;
4111
4186
  } catch (err) {
@@ -4124,7 +4199,7 @@ var init_codex_provider = __esm({
4124
4199
  const message = err instanceof Error ? err.message : String(err);
4125
4200
  console.error("[codex-provider] Error:", err instanceof Error ? err.stack || err.message : err);
4126
4201
  try {
4127
- controller.enqueue(sseEvent("error", message));
4202
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
4128
4203
  controller.close();
4129
4204
  } catch {
4130
4205
  }
@@ -4141,12 +4216,22 @@ var init_codex_provider = __esm({
4141
4216
  });
4142
4217
  }
4143
4218
  /**
4144
- * Map a completed Codex item to SSE events.
4219
+ * Map a Codex item event to SSE events.
4145
4220
  */
4146
- handleCompletedItem(controller, item) {
4221
+ handleItemEvent(controller, item, phase, sessionId, emittedToolStarts) {
4147
4222
  const itemType = item.type;
4223
+ const ensureToolUse = (toolId, name, input) => {
4224
+ if (emittedToolStarts.has(toolId)) return;
4225
+ emittedToolStarts.add(toolId);
4226
+ controller.enqueue(sseEvent("tool_use", {
4227
+ id: toolId,
4228
+ name,
4229
+ input
4230
+ }));
4231
+ };
4148
4232
  switch (itemType) {
4149
4233
  case "agent_message": {
4234
+ if (phase !== "completed") break;
4150
4235
  const text2 = item.text || "";
4151
4236
  if (text2) {
4152
4237
  controller.enqueue(sseEvent("text", text2));
@@ -4158,12 +4243,11 @@ var init_codex_provider = __esm({
4158
4243
  const command = item.command || "";
4159
4244
  const output = item.aggregated_output || "";
4160
4245
  const exitCode = item.exit_code;
4246
+ const status = item.status;
4161
4247
  const isError = exitCode != null && exitCode !== 0;
4162
- controller.enqueue(sseEvent("tool_use", {
4163
- id: toolId,
4164
- name: "Bash",
4165
- input: { command }
4166
- }));
4248
+ const terminal = phase === "completed" || status === "completed" || status === "failed";
4249
+ ensureToolUse(toolId, "Bash", { command });
4250
+ if (!terminal) break;
4167
4251
  const resultContent = output || (isError ? `Exit code: ${exitCode}` : "Done");
4168
4252
  controller.enqueue(sseEvent("tool_result", {
4169
4253
  tool_use_id: toolId,
@@ -4173,14 +4257,11 @@ var init_codex_provider = __esm({
4173
4257
  break;
4174
4258
  }
4175
4259
  case "file_change": {
4260
+ if (phase !== "completed") break;
4176
4261
  const toolId = item.id || `tool-${Date.now()}`;
4177
4262
  const changes = item.changes || [];
4178
4263
  const summary = changes.map((c) => `${c.kind}: ${c.path}`).join("\n");
4179
- controller.enqueue(sseEvent("tool_use", {
4180
- id: toolId,
4181
- name: "Edit",
4182
- input: { files: changes }
4183
- }));
4264
+ ensureToolUse(toolId, "Edit", { files: changes });
4184
4265
  controller.enqueue(sseEvent("tool_result", {
4185
4266
  tool_use_id: toolId,
4186
4267
  content: summary || "File changes applied",
@@ -4195,13 +4276,11 @@ var init_codex_provider = __esm({
4195
4276
  const args = item.arguments;
4196
4277
  const result = item.result;
4197
4278
  const error = item.error;
4198
- const resultContent = result?.content ?? result?.structured_content;
4199
- const resultText = typeof resultContent === "string" ? resultContent : resultContent ? JSON.stringify(resultContent) : void 0;
4200
- controller.enqueue(sseEvent("tool_use", {
4201
- id: toolId,
4202
- name: `mcp__${server}__${tool}`,
4203
- input: args
4204
- }));
4279
+ const status = item.status;
4280
+ const terminal = phase === "completed" || status === "completed" || status === "failed";
4281
+ const resultText = extractMcpContentText(result?.content) || stringifyUnknown(result?.structured_content) || stringifyUnknown(result?.content);
4282
+ ensureToolUse(toolId, `mcp__${server}__${tool}`, args);
4283
+ if (!terminal) break;
4205
4284
  controller.enqueue(sseEvent("tool_result", {
4206
4285
  tool_use_id: toolId,
4207
4286
  content: error?.message || resultText || "Done",
@@ -4209,6 +4288,18 @@ var init_codex_provider = __esm({
4209
4288
  }));
4210
4289
  break;
4211
4290
  }
4291
+ case "web_search": {
4292
+ const toolId = item.id || `tool-${Date.now()}`;
4293
+ const query2 = item.query || "";
4294
+ ensureToolUse(toolId, "Web Search", { query: query2 });
4295
+ if (phase !== "completed") break;
4296
+ controller.enqueue(sseEvent("tool_result", {
4297
+ tool_use_id: toolId,
4298
+ content: query2 || "Search completed",
4299
+ is_error: false
4300
+ }));
4301
+ break;
4302
+ }
4212
4303
  case "reasoning": {
4213
4304
  const text2 = item.text || "";
4214
4305
  if (text2) {
@@ -4216,8 +4307,33 @@ var init_codex_provider = __esm({
4216
4307
  }
4217
4308
  break;
4218
4309
  }
4310
+ case "todo_list": {
4311
+ const tasks = mapTodoListItems(item.items);
4312
+ controller.enqueue(sseEvent("task_update", {
4313
+ session_id: sessionId,
4314
+ sdk_session_id: this.threadIds.get(sessionId) || void 0,
4315
+ tasks,
4316
+ todos: tasks
4317
+ }));
4318
+ break;
4319
+ }
4320
+ case "error": {
4321
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(item.message || "Codex error")));
4322
+ break;
4323
+ }
4324
+ default: {
4325
+ const exhaustiveItem = item;
4326
+ console.warn(
4327
+ "[codex-provider] Unhandled thread item:",
4328
+ stringifyUnknown(exhaustiveItem)
4329
+ );
4330
+ break;
4331
+ }
4219
4332
  }
4220
4333
  }
4334
+ handleCompletedItem(controller, item) {
4335
+ this.handleItemEvent(controller, item, "completed", "test-session", /* @__PURE__ */ new Set());
4336
+ }
4221
4337
  };
4222
4338
  }
4223
4339
  });
@@ -5508,6 +5624,14 @@ function buildToolProgressMarkdown(tools) {
5508
5624
  });
5509
5625
  return lines.join("\n");
5510
5626
  }
5627
+ function buildTaskProgressMarkdown(tasks) {
5628
+ if (tasks.length === 0) return "";
5629
+ return tasks.map((task) => {
5630
+ const icon = task.status === "completed" ? "\u2705" : task.status === "in_progress" ? "\u{1F504}" : "\u23F3";
5631
+ const label = task.status === "completed" ? "\u5DF2\u5B8C\u6210" : task.status === "in_progress" ? "\u6267\u884C\u4E2D" : "\u7B49\u5F85\u4E2D";
5632
+ return `${icon} ${task.text}\uFF08${label}\uFF09`;
5633
+ }).join("\n");
5634
+ }
5511
5635
  function formatElapsed(ms) {
5512
5636
  if (ms < 1e3) return `${ms}ms`;
5513
5637
  const sec = ms / 1e3;
@@ -5522,9 +5646,13 @@ function buildStreamingTextContent(text2) {
5522
5646
  function buildStreamingToolsContent(tools) {
5523
5647
  return buildToolProgressMarkdown(tools);
5524
5648
  }
5525
- function buildFinalCardJson(text2, tools, footer) {
5649
+ function buildStreamingTaskContent(tasks) {
5650
+ return buildTaskProgressMarkdown(tasks);
5651
+ }
5652
+ function buildFinalCardJson(text2, tasks, tools, footer) {
5526
5653
  const elements = [];
5527
5654
  const content = preprocessFeishuMarkdown(text2);
5655
+ const taskMd = buildTaskProgressMarkdown(tasks);
5528
5656
  const toolMd = buildToolProgressMarkdown(tools);
5529
5657
  if (content) {
5530
5658
  elements.push({
@@ -5534,6 +5662,17 @@ function buildFinalCardJson(text2, tools, footer) {
5534
5662
  text_size: "normal"
5535
5663
  });
5536
5664
  }
5665
+ if (taskMd) {
5666
+ if (elements.length > 0) {
5667
+ elements.push({ tag: "hr" });
5668
+ }
5669
+ elements.push({
5670
+ tag: "markdown",
5671
+ content: taskMd,
5672
+ text_align: "left",
5673
+ text_size: "normal"
5674
+ });
5675
+ }
5537
5676
  if (toolMd) {
5538
5677
  if (elements.length > 0) {
5539
5678
  elements.push({ tag: "hr" });
@@ -5619,7 +5758,9 @@ var DEDUP_MAX = 1e3;
5619
5758
  var MAX_FILE_SIZE = 20 * 1024 * 1024;
5620
5759
  var TYPING_EMOJI = "Typing";
5621
5760
  var CARD_THROTTLE_MS = 200;
5761
+ var CARD_REQUEST_TIMEOUT_MS = 15e3;
5622
5762
  var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
5763
+ var EMPTY_STREAMING_TASKS = "";
5623
5764
  var EMPTY_STREAMING_TOOLS = "";
5624
5765
  var MIME_BY_TYPE = {
5625
5766
  image: "image/png",
@@ -5650,6 +5791,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5650
5791
  cardCreatePromises = /* @__PURE__ */ new Map();
5651
5792
  /** Cached tenant token for upload APIs. */
5652
5793
  tenantTokenCache = null;
5794
+ cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
5653
5795
  constructor(instance) {
5654
5796
  super();
5655
5797
  this.channelType = instance?.id || "feishu";
@@ -5877,6 +6019,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5877
6019
  text_size: "normal",
5878
6020
  element_id: "streaming_content"
5879
6021
  },
6022
+ {
6023
+ tag: "markdown",
6024
+ content: EMPTY_STREAMING_TASKS,
6025
+ text_align: "left",
6026
+ text_size: "normal",
6027
+ element_id: "streaming_tasks"
6028
+ },
5880
6029
  {
5881
6030
  tag: "markdown",
5882
6031
  content: EMPTY_STREAMING_TOOLS,
@@ -5894,9 +6043,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5894
6043
  ]
5895
6044
  }
5896
6045
  };
5897
- const createResp = await cardkit.card.create({
6046
+ const createResp = await this.withFeishuRequestTimeout(cardKey, "card.create", () => cardkit.card.create({
5898
6047
  data: { type: "card_json", data: JSON.stringify(cardBody) }
5899
- });
6048
+ }));
5900
6049
  const cardId = createResp?.data?.card_id;
5901
6050
  if (!cardId) {
5902
6051
  console.warn("[feishu-adapter] Card create returned no card_id");
@@ -5905,19 +6054,19 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5905
6054
  const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
5906
6055
  let msgResp;
5907
6056
  if (replyToMessageId) {
5908
- msgResp = await this.restClient.im.message.reply({
6057
+ msgResp = await this.withFeishuRequestTimeout(cardKey, "im.message.reply:interactive", () => this.restClient.im.message.reply({
5909
6058
  path: { message_id: replyToMessageId },
5910
6059
  data: { content: cardContent, msg_type: "interactive" }
5911
- });
6060
+ }));
5912
6061
  } else {
5913
- msgResp = await this.restClient.im.message.create({
6062
+ msgResp = await this.withFeishuRequestTimeout(cardKey, "im.message.create:interactive", () => this.restClient.im.message.create({
5914
6063
  params: { receive_id_type: "chat_id" },
5915
6064
  data: {
5916
6065
  receive_id: chatId,
5917
6066
  msg_type: "interactive",
5918
6067
  content: cardContent
5919
6068
  }
5920
- });
6069
+ }));
5921
6070
  }
5922
6071
  const messageId = msgResp?.data?.message_id;
5923
6072
  if (!messageId) {
@@ -5930,17 +6079,25 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5930
6079
  messageId,
5931
6080
  sequence: 0,
5932
6081
  startTime: Date.now(),
6082
+ taskItems: [],
5933
6083
  toolCalls: [],
5934
6084
  thinking: true,
5935
6085
  pendingText: null,
6086
+ pendingTasksText: EMPTY_STREAMING_TASKS,
5936
6087
  pendingStatusText: INITIAL_STREAMING_STATUS,
5937
6088
  renderedText: "\u{1F4AD} Thinking...",
6089
+ renderedTasksText: EMPTY_STREAMING_TASKS,
5938
6090
  renderedToolsText: EMPTY_STREAMING_TOOLS,
5939
6091
  renderedStatusText: INITIAL_STREAMING_STATUS,
5940
6092
  lastUpdateAt: 0,
5941
6093
  throttleTimer: null,
5942
6094
  flushInFlight: null,
5943
- flushQueued: false
6095
+ flushQueued: false,
6096
+ lastFlushStartedAt: null,
6097
+ lastSuccessfulFlushAt: null,
6098
+ lastFlushErrorAt: null,
6099
+ lastFlushError: null,
6100
+ consecutiveFlushFailures: 0
5944
6101
  });
5945
6102
  console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
5946
6103
  return true;
@@ -5969,6 +6126,14 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5969
6126
  state.pendingStatusText = statusText || INITIAL_STREAMING_STATUS;
5970
6127
  this.scheduleCardFlush(cardKey);
5971
6128
  }
6129
+ updateTaskProgress(chatId, tasks, streamKey) {
6130
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6131
+ const state = this.activeCards.get(cardKey);
6132
+ if (!state) return;
6133
+ state.taskItems = tasks;
6134
+ state.pendingTasksText = buildStreamingTaskContent(tasks) || EMPTY_STREAMING_TASKS;
6135
+ this.scheduleCardFlush(cardKey);
6136
+ }
5972
6137
  enqueueCardFlush(streamKey) {
5973
6138
  const state = this.activeCards.get(streamKey);
5974
6139
  if (!state) return;
@@ -5976,6 +6141,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5976
6141
  state.flushQueued = true;
5977
6142
  return;
5978
6143
  }
6144
+ state.lastFlushStartedAt = Date.now();
5979
6145
  state.flushInFlight = this.flushCardUpdate(streamKey).catch((err) => {
5980
6146
  console.warn("[feishu-adapter] cardElement.content failed:", err instanceof Error ? err.message : err);
5981
6147
  }).finally(() => {
@@ -6016,6 +6182,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6016
6182
  const cardkit = this.restClient.cardkit?.v1;
6017
6183
  if (!cardkit?.cardElement?.content) return;
6018
6184
  const content = buildStreamingTextContent(state.pendingText || "");
6185
+ const tasksText = state.pendingTasksText || EMPTY_STREAMING_TASKS;
6019
6186
  const toolsText = buildStreamingToolsContent(state.toolCalls) || EMPTY_STREAMING_TOOLS;
6020
6187
  const statusText = state.pendingStatusText || INITIAL_STREAMING_STATUS;
6021
6188
  const updates = [];
@@ -6028,6 +6195,15 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6028
6195
  }
6029
6196
  });
6030
6197
  }
6198
+ if (tasksText !== state.renderedTasksText) {
6199
+ updates.push({
6200
+ elementId: "streaming_tasks",
6201
+ content: tasksText,
6202
+ onSuccess: () => {
6203
+ state.renderedTasksText = tasksText;
6204
+ }
6205
+ });
6206
+ }
6031
6207
  if (toolsText !== state.renderedToolsText) {
6032
6208
  updates.push({
6033
6209
  elementId: "streaming_tools",
@@ -6051,13 +6227,14 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6051
6227
  for (const update of updates) {
6052
6228
  state.sequence++;
6053
6229
  try {
6054
- await cardkit.cardElement.content({
6230
+ await this.withFeishuRequestTimeout(streamKey, `cardElement.content:${update.elementId}`, () => cardkit.cardElement.content({
6055
6231
  path: { card_id: cardId, element_id: update.elementId },
6056
6232
  data: { content: update.content, sequence: state.sequence }
6057
- });
6233
+ }));
6058
6234
  update.onSuccess();
6059
- state.lastUpdateAt = Date.now();
6235
+ this.markCardFlushSuccess(state);
6060
6236
  } catch (err) {
6237
+ this.markCardFlushFailure(state, err);
6061
6238
  console.warn(
6062
6239
  `[feishu-adapter] cardElement.content failed for ${update.elementId}:`,
6063
6240
  err instanceof Error ? err.message : err
@@ -6118,13 +6295,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6118
6295
  await this.awaitCardFlushCompletion(cardKey);
6119
6296
  try {
6120
6297
  state.sequence++;
6121
- await cardkit.card.settings({
6298
+ await this.withFeishuRequestTimeout(cardKey, "card.settings", () => cardkit.card.settings({
6122
6299
  path: { card_id: state.cardId },
6123
6300
  data: {
6124
6301
  settings: JSON.stringify({ streaming_mode: false }),
6125
6302
  sequence: state.sequence
6126
6303
  }
6127
- });
6304
+ }));
6128
6305
  const statusLabels = {
6129
6306
  completed: "\u2705 Completed",
6130
6307
  interrupted: "\u26A0\uFE0F Interrupted",
@@ -6144,15 +6321,15 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6144
6321
 
6145
6322
  ${trimmedResponse}`;
6146
6323
  }
6147
- const finalCardJson = buildFinalCardJson(finalText, state.toolCalls, footer);
6324
+ const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer);
6148
6325
  state.sequence++;
6149
- await cardkit.card.update({
6326
+ await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
6150
6327
  path: { card_id: state.cardId },
6151
6328
  data: {
6152
6329
  card: { type: "card_json", data: finalCardJson },
6153
6330
  sequence: state.sequence
6154
6331
  }
6155
- });
6332
+ }));
6156
6333
  console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${status}, elapsed=${formatElapsed(elapsedMs)}`);
6157
6334
  return true;
6158
6335
  } catch (err) {
@@ -6184,6 +6361,76 @@ ${trimmedResponse}`;
6184
6361
  hasActiveStreamingUi(chatId, streamKey) {
6185
6362
  return this.hasActiveCard(chatId, streamKey);
6186
6363
  }
6364
+ getStructuredStreamingUiSnapshot(chatId, streamKey) {
6365
+ const state = this.activeCards.get(this.resolveStreamKey(chatId, streamKey));
6366
+ if (!state) return null;
6367
+ return {
6368
+ active: true,
6369
+ lastAttemptAt: state.lastFlushStartedAt,
6370
+ lastUpdateAt: state.lastSuccessfulFlushAt ?? (state.lastUpdateAt > 0 ? state.lastUpdateAt : null),
6371
+ lastErrorAt: state.lastFlushErrorAt,
6372
+ lastError: state.lastFlushError,
6373
+ flushInFlight: Boolean(state.flushInFlight),
6374
+ flushInFlightSince: state.flushInFlight ? state.lastFlushStartedAt : null,
6375
+ consecutiveFailures: state.consecutiveFlushFailures
6376
+ };
6377
+ }
6378
+ getCardRequestTimeoutMs() {
6379
+ return Math.max(1, this.cardRequestTimeoutMs);
6380
+ }
6381
+ logRequestOperation(phase, scope, target, startedAt, detail) {
6382
+ const durationMs = Math.max(0, Date.now() - startedAt);
6383
+ const suffix = detail ? `, detail=${detail}` : "";
6384
+ const line = `[feishu-adapter] Request ${phase}: scope=${scope}, target=${target}, duration=${durationMs}ms${suffix}`;
6385
+ if (phase === "start" || phase === "success") {
6386
+ console.log(line);
6387
+ return;
6388
+ }
6389
+ console.warn(line);
6390
+ }
6391
+ async withFeishuRequestTimeout(scope, target, operation) {
6392
+ const startedAt = Date.now();
6393
+ const timeoutMs = this.getCardRequestTimeoutMs();
6394
+ this.logRequestOperation("start", scope, target, startedAt);
6395
+ let timeoutHandle = null;
6396
+ const timeoutPromise = new Promise((_, reject) => {
6397
+ timeoutHandle = setTimeout(() => {
6398
+ reject(new Error(`timeout after ${timeoutMs}ms`));
6399
+ }, timeoutMs);
6400
+ });
6401
+ const operationPromise = operation();
6402
+ operationPromise.catch(() => {
6403
+ });
6404
+ try {
6405
+ const result = await Promise.race([operationPromise, timeoutPromise]);
6406
+ this.logRequestOperation("success", scope, target, startedAt);
6407
+ return result;
6408
+ } catch (error) {
6409
+ const detail = error instanceof Error ? error.message : String(error);
6410
+ this.logRequestOperation(
6411
+ detail.startsWith("timeout after ") ? "timeout" : "error",
6412
+ scope,
6413
+ target,
6414
+ startedAt,
6415
+ detail
6416
+ );
6417
+ throw error;
6418
+ } finally {
6419
+ if (timeoutHandle) clearTimeout(timeoutHandle);
6420
+ }
6421
+ }
6422
+ markCardFlushFailure(state, error) {
6423
+ state.lastFlushErrorAt = Date.now();
6424
+ state.lastFlushError = error instanceof Error ? error.message : String(error);
6425
+ state.consecutiveFlushFailures += 1;
6426
+ }
6427
+ markCardFlushSuccess(state) {
6428
+ const now2 = Date.now();
6429
+ state.lastUpdateAt = now2;
6430
+ state.lastSuccessfulFlushAt = now2;
6431
+ state.lastFlushError = null;
6432
+ state.consecutiveFlushFailures = 0;
6433
+ }
6187
6434
  // ── Streaming adapter interface ────────────────────────────────
6188
6435
  /**
6189
6436
  * Called by bridge-manager on each text SSE event.
@@ -6211,8 +6458,30 @@ ${trimmedResponse}`;
6211
6458
  }
6212
6459
  onToolEvent(chatId, tools, streamKey) {
6213
6460
  if (!this.isStreamingEnabled()) return;
6461
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6462
+ if (!this.activeCards.has(cardKey)) {
6463
+ const messageId = this.lastIncomingMessageId.get(chatId);
6464
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
6465
+ if (ok) this.updateToolProgress(chatId, tools, cardKey);
6466
+ }).catch(() => {
6467
+ });
6468
+ return;
6469
+ }
6214
6470
  this.updateToolProgress(chatId, tools, streamKey);
6215
6471
  }
6472
+ onTaskEvent(chatId, tasks, streamKey) {
6473
+ if (!this.isStreamingEnabled()) return;
6474
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6475
+ if (!this.activeCards.has(cardKey)) {
6476
+ const messageId = this.lastIncomingMessageId.get(chatId);
6477
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
6478
+ if (ok) this.updateTaskProgress(chatId, tasks, cardKey);
6479
+ }).catch(() => {
6480
+ });
6481
+ return;
6482
+ }
6483
+ this.updateTaskProgress(chatId, tasks, streamKey);
6484
+ }
6216
6485
  onStreamStatus(chatId, statusText, streamKey) {
6217
6486
  if (!this.isStreamingEnabled()) return;
6218
6487
  const cardKey = this.resolveStreamKey(chatId, streamKey);
@@ -6362,17 +6631,17 @@ ${trimmedResponse}`;
6362
6631
  }
6363
6632
  async sendStructuredMessage(chatId, msgType, content, replyToMessageId) {
6364
6633
  try {
6365
- const res = replyToMessageId ? await this.restClient.im.message.reply({
6634
+ const res = replyToMessageId ? await this.withFeishuRequestTimeout(chatId, `im.message.reply:${msgType}`, () => this.restClient.im.message.reply({
6366
6635
  path: { message_id: replyToMessageId },
6367
6636
  data: { msg_type: msgType, content }
6368
- }) : await this.restClient.im.message.create({
6637
+ })) : await this.withFeishuRequestTimeout(chatId, `im.message.create:${msgType}`, () => this.restClient.im.message.create({
6369
6638
  params: { receive_id_type: "chat_id" },
6370
6639
  data: {
6371
6640
  receive_id: chatId,
6372
6641
  msg_type: msgType,
6373
6642
  content
6374
6643
  }
6375
- });
6644
+ }));
6376
6645
  if (res?.data?.message_id) {
6377
6646
  return { ok: true, messageId: res.data.message_id };
6378
6647
  }
@@ -6388,14 +6657,14 @@ ${trimmedResponse}`;
6388
6657
  async sendAsCard(chatId, text2) {
6389
6658
  const cardContent = buildCardContent(text2);
6390
6659
  try {
6391
- const res = await this.restClient.im.message.create({
6660
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:interactive-card", () => this.restClient.im.message.create({
6392
6661
  params: { receive_id_type: "chat_id" },
6393
6662
  data: {
6394
6663
  receive_id: chatId,
6395
6664
  msg_type: "interactive",
6396
6665
  content: cardContent
6397
6666
  }
6398
- });
6667
+ }));
6399
6668
  if (res?.data?.message_id) {
6400
6669
  return { ok: true, messageId: res.data.message_id };
6401
6670
  }
@@ -6412,14 +6681,14 @@ ${trimmedResponse}`;
6412
6681
  async sendAsPost(chatId, text2) {
6413
6682
  const postContent = buildPostContent(text2);
6414
6683
  try {
6415
- const res = await this.restClient.im.message.create({
6684
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:post", () => this.restClient.im.message.create({
6416
6685
  params: { receive_id_type: "chat_id" },
6417
6686
  data: {
6418
6687
  receive_id: chatId,
6419
6688
  msg_type: "post",
6420
6689
  content: postContent
6421
6690
  }
6422
- });
6691
+ }));
6423
6692
  if (res?.data?.message_id) {
6424
6693
  return { ok: true, messageId: res.data.message_id };
6425
6694
  }
@@ -6431,14 +6700,14 @@ ${trimmedResponse}`;
6431
6700
  }
6432
6701
  async sendAsPlainText(chatId, text2) {
6433
6702
  try {
6434
- const res = await this.restClient.im.message.create({
6703
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:text", () => this.restClient.im.message.create({
6435
6704
  params: { receive_id_type: "chat_id" },
6436
6705
  data: {
6437
6706
  receive_id: chatId,
6438
6707
  msg_type: "text",
6439
6708
  content: JSON.stringify({ text: text2 })
6440
6709
  }
6441
- });
6710
+ }));
6442
6711
  if (res?.data?.message_id) {
6443
6712
  return { ok: true, messageId: res.data.message_id };
6444
6713
  }
@@ -6463,14 +6732,14 @@ ${trimmedResponse}`;
6463
6732
  if (permId) {
6464
6733
  const cardJson2 = buildPermissionButtonCard(mdText, permId, chatId);
6465
6734
  try {
6466
- const res = await this.restClient.im.message.create({
6735
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-button-card", () => this.restClient.im.message.create({
6467
6736
  params: { receive_id_type: "chat_id" },
6468
6737
  data: {
6469
6738
  receive_id: chatId,
6470
6739
  msg_type: "interactive",
6471
6740
  content: cardJson2
6472
6741
  }
6473
- });
6742
+ }));
6474
6743
  if (res?.data?.message_id) {
6475
6744
  return { ok: true, messageId: res.data.message_id };
6476
6745
  }
@@ -6514,14 +6783,14 @@ ${trimmedResponse}`;
6514
6783
  }
6515
6784
  });
6516
6785
  try {
6517
- const res = await this.restClient.im.message.create({
6786
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-fallback-card", () => this.restClient.im.message.create({
6518
6787
  params: { receive_id_type: "chat_id" },
6519
6788
  data: {
6520
6789
  receive_id: chatId,
6521
6790
  msg_type: "interactive",
6522
6791
  content: cardJson
6523
6792
  }
6524
- });
6793
+ }));
6525
6794
  if (res?.data?.message_id) {
6526
6795
  return { ok: true, messageId: res.data.message_id };
6527
6796
  }
@@ -6538,14 +6807,14 @@ ${trimmedResponse}`;
6538
6807
  ...permCommands
6539
6808
  ].join("\n");
6540
6809
  try {
6541
- const res = await this.restClient.im.message.create({
6810
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-fallback-text", () => this.restClient.im.message.create({
6542
6811
  params: { receive_id_type: "chat_id" },
6543
6812
  data: {
6544
6813
  receive_id: chatId,
6545
6814
  msg_type: "text",
6546
6815
  content: JSON.stringify({ text: plainText })
6547
6816
  }
6548
- });
6817
+ }));
6549
6818
  if (res?.data?.message_id) {
6550
6819
  return { ok: true, messageId: res.data.message_id };
6551
6820
  }
@@ -15021,6 +15290,54 @@ function extractNormalizedStructuredText(value) {
15021
15290
  collectStructuredTextParts(value, parts);
15022
15291
  return parts.length > 0 ? normalizeStructuredText(parts.join("\n\n")) : "";
15023
15292
  }
15293
+ function parseJsonSafely(value) {
15294
+ if (!value) return null;
15295
+ try {
15296
+ return JSON.parse(value);
15297
+ } catch {
15298
+ return null;
15299
+ }
15300
+ }
15301
+ function normalizeTaskStatus(value) {
15302
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
15303
+ if (normalized === "in_progress" || normalized === "running" || normalized === "active") {
15304
+ return "in_progress";
15305
+ }
15306
+ if (normalized === "completed" || normalized === "complete" || normalized === "done") {
15307
+ return "completed";
15308
+ }
15309
+ return "pending";
15310
+ }
15311
+ function parseTaskProgressItems(value) {
15312
+ if (!Array.isArray(value)) return [];
15313
+ return value.map((item) => {
15314
+ const record = item;
15315
+ const text2 = extractNormalizedStructuredText(record.text ?? record.step);
15316
+ if (!text2) return null;
15317
+ return {
15318
+ text: text2,
15319
+ status: normalizeTaskStatus(record.status)
15320
+ };
15321
+ }).filter((item) => Boolean(item));
15322
+ }
15323
+ function parseUpdatePlanTasks(argumentsJson) {
15324
+ const parsed = parseJsonSafely(argumentsJson);
15325
+ if (!parsed || typeof parsed !== "object") return [];
15326
+ return parseTaskProgressItems(parsed.plan ?? parsed.tasks);
15327
+ }
15328
+ function extractToolOutputText(value) {
15329
+ if (typeof value !== "string") return extractNormalizedFreeText(value);
15330
+ const trimmed = value.trim();
15331
+ if (!trimmed) return "";
15332
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15333
+ const parsed = parseJsonSafely(trimmed);
15334
+ if (parsed && typeof parsed === "object") {
15335
+ const extracted = extractNormalizedFreeText(parsed.output ?? parsed);
15336
+ if (extracted) return extracted;
15337
+ }
15338
+ }
15339
+ return extractNormalizedFreeText(value);
15340
+ }
15024
15341
  function createDesktopEventSignature(rawLine) {
15025
15342
  return crypto7.createHash("sha1").update(rawLine).digest("hex");
15026
15343
  }
@@ -15052,6 +15369,17 @@ function isSessionMessageLine(line) {
15052
15369
  function isTurnContextLine(line) {
15053
15370
  return line.type === "turn_context";
15054
15371
  }
15372
+ function describeUnhandledMirrorLineKind(line) {
15373
+ if (isSessionEventLine(line)) {
15374
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
15375
+ return `event_msg:${payloadType || "<unknown>"}`;
15376
+ }
15377
+ if (isSessionMessageLine(line)) {
15378
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
15379
+ return `response_item:${payloadType || "<unknown>"}`;
15380
+ }
15381
+ return null;
15382
+ }
15055
15383
  function listDesktopSessions(limit) {
15056
15384
  const root = getCodexSessionsRoot();
15057
15385
  if (!fs4.existsSync(root)) return [];
@@ -15144,81 +15472,213 @@ function pushDesktopSessionEvent(events, parsed, rawLine) {
15144
15472
  });
15145
15473
  }
15146
15474
  }
15147
- function pushDesktopMirrorRecord(records, parsed, rawLine, activeTurnId) {
15148
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_started") {
15475
+ function pushDesktopMirrorRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
15476
+ if (isSessionEventLine(parsed)) {
15477
+ return pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId);
15478
+ }
15479
+ if (isSessionMessageLine(parsed)) {
15480
+ return pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds);
15481
+ }
15482
+ return false;
15483
+ }
15484
+ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
15485
+ const signature = createDesktopEventSignature(rawLine);
15486
+ const timestamp = parsed.timestamp || "";
15487
+ if (parsed.payload?.type === "task_started") {
15149
15488
  records.push({
15150
- signature: createDesktopEventSignature(rawLine),
15489
+ signature,
15151
15490
  type: "task_started",
15152
15491
  content: "",
15153
- timestamp: parsed.timestamp || "",
15492
+ timestamp,
15154
15493
  turnId: parsed.payload.turn_id || ""
15155
15494
  });
15156
- return;
15495
+ return true;
15157
15496
  }
15158
- if (isSessionEventLine(parsed) && parsed.payload?.type === "user_message") {
15497
+ if (parsed.payload?.type === "turn_aborted") {
15498
+ records.push({
15499
+ signature,
15500
+ type: "task_aborted",
15501
+ content: extractNormalizedStructuredText(parsed.payload.reason),
15502
+ timestamp,
15503
+ ...activeTurnId ? { turnId: activeTurnId } : {}
15504
+ });
15505
+ return true;
15506
+ }
15507
+ if (parsed.payload?.type === "agent_reasoning") {
15508
+ const text2 = extractNormalizedStructuredText(parsed.payload.text);
15509
+ if (!text2) return true;
15510
+ records.push({
15511
+ signature,
15512
+ type: "reasoning",
15513
+ content: text2,
15514
+ timestamp,
15515
+ ...activeTurnId ? { turnId: activeTurnId } : {}
15516
+ });
15517
+ return true;
15518
+ }
15519
+ if (parsed.payload?.type === "web_search_end") {
15520
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15521
+ records.push({
15522
+ signature,
15523
+ type: "tool_finished",
15524
+ content: extractNormalizedStructuredText(parsed.payload.query),
15525
+ timestamp,
15526
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15527
+ toolId,
15528
+ toolName: "Web Search"
15529
+ });
15530
+ return true;
15531
+ }
15532
+ if (parsed.payload?.type === "mcp_tool_call_end") {
15533
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15534
+ const server = extractNormalizedFreeText(parsed.payload.invocation?.server);
15535
+ const tool = extractNormalizedFreeText(parsed.payload.invocation?.tool);
15536
+ const toolName = server && tool ? `mcp__${server}__${tool}` : "mcp_tool_call";
15537
+ records.push({
15538
+ signature,
15539
+ type: "tool_finished",
15540
+ content: "",
15541
+ timestamp,
15542
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15543
+ toolId,
15544
+ toolName,
15545
+ isError: false
15546
+ });
15547
+ return true;
15548
+ }
15549
+ if (parsed.payload?.type === "user_message") {
15159
15550
  const text2 = extractNormalizedStructuredText(parsed.payload.message);
15160
- if (!text2) return;
15551
+ if (!text2) return true;
15161
15552
  records.push({
15162
- signature: createDesktopEventSignature(rawLine),
15553
+ signature,
15163
15554
  type: "message",
15164
15555
  role: "user",
15165
15556
  content: text2,
15166
- timestamp: parsed.timestamp || "",
15557
+ timestamp,
15167
15558
  ...activeTurnId ? { turnId: activeTurnId } : {}
15168
15559
  });
15169
- return;
15560
+ return true;
15170
15561
  }
15171
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_complete") {
15562
+ if (parsed.payload?.type === "task_complete") {
15172
15563
  records.push({
15173
- signature: createDesktopEventSignature(rawLine),
15564
+ signature,
15174
15565
  type: "task_complete",
15175
15566
  role: "assistant",
15176
15567
  content: extractNormalizedStructuredText(parsed.payload.last_agent_message),
15177
- timestamp: parsed.timestamp || "",
15568
+ timestamp,
15178
15569
  turnId: parsed.payload.turn_id || ""
15179
15570
  });
15180
- return;
15571
+ return true;
15181
15572
  }
15182
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
15573
+ return false;
15574
+ }
15575
+ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
15576
+ const signature = createDesktopEventSignature(rawLine);
15577
+ const timestamp = parsed.timestamp || "";
15578
+ if (parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
15183
15579
  const text2 = extractDesktopMessageText(parsed);
15184
- if (!text2) return;
15580
+ if (!text2) return true;
15185
15581
  records.push({
15186
- signature: createDesktopEventSignature(rawLine),
15582
+ signature,
15187
15583
  type: "message",
15188
15584
  role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
15189
15585
  content: parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2,
15190
- timestamp: parsed.timestamp || "",
15586
+ timestamp,
15191
15587
  ...activeTurnId ? { turnId: activeTurnId } : {}
15192
15588
  });
15193
- return;
15589
+ return true;
15194
15590
  }
15195
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "function_call") {
15591
+ if (parsed.payload?.type === "function_call") {
15196
15592
  const toolName = extractNormalizedFreeText(parsed.payload.name);
15197
- const toolId = extractNormalizedFreeText(parsed.payload.call_id) || createDesktopEventSignature(rawLine);
15198
- if (!toolName) return;
15593
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15594
+ if (!toolName) return true;
15595
+ if (toolName === "update_plan") {
15596
+ const tasks = parseUpdatePlanTasks(parsed.payload.arguments);
15597
+ activeSpecialCallIds.add(toolId);
15598
+ records.push({
15599
+ signature,
15600
+ type: "plan_update",
15601
+ content: "",
15602
+ timestamp,
15603
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15604
+ tasks
15605
+ });
15606
+ return true;
15607
+ }
15199
15608
  records.push({
15200
- signature: createDesktopEventSignature(rawLine),
15609
+ signature,
15201
15610
  type: "tool_started",
15202
15611
  content: "",
15203
- timestamp: parsed.timestamp || "",
15612
+ timestamp,
15204
15613
  ...activeTurnId ? { turnId: activeTurnId } : {},
15205
15614
  toolId,
15206
15615
  toolName
15207
15616
  });
15208
- return;
15617
+ return true;
15618
+ }
15619
+ if (parsed.payload?.type === "custom_tool_call") {
15620
+ const toolName = extractNormalizedFreeText(parsed.payload.name);
15621
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15622
+ if (!toolName) return true;
15623
+ if (toolName === "update_plan") {
15624
+ const tasks = parseUpdatePlanTasks(typeof parsed.payload.input === "string" ? parsed.payload.input : void 0);
15625
+ activeSpecialCallIds.add(toolId);
15626
+ records.push({
15627
+ signature,
15628
+ type: "plan_update",
15629
+ content: "",
15630
+ timestamp,
15631
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15632
+ tasks
15633
+ });
15634
+ return true;
15635
+ }
15636
+ records.push({
15637
+ signature,
15638
+ type: "tool_started",
15639
+ content: "",
15640
+ timestamp,
15641
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15642
+ toolId,
15643
+ toolName
15644
+ });
15645
+ return true;
15209
15646
  }
15210
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "function_call_output") {
15211
- const toolId = extractNormalizedFreeText(parsed.payload.call_id) || createDesktopEventSignature(rawLine);
15647
+ if (parsed.payload?.type === "function_call_output") {
15648
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15649
+ if (activeSpecialCallIds.has(toolId)) {
15650
+ activeSpecialCallIds.delete(toolId);
15651
+ return true;
15652
+ }
15212
15653
  records.push({
15213
- signature: createDesktopEventSignature(rawLine),
15654
+ signature,
15655
+ type: "tool_finished",
15656
+ content: extractToolOutputText(parsed.payload.output),
15657
+ timestamp,
15658
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15659
+ toolId,
15660
+ isError: parsed.payload.is_error === true
15661
+ });
15662
+ return true;
15663
+ }
15664
+ if (parsed.payload?.type === "custom_tool_call_output") {
15665
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15666
+ if (activeSpecialCallIds.has(toolId)) {
15667
+ activeSpecialCallIds.delete(toolId);
15668
+ return true;
15669
+ }
15670
+ records.push({
15671
+ signature,
15214
15672
  type: "tool_finished",
15215
- content: extractNormalizedFreeText(parsed.payload.output),
15216
- timestamp: parsed.timestamp || "",
15673
+ content: extractToolOutputText(parsed.payload.output),
15674
+ timestamp,
15217
15675
  ...activeTurnId ? { turnId: activeTurnId } : {},
15218
15676
  toolId,
15219
15677
  isError: parsed.payload.is_error === true
15220
15678
  });
15679
+ return true;
15221
15680
  }
15681
+ return false;
15222
15682
  }
15223
15683
  function parseDesktopSessionEventText(content, leadingText = "", flushTrailingText = false) {
15224
15684
  const combined = `${leadingText}${content}`;
@@ -15254,14 +15714,16 @@ function parseDesktopSessionEventText(content, leadingText = "", flushTrailingTe
15254
15714
  trailingText
15255
15715
  };
15256
15716
  }
15257
- function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingText = false, initialTurnId = null) {
15717
+ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingText = false, initialTurnId = null, initialSpecialCallIds = []) {
15258
15718
  const combined = `${leadingText}${content}`;
15259
15719
  if (!combined) {
15260
15720
  return {
15261
15721
  records: [],
15262
15722
  nextOffset: 0,
15263
15723
  trailingText: "",
15264
- nextTurnId: initialTurnId
15724
+ nextTurnId: initialTurnId,
15725
+ nextSpecialCallIds: [],
15726
+ unknownKinds: []
15265
15727
  };
15266
15728
  }
15267
15729
  const hasTrailingNewline = combined.endsWith("\n") || combined.endsWith("\r");
@@ -15273,6 +15735,8 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
15273
15735
  }
15274
15736
  const records = [];
15275
15737
  let activeTurnId = initialTurnId;
15738
+ const activeSpecialCallIds = new Set(initialSpecialCallIds);
15739
+ const unknownKinds = /* @__PURE__ */ new Set();
15276
15740
  for (const line of rawLines) {
15277
15741
  const trimmed = line.trim();
15278
15742
  if (!trimmed) continue;
@@ -15290,20 +15754,27 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
15290
15754
  const eventPayload = parsed.payload;
15291
15755
  activeTurnId = eventPayload?.turn_id || activeTurnId;
15292
15756
  }
15293
- pushDesktopMirrorRecord(records, parsed, trimmed, activeTurnId);
15294
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_complete") {
15757
+ const handled = pushDesktopMirrorRecord(records, parsed, trimmed, activeTurnId, activeSpecialCallIds);
15758
+ if (!handled) {
15759
+ const unknownKind = describeUnhandledMirrorLineKind(parsed);
15760
+ if (unknownKind) unknownKinds.add(unknownKind);
15761
+ }
15762
+ if (isSessionEventLine(parsed) && (parsed.payload?.type === "task_complete" || parsed.payload?.type === "turn_aborted")) {
15295
15763
  const eventPayload = parsed.payload;
15296
15764
  const completedTurnId = eventPayload?.turn_id || activeTurnId;
15297
15765
  if (!completedTurnId || completedTurnId === activeTurnId) {
15298
15766
  activeTurnId = null;
15299
15767
  }
15768
+ activeSpecialCallIds.clear();
15300
15769
  }
15301
15770
  }
15302
15771
  return {
15303
15772
  records,
15304
15773
  nextOffset: 0,
15305
15774
  trailingText,
15306
- nextTurnId: activeTurnId
15775
+ nextTurnId: activeTurnId,
15776
+ nextSpecialCallIds: Array.from(activeSpecialCallIds),
15777
+ unknownKinds: Array.from(unknownKinds)
15307
15778
  };
15308
15779
  }
15309
15780
  function readDesktopSessionMessages(threadId, limit = 8) {
@@ -15324,7 +15795,7 @@ function readDesktopSessionEventStreamByFilePath(filePath) {
15324
15795
  }
15325
15796
  return parseDesktopSessionEventText(content, "", true).events;
15326
15797
  }
15327
- function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null) {
15798
+ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
15328
15799
  let content = "";
15329
15800
  try {
15330
15801
  content = readFileUtf8Range(filePath, startOffset, endOffset);
@@ -15333,15 +15804,19 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
15333
15804
  records: [],
15334
15805
  nextOffset: startOffset,
15335
15806
  trailingText,
15336
- nextTurnId: currentTurnId
15807
+ nextTurnId: currentTurnId,
15808
+ nextSpecialCallIds: Array.from(currentSpecialCallIds),
15809
+ unknownKinds: []
15337
15810
  };
15338
15811
  }
15339
- const parsed = parseDesktopMirrorRecordText(content, trailingText, false, currentTurnId);
15812
+ const parsed = parseDesktopMirrorRecordText(content, trailingText, false, currentTurnId, currentSpecialCallIds);
15340
15813
  return {
15341
15814
  records: parsed.records,
15342
15815
  nextOffset: Math.max(startOffset, endOffset),
15343
15816
  trailingText: parsed.trailingText,
15344
- nextTurnId: parsed.nextTurnId
15817
+ nextTurnId: parsed.nextTurnId,
15818
+ nextSpecialCallIds: parsed.nextSpecialCallIds,
15819
+ unknownKinds: parsed.unknownKinds
15345
15820
  };
15346
15821
  }
15347
15822
  function readDesktopSessionEventStream(threadId) {
@@ -16446,6 +16921,8 @@ function formatHealthStatusLabel(healthStatus) {
16446
16921
  return "\u957F\u65F6\u8FD0\u884C\uFF0C\u5F85\u89C2\u5BDF";
16447
16922
  case "suspected_stall":
16448
16923
  return "\u7591\u4F3C\u5361\u4F4F";
16924
+ case "suspected_stream_ui_stall":
16925
+ return "\u6D41\u5F0F UI \u7591\u4F3C\u5361\u4F4F";
16449
16926
  case "suspected_detached":
16450
16927
  return "\u7591\u4F3C\u8131\u6302";
16451
16928
  case "completed":
@@ -16500,8 +16977,10 @@ function buildHealthCommandResponse(title, diagnosis, markdown = false) {
16500
16977
  ["\u5065\u5EB7\u72B6\u6001", formatHealthStatusLabel(diagnosis.healthStatus)],
16501
16978
  ["\u5F53\u524D\u9636\u6BB5", currentStage],
16502
16979
  ["\u6700\u540E\u8FDB\u5C55", formatCommandTimestamp(diagnosis.lastProgressAt)],
16980
+ ["\u6D41\u5F0F UI \u5237\u65B0", formatCommandTimestamp(diagnosis.lastStreamUiUpdateAt)],
16503
16981
  ["\u5DE5\u5177\u5F00\u59CB", formatCommandTimestamp(diagnosis.activeToolStartedAt)],
16504
16982
  ["\u6700\u8FD1\u5DE5\u5177\u5B8C\u6210", formatCommandTimestamp(diagnosis.lastToolFinishedAt)],
16983
+ ["\u6D41\u5F0F UI \u9519\u8BEF", diagnosis.lastStreamUiErrorAt ? `${formatCommandTimestamp(diagnosis.lastStreamUiErrorAt)}${diagnosis.lastStreamUiError ? ` \xB7 ${diagnosis.lastStreamUiError}` : ""}` : "-"],
16505
16984
  ["\u672C\u5730\u8FDB\u7A0B", formatHealthProcessProbe(diagnosis)]
16506
16985
  ],
16507
16986
  [diagnosis.healthReason],
@@ -16621,11 +17100,15 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
16621
17100
  streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
16622
17101
  startedAt: safeTimestamp,
16623
17102
  lastActivityAt: safeTimestamp,
17103
+ lastStatusText: null,
17104
+ lastStatusAt: 0,
17105
+ statusNote: null,
16624
17106
  userText: null,
16625
17107
  lastAssistantText: null,
16626
17108
  lastCommentaryText: null,
16627
17109
  streamedText: "",
16628
17110
  streamStarted: false,
17111
+ taskItems: [],
16629
17112
  toolCalls: /* @__PURE__ */ new Map()
16630
17113
  };
16631
17114
  }
@@ -16673,7 +17156,7 @@ function finalizeMirrorTurn(subscription, signature, timestamp, status, preferre
16673
17156
  pendingTurn.lastCommentaryText
16674
17157
  ].map((value) => (value || "").trim()).find(Boolean) || "";
16675
17158
  const userText = pendingTurn.userText?.trim() || null;
16676
- if (!text2 && !userText && pendingTurn.toolCalls.size === 0) return null;
17159
+ if (!text2 && !userText && pendingTurn.toolCalls.size === 0 && pendingTurn.taskItems.length === 0) return null;
16677
17160
  return {
16678
17161
  streamKey: pendingTurn.streamKey,
16679
17162
  userText,
@@ -16712,6 +17195,12 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
16712
17195
  if (completed) finalized.push(completed);
16713
17196
  continue;
16714
17197
  }
17198
+ if (record.type === "task_aborted") {
17199
+ ensureMirrorTurnState(subscription, record);
17200
+ const interrupted = finalizeMirrorTurn(subscription, record.signature, record.timestamp, "interrupted");
17201
+ if (interrupted) finalized.push(interrupted);
17202
+ continue;
17203
+ }
16715
17204
  if (record.type === "message" && record.role === "user") {
16716
17205
  const pendingTurn = ensureMirrorTurnState(subscription, record);
16717
17206
  const text2 = record.content.trim();
@@ -16740,6 +17229,20 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
16740
17229
  }
16741
17230
  continue;
16742
17231
  }
17232
+ if (record.type === "reasoning") {
17233
+ const pendingTurn = ensureMirrorTurnState(subscription, record);
17234
+ const text2 = record.content.trim();
17235
+ if (!text2) continue;
17236
+ pendingTurn.statusNote = text2;
17237
+ hooks.onStatusProgress?.(subscription, pendingTurn);
17238
+ continue;
17239
+ }
17240
+ if (record.type === "plan_update") {
17241
+ const pendingTurn = ensureMirrorTurnState(subscription, record);
17242
+ pendingTurn.taskItems = record.tasks || [];
17243
+ hooks.onTaskProgress?.(subscription, pendingTurn);
17244
+ continue;
17245
+ }
16743
17246
  if (record.type === "tool_started") {
16744
17247
  const pendingTurn = ensureMirrorTurnState(subscription, record);
16745
17248
  const toolId = record.toolId || record.signature;
@@ -16782,8 +17285,30 @@ function flushTimedOutMirrorTurn(subscription, idleTimeoutMs, nowMs = Date.now()
16782
17285
  "interrupted"
16783
17286
  );
16784
17287
  }
17288
+ function enqueuePendingMirrorDeliveries(subscription, turns) {
17289
+ if (turns.length === 0) return;
17290
+ const existingSignatures = new Set(subscription.pendingDeliveries.map((turn) => turn.signature));
17291
+ for (const turn of turns) {
17292
+ if (existingSignatures.has(turn.signature)) continue;
17293
+ subscription.pendingDeliveries.push(turn);
17294
+ existingSignatures.add(turn.signature);
17295
+ }
17296
+ }
17297
+ function removePendingMirrorDeliveries(subscription, turns) {
17298
+ if (turns.length === 0 || subscription.pendingDeliveries.length === 0) return;
17299
+ const deliveredSignatures = new Set(turns.map((turn) => turn.signature));
17300
+ subscription.pendingDeliveries = subscription.pendingDeliveries.filter(
17301
+ (turn) => !deliveredSignatures.has(turn.signature)
17302
+ );
17303
+ }
17304
+ function selectPendingMirrorDeliveries(subscription, blocked) {
17305
+ if (!blocked) {
17306
+ return subscription.pendingDeliveries.slice();
17307
+ }
17308
+ return subscription.pendingDeliveries.filter((turn) => turn.timedOut);
17309
+ }
16785
17310
  function hasPendingMirrorWork(subscription) {
16786
- return subscription.bufferedRecords.length > 0 || subscription.pendingTurn !== null;
17311
+ return subscription.bufferedRecords.length > 0 || subscription.pendingTurn !== null || subscription.pendingDeliveries.length > 0;
16787
17312
  }
16788
17313
  function consumeBufferedMirrorTurns(subscription, idleTimeoutMs, nowMs = Date.now(), hooks = {}) {
16789
17314
  const bufferedRecords = subscription.bufferedRecords;
@@ -18604,7 +19129,7 @@ function buildConversationPromptText(text2, files = []) {
18604
19129
 
18605
19130
  ${attachmentSupplement}` : attachmentSupplement;
18606
19131
  }
18607
- async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onPromptPrepared) {
19132
+ async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onTaskEvent, onStatusNote, onPromptPrepared) {
18608
19133
  const { store, llm } = getBridgeContext();
18609
19134
  const sessionId = binding.codepilotSessionId;
18610
19135
  const lockId = crypto8.randomBytes(8).toString("hex");
@@ -18723,14 +19248,22 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
18723
19248
  }
18724
19249
  }
18725
19250
  });
18726
- return await consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent);
19251
+ return await consumeStream(
19252
+ stream,
19253
+ sessionId,
19254
+ onPermissionRequest,
19255
+ onPartialText,
19256
+ onToolEvent,
19257
+ onTaskEvent,
19258
+ onStatusNote
19259
+ );
18727
19260
  } finally {
18728
19261
  clearInterval(renewalInterval);
18729
19262
  store.releaseSessionLock(sessionId, lockId);
18730
19263
  store.setSessionRuntimeStatus(sessionId, "idle");
18731
19264
  }
18732
19265
  }
18733
- async function consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent) {
19266
+ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent, onTaskEvent, onStatusNote) {
18734
19267
  const { store } = getBridgeContext();
18735
19268
  const contentBlocks = [];
18736
19269
  let currentText = "";
@@ -18839,6 +19372,12 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
18839
19372
  if (statusData.model) {
18840
19373
  store.updateSessionModel(sessionId, statusData.model);
18841
19374
  }
19375
+ if (typeof statusData.reasoning === "string" && onStatusNote) {
19376
+ try {
19377
+ onStatusNote(statusData.reasoning);
19378
+ } catch {
19379
+ }
19380
+ }
18842
19381
  } catch {
18843
19382
  }
18844
19383
  break;
@@ -18846,8 +19385,15 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
18846
19385
  case "task_update": {
18847
19386
  try {
18848
19387
  const taskData = JSON.parse(event.data);
18849
- if (taskData.session_id && taskData.todos) {
18850
- store.syncSdkTasks(taskData.session_id, taskData.todos);
19388
+ const tasks = Array.isArray(taskData.tasks) ? taskData.tasks : Array.isArray(taskData.todos) ? taskData.todos : null;
19389
+ if (tasks) {
19390
+ store.syncSdkTasks(sessionId, tasks);
19391
+ if (onTaskEvent) {
19392
+ try {
19393
+ onTaskEvent(tasks);
19394
+ } catch {
19395
+ }
19396
+ }
18851
19397
  }
18852
19398
  } catch {
18853
19399
  }
@@ -18945,6 +19491,14 @@ function pushStreamFeedbackTools(target, tools) {
18945
19491
  } catch {
18946
19492
  }
18947
19493
  }
19494
+ function pushStreamFeedbackTasks(target, tasks) {
19495
+ if (typeof target.adapter.onTaskEvent !== "function") return;
19496
+ target.ensureStarted?.();
19497
+ try {
19498
+ target.adapter.onTaskEvent(target.chatId, tasks, target.streamKey);
19499
+ } catch {
19500
+ }
19501
+ }
18948
19502
  function pushStreamFeedbackStatus(target, text2) {
18949
19503
  if (typeof target.adapter.onStreamStatus !== "function") return;
18950
19504
  target.ensureStarted?.();
@@ -19023,12 +19577,15 @@ function formatRuntimeDuration(ms) {
19023
19577
  if (seconds === 0) return `${hours}h ${minutes}m`;
19024
19578
  return `${hours}h ${minutes}m ${seconds}s`;
19025
19579
  }
19026
- function formatInteractiveRuntimeStatus(elapsedMs, silentMs) {
19580
+ function formatInteractiveRuntimeStatus(elapsedMs, silentMs, statusNote) {
19027
19581
  const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
19028
19582
  if (typeof silentMs === "number" && silentMs >= 0) {
19029
19583
  parts.push(`\u6700\u8FD1 ${formatRuntimeDuration(silentMs)} \u65E0\u65B0\u8F93\u51FA`);
19030
19584
  }
19031
- return parts.join("\uFF0C");
19585
+ const runtimeText = parts.join("\uFF0C");
19586
+ const note = (statusNote || "").trim();
19587
+ return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
19588
+ ${runtimeText}` : runtimeText;
19032
19589
  }
19033
19590
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19034
19591
  const binding = resolve(msg.address);
@@ -19123,23 +19680,35 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19123
19680
  adapter,
19124
19681
  channelType: adapter.channelType,
19125
19682
  chatId: msg.address.chatId,
19126
- streamKey
19683
+ streamKey,
19684
+ ensureStarted: () => {
19685
+ adapter.onMessageStart?.(msg.address.chatId, streamKey);
19686
+ }
19127
19687
  };
19128
19688
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
19129
19689
  const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
19690
+ let latestStatusNote = null;
19691
+ let latestTasks = [];
19130
19692
  const syncStructuredStreamUiState = () => {
19131
19693
  if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
19132
19694
  if (adapter.hasActiveStreamingUi?.(msg.address.chatId, streamKey)) {
19133
19695
  taskState.structuredStreamUiActive = true;
19134
19696
  }
19135
19697
  };
19698
+ const syncStructuredStreamUiSnapshot = () => {
19699
+ if (!supportsStructuredStreamUi) return;
19700
+ syncStructuredStreamUiState();
19701
+ const snapshot = adapter.getStructuredStreamingUiSnapshot?.(msg.address.chatId, streamKey);
19702
+ if (!snapshot) return;
19703
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, snapshot);
19704
+ };
19136
19705
  const pushRunningStatus = (silentMs) => {
19137
19706
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
19138
19707
  pushStreamFeedbackStatus(
19139
19708
  streamFeedbackTarget,
19140
- formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, silentMs)
19709
+ formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, silentMs, latestStatusNote)
19141
19710
  );
19142
- syncStructuredStreamUiState();
19711
+ syncStructuredStreamUiSnapshot();
19143
19712
  };
19144
19713
  let streamStatusHeartbeat = null;
19145
19714
  let streamStatusUpdatesClosed = false;
@@ -19173,7 +19742,24 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19173
19742
  pushStreamFeedbackTools(streamFeedbackTarget, Array.from(toolCallTracker.values()));
19174
19743
  }
19175
19744
  pushRunningStatus(null);
19176
- syncStructuredStreamUiState();
19745
+ syncStructuredStreamUiSnapshot();
19746
+ };
19747
+ const onTaskEvent = (tasks) => {
19748
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
19749
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19750
+ latestTasks = tasks;
19751
+ if (hasStreamingCards) {
19752
+ pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
19753
+ }
19754
+ pushRunningStatus(null);
19755
+ syncStructuredStreamUiSnapshot();
19756
+ };
19757
+ const onStatusNote = (note) => {
19758
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
19759
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19760
+ latestStatusNote = (note || "").trim() || null;
19761
+ pushRunningStatus(null);
19762
+ syncStructuredStreamUiSnapshot();
19177
19763
  };
19178
19764
  const onPartialText = (fullText) => {
19179
19765
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
@@ -19182,7 +19768,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19182
19768
  previewOnPartialText?.(fullText);
19183
19769
  onStreamCardText?.(fullText);
19184
19770
  pushRunningStatus(null);
19185
- syncStructuredStreamUiState();
19771
+ syncStructuredStreamUiSnapshot();
19186
19772
  };
19187
19773
  if (supportsStructuredStreamUi) {
19188
19774
  pushRunningStatus(null);
@@ -19196,11 +19782,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19196
19782
  return;
19197
19783
  }
19198
19784
  const elapsedMs = nowMs() - taskStartedAt;
19199
- if (elapsedMs < streamStatusIdleDetectionStartMs) return;
19200
19785
  const silentMs = nowMs() - taskState.lastActivityAt;
19201
- if (silentMs < streamStatusHeartbeatMs) return;
19202
- pushRunningStatus(silentMs);
19203
- syncStructuredStreamUiState();
19786
+ const showSilentDuration = elapsedMs >= streamStatusIdleDetectionStartMs && silentMs >= streamStatusHeartbeatMs ? silentMs : null;
19787
+ pushRunningStatus(showSilentDuration);
19788
+ syncStructuredStreamUiSnapshot();
19204
19789
  }, streamStatusHeartbeatMs);
19205
19790
  }
19206
19791
  let finalOutcome = "failed";
@@ -19227,11 +19812,16 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19227
19812
  "permission_wait",
19228
19813
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
19229
19814
  );
19815
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19816
+ pushRunningStatus(null);
19817
+ syncStructuredStreamUiSnapshot();
19230
19818
  },
19231
19819
  taskAbort.signal,
19232
19820
  attachments && attachments.length > 0 ? attachments : void 0,
19233
19821
  onPartialText,
19234
19822
  onToolEvent,
19823
+ onTaskEvent,
19824
+ onStatusNote,
19235
19825
  (preparedPrompt) => {
19236
19826
  if (!taskState.mirrorSuppressionId) {
19237
19827
  taskState.mirrorSuppressionId = deps.beginMirrorSuppression(binding.codepilotSessionId, preparedPrompt);
@@ -19254,14 +19844,16 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19254
19844
  }
19255
19845
  if (result.responseText || result.outboundAttachments.length > 0) {
19256
19846
  const textToDeliver = cardFinalized ? "" : result.responseText;
19257
- await deps.deliverResponse(
19258
- adapter,
19259
- msg.address,
19260
- textToDeliver,
19261
- binding.codepilotSessionId,
19262
- msg.messageId,
19263
- result.outboundAttachments
19264
- );
19847
+ if (!cardFinalized || result.outboundAttachments.length > 0) {
19848
+ await deps.deliverResponse(
19849
+ adapter,
19850
+ msg.address,
19851
+ textToDeliver,
19852
+ binding.codepilotSessionId,
19853
+ msg.messageId,
19854
+ result.outboundAttachments
19855
+ );
19856
+ }
19265
19857
  } else if (result.hasError) {
19266
19858
  await deps.deliverResponse(
19267
19859
  adapter,
@@ -19280,6 +19872,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19280
19872
  finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
19281
19873
  } finally {
19282
19874
  stopStructuredStreamStatusUpdates();
19875
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
19283
19876
  if (previewState) {
19284
19877
  if (previewState.throttleTimer) {
19285
19878
  clearTimeout(previewState.throttleTimer);
@@ -19318,6 +19911,14 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19318
19911
  }
19319
19912
 
19320
19913
  // src/lib/bridge/interactive-runtime.ts
19914
+ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
19915
+ "completed",
19916
+ "failed",
19917
+ "aborted"
19918
+ ]);
19919
+ function isTerminalSessionHealthStatus(status) {
19920
+ return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
19921
+ }
19321
19922
  function buildInteractiveIdleReminderNotice() {
19322
19923
  return [
19323
19924
  "\u63D0\u9192\uFF1A\u8FD9\u8F6E\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5DF2\u7ECF\u8D85\u8FC7 10 \u5206\u949F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8F93\u51FA\u3002",
@@ -19391,6 +19992,24 @@ function createInteractiveRuntime(getState2, options, deps) {
19391
19992
  await remindIdleInteractiveTask(task);
19392
19993
  }
19393
19994
  }
19995
+ function reconcileTerminalSessionRuntimeState() {
19996
+ const store = deps.getStore();
19997
+ for (const session of store.listSessions()) {
19998
+ const queuedCount = getQueuedCount(session.id);
19999
+ const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
20000
+ const hasActiveTask = getState2().activeTasks.has(session.id);
20001
+ if (hasActiveTask || queuedCount > 0) continue;
20002
+ if (!isTerminalSessionHealthStatus(session.health_status)) continue;
20003
+ if (persistedQueuedCount === 0 && session.runtime_status !== "running" && session.runtime_status !== "queued") {
20004
+ continue;
20005
+ }
20006
+ store.updateSession(session.id, {
20007
+ queued_count: 0,
20008
+ runtime_status: "idle",
20009
+ last_runtime_update_at: deps.nowIso()
20010
+ });
20011
+ }
20012
+ }
19394
20013
  function resetPersistedInteractiveRuntimeState() {
19395
20014
  const store = deps.getStore();
19396
20015
  for (const session of store.listSessions()) {
@@ -19452,6 +20071,7 @@ function createInteractiveRuntime(getState2, options, deps) {
19452
20071
  releaseInteractiveTask,
19453
20072
  syncSessionRuntimeState,
19454
20073
  reconcileIdleInteractiveTasks,
20074
+ reconcileTerminalSessionRuntimeState,
19455
20075
  resetPersistedInteractiveRuntimeState,
19456
20076
  processWithSessionLock
19457
20077
  };
@@ -19468,6 +20088,7 @@ function resetMirrorReadState(subscription) {
19468
20088
  subscription.fileIdentity = null;
19469
20089
  subscription.trailingText = "";
19470
20090
  subscription.activeMirrorTurnId = null;
20091
+ subscription.activeSpecialCallIds.clear();
19471
20092
  subscription.bufferedRecords = [];
19472
20093
  }
19473
20094
  function createMirrorSubscription(input) {
@@ -19491,8 +20112,11 @@ function createMirrorSubscription(input) {
19491
20112
  fileIdentity: null,
19492
20113
  trailingText: "",
19493
20114
  activeMirrorTurnId: null,
20115
+ activeSpecialCallIds: /* @__PURE__ */ new Set(),
19494
20116
  bufferedRecords: [],
19495
20117
  pendingTurn: null,
20118
+ pendingDeliveries: [],
20119
+ unknownMirrorKindsSeen: /* @__PURE__ */ new Set(),
19496
20120
  missingThreadPolls: 0,
19497
20121
  consecutiveFailures: 0,
19498
20122
  suspendedUntil: null
@@ -19503,6 +20127,8 @@ function resetMirrorSubscriptionForThreadChange(subscription, lastDeliveredAt) {
19503
20127
  subscription.lastDeliveredAt = lastDeliveredAt;
19504
20128
  subscription.dirty = true;
19505
20129
  subscription.pendingTurn = null;
20130
+ subscription.pendingDeliveries = [];
20131
+ subscription.unknownMirrorKindsSeen.clear();
19506
20132
  subscription.missingThreadPolls = 0;
19507
20133
  subscription.consecutiveFailures = 0;
19508
20134
  subscription.suspendedUntil = null;
@@ -19699,6 +20325,7 @@ function isMirrorSnapshotUnchanged(subscription, snapshot) {
19699
20325
  }
19700
20326
  function readMirrorDeliverableRecords(subscription, snapshot) {
19701
20327
  let deliverableRecords = [];
20328
+ let unknownKinds = [];
19702
20329
  const requiresFullRecover = !subscription.cursor.initialized || subscription.fileOffset === 0 || subscription.fileIdentity !== null && subscription.fileIdentity !== snapshot.identity || subscription.fileSize !== null && snapshot.size < subscription.fileOffset || subscription.fileSize !== null && snapshot.size === subscription.fileOffset && subscription.fileMtimeMs !== null && snapshot.mtimeMs !== subscription.fileMtimeMs;
19703
20330
  if (requiresFullRecover) {
19704
20331
  const previousCursor = subscription.cursor;
@@ -19707,7 +20334,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19707
20334
  0,
19708
20335
  snapshot.size,
19709
20336
  "",
19710
- null
20337
+ null,
20338
+ []
19711
20339
  );
19712
20340
  const delta = reconcileDesktopMirrorCursor(subscription.cursor, fullDelta.records);
19713
20341
  subscription.cursor = delta.nextCursor;
@@ -19715,6 +20343,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19715
20343
  subscription.trailingText = "";
19716
20344
  subscription.fileOffset = snapshot.size;
19717
20345
  subscription.activeMirrorTurnId = fullDelta.nextTurnId;
20346
+ subscription.activeSpecialCallIds = new Set(fullDelta.nextSpecialCallIds);
20347
+ unknownKinds = fullDelta.unknownKinds;
19718
20348
  } else if (snapshot.size > subscription.fileOffset || subscription.trailingText) {
19719
20349
  const previousCursor = subscription.cursor;
19720
20350
  const delta = readDesktopSessionMirrorRecordDeltaByFilePath(
@@ -19722,19 +20352,25 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19722
20352
  subscription.fileOffset,
19723
20353
  snapshot.size,
19724
20354
  subscription.trailingText,
19725
- subscription.activeMirrorTurnId
20355
+ subscription.activeMirrorTurnId,
20356
+ subscription.activeSpecialCallIds
19726
20357
  );
19727
20358
  deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.records);
19728
20359
  subscription.cursor = advanceDesktopMirrorCursor(subscription.cursor, delta.records);
19729
20360
  subscription.trailingText = delta.trailingText;
19730
20361
  subscription.fileOffset = delta.nextOffset;
19731
20362
  subscription.activeMirrorTurnId = delta.nextTurnId;
20363
+ subscription.activeSpecialCallIds = new Set(delta.nextSpecialCallIds);
20364
+ unknownKinds = delta.unknownKinds;
19732
20365
  }
19733
20366
  subscription.fileSize = snapshot.size;
19734
20367
  subscription.fileMtimeMs = snapshot.mtimeMs;
19735
20368
  subscription.fileIdentity = snapshot.identity;
19736
20369
  subscription.dirty = false;
19737
- return deliverableRecords;
20370
+ return {
20371
+ records: deliverableRecords,
20372
+ unknownKinds
20373
+ };
19738
20374
  }
19739
20375
 
19740
20376
  // src/lib/bridge/mirror-delivery-plan.ts
@@ -19749,7 +20385,7 @@ function buildMirrorDeliveryPlan(subscription, deliverableRecords, options) {
19749
20385
  if (options.blocked) {
19750
20386
  return {
19751
20387
  syncReason: "mirror reconcile active task",
19752
- turnsToDeliver: timedOutTurn ? [timedOutTurn] : []
20388
+ finalizedTurns: timedOutTurn ? [timedOutTurn] : []
19753
20389
  };
19754
20390
  }
19755
20391
  const finalizedTurns = timedOutTurn ? [timedOutTurn] : [];
@@ -19757,12 +20393,12 @@ function buildMirrorDeliveryPlan(subscription, deliverableRecords, options) {
19757
20393
  if (finalizedTurns.length === 0) {
19758
20394
  return {
19759
20395
  syncReason: "mirror reconcile no finalized turns",
19760
- turnsToDeliver: []
20396
+ finalizedTurns: []
19761
20397
  };
19762
20398
  }
19763
20399
  return {
19764
20400
  syncReason: "mirror reconcile delivered turns",
19765
- turnsToDeliver: finalizedTurns
20401
+ finalizedTurns
19766
20402
  };
19767
20403
  }
19768
20404
 
@@ -19986,21 +20622,36 @@ function createMirrorRuntime(getState2, options, deps) {
19986
20622
  deps.syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile unchanged snapshot");
19987
20623
  return "processed";
19988
20624
  }
19989
- const deliverableRecords = readMirrorDeliverableRecords(subscription, snapshot);
20625
+ const readResult = readMirrorDeliverableRecords(subscription, snapshot);
20626
+ const deliverableRecords = readResult.records;
20627
+ for (const kind of readResult.unknownKinds) {
20628
+ if (subscription.unknownMirrorKindsSeen.has(kind)) continue;
20629
+ subscription.unknownMirrorKindsSeen.add(kind);
20630
+ console.warn(
20631
+ `[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
20632
+ );
20633
+ }
19990
20634
  if (deliverableRecords.length > 0) {
19991
20635
  deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, deliverableRecords);
19992
20636
  }
20637
+ const blocked = getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId);
19993
20638
  const deliveryPlan = buildMirrorDeliveryPlan(subscription, deliverableRecords, {
19994
- blocked: getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId),
20639
+ blocked,
19995
20640
  filterSuppressedRecords: deps.filterSuppressedMirrorRecords,
19996
20641
  flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
19997
20642
  consumeBufferedTurns: (currentSubscription) => deps.consumeBufferedMirrorTurns(currentSubscription)
19998
20643
  });
19999
- if (deliveryPlan.turnsToDeliver.length > 0) {
20000
- try {
20001
- await deps.deliverMirrorTurns(subscription, deliveryPlan.turnsToDeliver);
20002
- } catch (error) {
20003
- subscription.dirty = true;
20644
+ if (deliveryPlan.finalizedTurns.length > 0) {
20645
+ enqueuePendingMirrorDeliveries(subscription, deliveryPlan.finalizedTurns);
20646
+ }
20647
+ const turnsToAttempt = selectPendingMirrorDeliveries(subscription, blocked);
20648
+ if (turnsToAttempt.length > 0) {
20649
+ const deliveryResult = await deps.deliverMirrorTurns(subscription, turnsToAttempt);
20650
+ if (deliveryResult.deliveredCount > 0) {
20651
+ removePendingMirrorDeliveries(subscription, turnsToAttempt.slice(0, deliveryResult.deliveredCount));
20652
+ }
20653
+ if (deliveryResult.error) {
20654
+ const error = deliveryResult.error;
20004
20655
  console.warn("[bridge-manager] Mirror delivery failed:", error instanceof Error ? error.message : error);
20005
20656
  }
20006
20657
  }
@@ -20174,11 +20825,13 @@ var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
20174
20825
  var HEALTH_SLOW_OBSERVED_MS = 30 * 60 * 1e3;
20175
20826
  var HEALTH_PROGRESS_PERSIST_THROTTLE_MS = 15 * 1e3;
20176
20827
  var HEALTH_PROCESS_PROBE_CACHE_MS = 30 * 1e3;
20828
+ var HEALTH_STREAM_UI_STALL_MS = 60 * 1e3;
20177
20829
  var RUNNING_HEALTH_STATUSES = /* @__PURE__ */ new Set([
20178
20830
  "running_active",
20179
20831
  "waiting_tool",
20180
20832
  "slow_observed",
20181
20833
  "suspected_stall",
20834
+ "suspected_stream_ui_stall",
20182
20835
  "suspected_detached"
20183
20836
  ]);
20184
20837
  function parseIsoMs(value) {
@@ -20246,6 +20899,10 @@ function buildProgressReason(type, detail) {
20246
20899
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u684C\u9762\u4F1A\u8BDD\u6D88\u606F\u3002";
20247
20900
  case "commentary":
20248
20901
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u6267\u884C\u8FDB\u5C55\u8BF4\u660E\u3002";
20902
+ case "reasoning":
20903
+ return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u601D\u8003/\u72B6\u6001\u8BF4\u660E\u3002";
20904
+ case "plan_update":
20905
+ return "\u6700\u8FD1\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
20249
20906
  case "text":
20250
20907
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u6B63\u6587\u8F93\u51FA\u3002";
20251
20908
  case "permission_wait":
@@ -20293,6 +20950,12 @@ function computeBaseDiagnosis(session, nowMs) {
20293
20950
  const activeToolName = summarizeActiveTools(activeTools) || trimOrNull(session.active_tool_name);
20294
20951
  const activeToolStartedAt = getActiveToolStartedAt(activeTools) || trimOrNull(session.active_tool_started_at);
20295
20952
  const lastToolFinishedAt = trimOrNull(session.last_tool_finished_at);
20953
+ const lastStreamUiAttemptAt = trimOrNull(session.last_stream_ui_attempt_at);
20954
+ const lastStreamUiUpdateAt = trimOrNull(session.last_stream_ui_update_at);
20955
+ const streamUiFlushStartedAt = trimOrNull(session.stream_ui_flush_started_at);
20956
+ const lastStreamUiErrorAt = trimOrNull(session.last_stream_ui_error_at);
20957
+ const lastStreamUiError = trimOrNull(session.last_stream_ui_error);
20958
+ 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;
20296
20959
  const sdkSessionId = trimOrNull(session.sdk_session_id);
20297
20960
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
20298
20961
  const previousStatus = session.health_status || "idle";
@@ -20308,6 +20971,12 @@ function computeBaseDiagnosis(session, nowMs) {
20308
20971
  activeToolName,
20309
20972
  activeToolStartedAt,
20310
20973
  lastToolFinishedAt,
20974
+ lastStreamUiAttemptAt,
20975
+ lastStreamUiUpdateAt,
20976
+ streamUiFlushStartedAt,
20977
+ lastStreamUiErrorAt,
20978
+ lastStreamUiError,
20979
+ streamUiConsecutiveFailures,
20311
20980
  sdkSessionId
20312
20981
  };
20313
20982
  }
@@ -20339,6 +21008,12 @@ function computeBaseDiagnosis(session, nowMs) {
20339
21008
  activeToolName,
20340
21009
  activeToolStartedAt,
20341
21010
  lastToolFinishedAt,
21011
+ lastStreamUiAttemptAt,
21012
+ lastStreamUiUpdateAt,
21013
+ streamUiFlushStartedAt,
21014
+ lastStreamUiErrorAt,
21015
+ lastStreamUiError,
21016
+ streamUiConsecutiveFailures,
20342
21017
  sdkSessionId
20343
21018
  };
20344
21019
  }
@@ -20379,11 +21054,75 @@ function applyProcessProbeDiagnosis(diagnosis, processProbe) {
20379
21054
  processProbe
20380
21055
  };
20381
21056
  }
21057
+ function applyStreamUiDiagnosis(diagnosis, nowMs) {
21058
+ if (!isRunningRuntimeStatus(diagnosis.runtimeStatus)) {
21059
+ return diagnosis;
21060
+ }
21061
+ const lastProgressMs = parseIsoMs(diagnosis.lastProgressAt || void 0);
21062
+ if (!lastProgressMs || nowMs - lastProgressMs > HEALTH_RECENT_PROGRESS_MS) {
21063
+ return diagnosis;
21064
+ }
21065
+ const lastStreamUiUpdateMs = parseIsoMs(diagnosis.lastStreamUiUpdateAt || void 0);
21066
+ const lastStreamUiAttemptMs = parseIsoMs(diagnosis.lastStreamUiAttemptAt || void 0);
21067
+ const streamUiFlushStartedMs = parseIsoMs(diagnosis.streamUiFlushStartedAt || void 0);
21068
+ const lastStreamUiErrorText = diagnosis.lastStreamUiError?.trim();
21069
+ if (streamUiFlushStartedMs && nowMs - streamUiFlushStartedMs >= HEALTH_STREAM_UI_STALL_MS) {
21070
+ const details = ["\u4EFB\u52A1\u4ECD\u5728\u7EE7\u7EED\uFF0C\u4F46\u6D41\u5F0F UI \u5237\u65B0\u8BF7\u6C42\u5DF2\u957F\u65F6\u95F4\u672A\u5B8C\u6210\uFF0C\u7591\u4F3C\u5361\u4F4F\u3002"];
21071
+ if (diagnosis.streamUiConsecutiveFailures > 0) {
21072
+ details.push(`\u6700\u8FD1\u8FDE\u7EED\u5931\u8D25 ${diagnosis.streamUiConsecutiveFailures} \u6B21\u3002`);
21073
+ }
21074
+ if (lastStreamUiErrorText) {
21075
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21076
+ }
21077
+ return {
21078
+ ...diagnosis,
21079
+ healthStatus: "suspected_stream_ui_stall",
21080
+ healthReason: details.join(" ")
21081
+ };
21082
+ }
21083
+ if (lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
21084
+ 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"];
21085
+ if (lastStreamUiErrorText) {
21086
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21087
+ }
21088
+ return {
21089
+ ...diagnosis,
21090
+ healthStatus: "suspected_stream_ui_stall",
21091
+ healthReason: details.join(" ")
21092
+ };
21093
+ }
21094
+ if (!lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
21095
+ 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"];
21096
+ if (lastStreamUiErrorText) {
21097
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21098
+ }
21099
+ return {
21100
+ ...diagnosis,
21101
+ healthStatus: "suspected_stream_ui_stall",
21102
+ healthReason: details.join(" ")
21103
+ };
21104
+ }
21105
+ return diagnosis;
21106
+ }
20382
21107
 
20383
21108
  // src/lib/bridge/session-health-runtime.ts
20384
21109
  function createSessionHealthRuntime(deps) {
20385
21110
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
20386
21111
  const processProbeCache = /* @__PURE__ */ new Map();
21112
+ function summarizePlanUpdate(tasks) {
21113
+ if (!Array.isArray(tasks) || tasks.length === 0) {
21114
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
21115
+ }
21116
+ let inProgress = 0;
21117
+ let pending = 0;
21118
+ let completed = 0;
21119
+ for (const task of tasks) {
21120
+ if (task?.status === "completed") completed += 1;
21121
+ else if (task?.status === "in_progress") inProgress += 1;
21122
+ else pending += 1;
21123
+ }
21124
+ return `\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\uFF08\u6267\u884C\u4E2D ${inProgress} \u9879\uFF0C\u7B49\u5F85\u4E2D ${pending} \u9879\uFF0C\u5DF2\u5B8C\u6210 ${completed} \u9879\uFF09\u3002`;
21125
+ }
20387
21126
  function updateSessionHealth(sessionId, updates, options) {
20388
21127
  const store = deps.getStore();
20389
21128
  const session = store.getSession(sessionId);
@@ -20426,7 +21165,13 @@ function createSessionHealthRuntime(deps) {
20426
21165
  active_tools_json: void 0,
20427
21166
  active_tool_name: void 0,
20428
21167
  active_tool_started_at: void 0,
20429
- last_tool_finished_at: void 0
21168
+ last_tool_finished_at: void 0,
21169
+ last_stream_ui_attempt_at: void 0,
21170
+ last_stream_ui_update_at: void 0,
21171
+ stream_ui_flush_started_at: void 0,
21172
+ last_stream_ui_error_at: void 0,
21173
+ last_stream_ui_error: void 0,
21174
+ stream_ui_consecutive_failures: void 0
20430
21175
  });
20431
21176
  }
20432
21177
  function recordInteractiveProgress(sessionId, type, detail) {
@@ -20494,11 +21239,29 @@ function createSessionHealthRuntime(deps) {
20494
21239
  active_tools_json: void 0,
20495
21240
  active_tool_name: void 0,
20496
21241
  active_tool_started_at: void 0,
21242
+ stream_ui_flush_started_at: void 0,
20497
21243
  last_health_check_at: nowIso4
20498
21244
  }, { force: true });
20499
21245
  lastProgressPersistAt.set(sessionId, Date.now());
20500
21246
  processProbeCache.delete(sessionId);
20501
21247
  }
21248
+ function toIso(value) {
21249
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
21250
+ return new Date(value).toISOString();
21251
+ }
21252
+ function recordStructuredStreamUi(sessionId, snapshot) {
21253
+ const updates = {
21254
+ stream_ui_flush_started_at: snapshot.active && snapshot.flushInFlight ? toIso(snapshot.flushInFlightSince ?? snapshot.lastAttemptAt) : void 0
21255
+ };
21256
+ if (snapshot.active) {
21257
+ updates.last_stream_ui_attempt_at = toIso(snapshot.lastAttemptAt);
21258
+ updates.last_stream_ui_update_at = toIso(snapshot.lastUpdateAt);
21259
+ updates.last_stream_ui_error_at = toIso(snapshot.lastErrorAt);
21260
+ updates.last_stream_ui_error = snapshot.lastError?.trim() || void 0;
21261
+ updates.stream_ui_consecutive_failures = snapshot.consecutiveFailures && snapshot.consecutiveFailures > 0 ? snapshot.consecutiveFailures : void 0;
21262
+ }
21263
+ updateSessionHealth(sessionId, updates);
21264
+ }
20502
21265
  function observeDesktopMirrorRecords(sessionId, _threadId, records) {
20503
21266
  for (const record of records) {
20504
21267
  if (record.type === "task_started") {
@@ -20509,6 +21272,10 @@ function createSessionHealthRuntime(deps) {
20509
21272
  recordInteractiveEnd(sessionId, "completed", "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002");
20510
21273
  continue;
20511
21274
  }
21275
+ if (record.type === "task_aborted") {
21276
+ recordInteractiveEnd(sessionId, "aborted", "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002");
21277
+ continue;
21278
+ }
20512
21279
  if (record.type === "tool_started") {
20513
21280
  recordToolState(sessionId, record.toolId || record.signature, record.toolName || "tool", "running");
20514
21281
  continue;
@@ -20522,6 +21289,22 @@ function createSessionHealthRuntime(deps) {
20522
21289
  );
20523
21290
  continue;
20524
21291
  }
21292
+ if (record.type === "reasoning") {
21293
+ recordInteractiveProgress(
21294
+ sessionId,
21295
+ "reasoning",
21296
+ "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u65B0\u7684\u601D\u8003/\u72B6\u6001\u8BF4\u660E\u3002"
21297
+ );
21298
+ continue;
21299
+ }
21300
+ if (record.type === "plan_update") {
21301
+ recordInteractiveProgress(
21302
+ sessionId,
21303
+ "plan_update",
21304
+ summarizePlanUpdate(record.tasks)
21305
+ );
21306
+ continue;
21307
+ }
20525
21308
  if (record.type === "message") {
20526
21309
  recordInteractiveProgress(
20527
21310
  sessionId,
@@ -20536,7 +21319,10 @@ function createSessionHealthRuntime(deps) {
20536
21319
  const store = deps.getStore();
20537
21320
  for (const session of store.listSessions()) {
20538
21321
  if (!shouldTrackSession(session)) continue;
20539
- const diagnosis = computeBaseDiagnosis(session, nowMs);
21322
+ const diagnosis = applyStreamUiDiagnosis(
21323
+ { ...computeBaseDiagnosis(session, nowMs), processProbe: null },
21324
+ nowMs
21325
+ );
20540
21326
  updateSessionHealth(session.id, {
20541
21327
  health_status: diagnosis.healthStatus,
20542
21328
  health_reason: diagnosis.healthReason
@@ -20564,7 +21350,10 @@ function createSessionHealthRuntime(deps) {
20564
21350
  if (!session) return null;
20565
21351
  const base = computeBaseDiagnosis(session, Date.now());
20566
21352
  const processProbe = await loadProcessProbe(session);
20567
- const diagnosis = applyProcessProbeDiagnosis(base, processProbe);
21353
+ const diagnosis = applyStreamUiDiagnosis(
21354
+ applyProcessProbeDiagnosis(base, processProbe),
21355
+ Date.now()
21356
+ );
20568
21357
  updateSessionHealth(sessionId, {
20569
21358
  health_status: diagnosis.healthStatus,
20570
21359
  health_reason: diagnosis.healthReason,
@@ -20582,6 +21371,7 @@ function createSessionHealthRuntime(deps) {
20582
21371
  recordInteractiveStart,
20583
21372
  recordInteractiveProgress,
20584
21373
  recordToolState,
21374
+ recordStructuredStreamUi,
20585
21375
  recordInteractiveEnd,
20586
21376
  observeDesktopMirrorRecords,
20587
21377
  reconcileSessionHealth,
@@ -20601,6 +21391,8 @@ var MIRROR_EVENT_BATCH_LIMIT = 8;
20601
21391
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
20602
21392
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
20603
21393
  var INTERACTIVE_IDLE_REMINDER_MS = 6e5;
21394
+ var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
21395
+ var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
20604
21396
  var MIRROR_IDLE_TIMEOUT_MS = 6e5;
20605
21397
  function describeUnknownError(error) {
20606
21398
  if (error instanceof Error) {
@@ -20782,6 +21574,21 @@ function getMirrorStreamingText(subscription, turnState) {
20782
21574
  );
20783
21575
  return rendered || buildMirrorTitle(title, markdown);
20784
21576
  }
21577
+ function getMirrorStructuredStreamStatusConfig() {
21578
+ const { store } = getBridgeContext();
21579
+ const idleStartSeconds = parseInt(store.getSetting("bridge_stream_status_idle_start_seconds") || "", 10);
21580
+ const heartbeatSeconds = parseInt(store.getSetting("bridge_stream_status_check_interval_seconds") || "", 10);
21581
+ return {
21582
+ idleStartMs: Math.max(
21583
+ 0,
21584
+ (Number.isFinite(idleStartSeconds) && idleStartSeconds > 0 ? idleStartSeconds : MIRROR_STREAM_STATUS_IDLE_START_MS / 1e3) * 1e3
21585
+ ),
21586
+ heartbeatMs: Math.max(
21587
+ 1e3,
21588
+ (Number.isFinite(heartbeatSeconds) && heartbeatSeconds > 0 ? heartbeatSeconds : MIRROR_STREAM_STATUS_HEARTBEAT_MS / 1e3) * 1e3
21589
+ )
21590
+ };
21591
+ }
20785
21592
  function startMirrorStreaming(subscription, turnState) {
20786
21593
  const adapter = getMirrorStreamingAdapter(subscription);
20787
21594
  if (!adapter || turnState.streamStarted) return;
@@ -20805,6 +21612,51 @@ function createMirrorStreamFeedbackTarget(subscription, turnState, adapter) {
20805
21612
  }
20806
21613
  };
20807
21614
  }
21615
+ function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
21616
+ const adapter = getMirrorStreamingAdapter(subscription);
21617
+ if (!adapter || typeof adapter.onStreamStatus !== "function") return;
21618
+ if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
21619
+ const startedAtMs = Date.parse(turnState.startedAt);
21620
+ if (!Number.isFinite(startedAtMs)) return;
21621
+ const nowMs = options.nowMs ?? Date.now();
21622
+ const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
21623
+ if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
21624
+ return;
21625
+ }
21626
+ const statusText = formatInteractiveRuntimeStatus(
21627
+ Math.max(0, nowMs - startedAtMs),
21628
+ options.silentMs,
21629
+ turnState.statusNote
21630
+ );
21631
+ if (turnState.lastStatusText === statusText) return;
21632
+ pushStreamFeedbackStatus(
21633
+ createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
21634
+ statusText
21635
+ );
21636
+ turnState.lastStatusText = statusText;
21637
+ turnState.lastStatusAt = nowMs;
21638
+ }
21639
+ function refreshMirrorStreamingStatus(subscription, nowMs = Date.now(), config2 = getMirrorStructuredStreamStatusConfig()) {
21640
+ const pendingTurn = subscription.pendingTurn;
21641
+ if (!pendingTurn?.streamStarted) return;
21642
+ const startedAtMs = Date.parse(pendingTurn.startedAt);
21643
+ const lastActivityMs = Date.parse(pendingTurn.lastActivityAt);
21644
+ if (!Number.isFinite(startedAtMs) || !Number.isFinite(lastActivityMs)) return;
21645
+ const elapsedMs = nowMs - startedAtMs;
21646
+ if (elapsedMs < config2.idleStartMs) return;
21647
+ const silentMs = nowMs - lastActivityMs;
21648
+ if (silentMs < config2.heartbeatMs) return;
21649
+ pushMirrorStreamingStatus(subscription, pendingTurn, {
21650
+ nowMs,
21651
+ silentMs,
21652
+ minIntervalMs: config2.heartbeatMs
21653
+ });
21654
+ }
21655
+ function refreshActiveMirrorStreamingStatuses(nowMs = Date.now()) {
21656
+ for (const subscription of getState().mirrorSubscriptions.values()) {
21657
+ refreshMirrorStreamingStatus(subscription, nowMs);
21658
+ }
21659
+ }
20808
21660
  function updateMirrorStreaming(subscription, turnState) {
20809
21661
  const adapter = getMirrorStreamingAdapter(subscription);
20810
21662
  if (!adapter) return;
@@ -20812,6 +21664,7 @@ function updateMirrorStreaming(subscription, turnState) {
20812
21664
  createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
20813
21665
  getMirrorStreamingText(subscription, turnState)
20814
21666
  );
21667
+ pushMirrorStreamingStatus(subscription, turnState);
20815
21668
  }
20816
21669
  function updateMirrorToolProgress(subscription, turnState) {
20817
21670
  const adapter = getMirrorStreamingAdapter(subscription);
@@ -20820,6 +21673,21 @@ function updateMirrorToolProgress(subscription, turnState) {
20820
21673
  createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
20821
21674
  Array.from(turnState.toolCalls.values())
20822
21675
  );
21676
+ pushMirrorStreamingStatus(subscription, turnState);
21677
+ }
21678
+ function updateMirrorTaskProgress(subscription, turnState) {
21679
+ const adapter = getMirrorStreamingAdapter(subscription);
21680
+ if (!adapter) return;
21681
+ pushStreamFeedbackTasks(
21682
+ createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
21683
+ turnState.taskItems
21684
+ );
21685
+ pushMirrorStreamingStatus(subscription, turnState);
21686
+ }
21687
+ function updateMirrorStatusProgress(subscription, turnState) {
21688
+ const adapter = getMirrorStreamingAdapter(subscription);
21689
+ if (!adapter) return;
21690
+ pushMirrorStreamingStatus(subscription, turnState);
20823
21691
  }
20824
21692
  function stopMirrorStreaming(subscription, status = "interrupted") {
20825
21693
  const adapter = getMirrorStreamingAdapter(subscription);
@@ -20881,12 +21749,21 @@ async function deliverMirrorTurn(subscription, turn) {
20881
21749
  subscription.lastDeliveredAt = turn.timestamp || nowIso3();
20882
21750
  }
20883
21751
  async function deliverMirrorTurns(subscription, turns) {
20884
- for (const turn of turns.slice(-MIRROR_EVENT_BATCH_LIMIT)) {
20885
- await deliverMirrorTurn(subscription, turn);
21752
+ let deliveredCount = 0;
21753
+ for (const turn of turns.slice(0, MIRROR_EVENT_BATCH_LIMIT)) {
21754
+ try {
21755
+ await deliverMirrorTurn(subscription, turn);
21756
+ deliveredCount += 1;
21757
+ } catch (error) {
21758
+ return { deliveredCount, error };
21759
+ }
20886
21760
  }
21761
+ return { deliveredCount };
20887
21762
  }
20888
21763
  var MIRROR_TURN_HOOKS = {
20889
21764
  onStreamText: updateMirrorStreaming,
21765
+ onStatusProgress: updateMirrorStatusProgress,
21766
+ onTaskProgress: updateMirrorTaskProgress,
20890
21767
  onToolProgress: updateMirrorToolProgress
20891
21768
  };
20892
21769
  function consumeMirrorRecords2(subscription, records) {
@@ -20928,6 +21805,7 @@ function resetMirrorSessionForInteractiveRun(sessionId) {
20928
21805
  }
20929
21806
  async function reconcileMirrorSubscriptions() {
20930
21807
  await MIRROR_RUNTIME.reconcileMirrorSubscriptions();
21808
+ refreshActiveMirrorStreamingStatuses();
20931
21809
  }
20932
21810
  function clearMirrorSubscriptions() {
20933
21811
  MIRROR_RUNTIME.clearMirrorSubscriptions();
@@ -20978,6 +21856,7 @@ async function start() {
20978
21856
  });
20979
21857
  try {
20980
21858
  SESSION_HEALTH_RUNTIME.reconcileSessionHealth();
21859
+ INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState();
20981
21860
  } catch (err) {
20982
21861
  console.error("[bridge-manager] Session health reconcile failed:", describeUnknownError(err));
20983
21862
  }
@@ -21171,6 +22050,9 @@ async function handleMessage(adapter, msg) {
21171
22050
  recordInteractiveHealthTool: (sessionId, toolId, toolName, status) => {
21172
22051
  SESSION_HEALTH_RUNTIME.recordToolState(sessionId, toolId, toolName, status);
21173
22052
  },
22053
+ recordInteractiveStreamUiSnapshot: (sessionId, snapshot) => {
22054
+ SESSION_HEALTH_RUNTIME.recordStructuredStreamUi(sessionId, snapshot);
22055
+ },
21174
22056
  recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
21175
22057
  beginMirrorSuppression: beginMirrorSuppression2,
21176
22058
  abortMirrorSuppression: abortMirrorSuppression2,