codex-to-im 1.0.40 → 1.0.42

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
@@ -3935,9 +3935,9 @@ var codex_provider_exports = {};
3935
3935
  __export(codex_provider_exports, {
3936
3936
  CodexProvider: () => CodexProvider
3937
3937
  });
3938
- import fs14 from "node:fs";
3938
+ import fs15 from "node:fs";
3939
3939
  import os4 from "node:os";
3940
- import path15 from "node:path";
3940
+ import path16 from "node:path";
3941
3941
  function toApprovalPolicy(permissionMode) {
3942
3942
  switch (permissionMode) {
3943
3943
  case "never":
@@ -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"() {
@@ -3979,6 +4028,9 @@ var init_codex_provider = __esm({
3979
4028
  threadIds = /* @__PURE__ */ new Map();
3980
4029
  constructor(_pendingPerms) {
3981
4030
  }
4031
+ clearCachedThreadId(sessionId) {
4032
+ this.threadIds.delete(sessionId);
4033
+ }
3982
4034
  /**
3983
4035
  * Lazily load the Codex SDK. Throws a clear error if the installation is incomplete.
3984
4036
  */
@@ -4032,13 +4084,13 @@ var init_codex_provider = __esm({
4032
4084
  { type: "text", text: params.prompt }
4033
4085
  ];
4034
4086
  for (const file of imageFiles) {
4035
- if (file.filePath && fs14.existsSync(file.filePath)) {
4087
+ if (file.filePath && fs15.existsSync(file.filePath)) {
4036
4088
  parts.push({ type: "local_image", path: file.filePath });
4037
4089
  continue;
4038
4090
  }
4039
4091
  const ext = MIME_EXT[file.type] || ".png";
4040
- const tmpPath = path15.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
4041
- fs14.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
4092
+ const tmpPath = path16.join(os4.tmpdir(), `cti-img-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
4093
+ fs15.writeFileSync(tmpPath, Buffer.from(file.data, "base64"));
4042
4094
  tempFiles.push(tmpPath);
4043
4095
  parts.push({ type: "local_image", path: tmpPath });
4044
4096
  }
@@ -4047,6 +4099,7 @@ var init_codex_provider = __esm({
4047
4099
  input = params.prompt;
4048
4100
  }
4049
4101
  let retryFresh = false;
4102
+ const emittedToolStarts = /* @__PURE__ */ new Set();
4050
4103
  while (true) {
4051
4104
  let thread;
4052
4105
  if (savedThreadId) {
@@ -4078,9 +4131,19 @@ var init_codex_provider = __esm({
4078
4131
  }));
4079
4132
  break;
4080
4133
  }
4134
+ case "turn.started":
4135
+ break;
4136
+ case "item.started":
4137
+ case "item.updated":
4081
4138
  case "item.completed": {
4082
4139
  const item = event.item;
4083
- self.handleCompletedItem(controller, item);
4140
+ self.handleItemEvent(
4141
+ controller,
4142
+ item,
4143
+ event.type === "item.started" ? "started" : event.type === "item.updated" ? "updated" : "completed",
4144
+ params.sessionId,
4145
+ emittedToolStarts
4146
+ );
4084
4147
  break;
4085
4148
  }
4086
4149
  case "turn.completed": {
@@ -4098,17 +4161,27 @@ var init_codex_provider = __esm({
4098
4161
  break;
4099
4162
  }
4100
4163
  case "turn.failed": {
4101
- const error = event.message;
4102
- controller.enqueue(sseEvent("error", error || "Turn failed"));
4164
+ const error = event.error?.message;
4165
+ self.clearCachedThreadId(params.sessionId);
4166
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Turn failed")));
4103
4167
  sawTerminalEvent = true;
4104
4168
  break;
4105
4169
  }
4106
4170
  case "error": {
4107
4171
  const error = event.message;
4108
- controller.enqueue(sseEvent("error", error || "Thread error"));
4172
+ self.clearCachedThreadId(params.sessionId);
4173
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Thread error")));
4109
4174
  sawTerminalEvent = true;
4110
4175
  break;
4111
4176
  }
4177
+ default: {
4178
+ const exhaustiveEvent = event;
4179
+ console.warn(
4180
+ "[codex-provider] Unhandled thread event:",
4181
+ stringifyUnknown(exhaustiveEvent)
4182
+ );
4183
+ break;
4184
+ }
4112
4185
  }
4113
4186
  if (sawTerminalEvent) {
4114
4187
  break;
@@ -4119,10 +4192,12 @@ var init_codex_provider = __esm({
4119
4192
  const message = err instanceof Error ? err.message : String(err);
4120
4193
  if (savedThreadId && !retryFresh && !sawAnyEvent && shouldRetryFreshThread(message)) {
4121
4194
  console.warn("[codex-provider] Resume failed, retrying with a fresh thread:", message);
4195
+ self.clearCachedThreadId(params.sessionId);
4122
4196
  savedThreadId = void 0;
4123
4197
  retryFresh = true;
4124
4198
  continue;
4125
4199
  }
4200
+ self.clearCachedThreadId(params.sessionId);
4126
4201
  throw err;
4127
4202
  }
4128
4203
  }
@@ -4130,15 +4205,16 @@ var init_codex_provider = __esm({
4130
4205
  } catch (err) {
4131
4206
  const message = err instanceof Error ? err.message : String(err);
4132
4207
  console.error("[codex-provider] Error:", err instanceof Error ? err.stack || err.message : err);
4208
+ self.clearCachedThreadId(params.sessionId);
4133
4209
  try {
4134
- controller.enqueue(sseEvent("error", message));
4210
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
4135
4211
  controller.close();
4136
4212
  } catch {
4137
4213
  }
4138
4214
  } finally {
4139
4215
  for (const tmp of tempFiles) {
4140
4216
  try {
4141
- fs14.unlinkSync(tmp);
4217
+ fs15.unlinkSync(tmp);
4142
4218
  } catch {
4143
4219
  }
4144
4220
  }
@@ -4148,12 +4224,22 @@ var init_codex_provider = __esm({
4148
4224
  });
4149
4225
  }
4150
4226
  /**
4151
- * Map a completed Codex item to SSE events.
4227
+ * Map a Codex item event to SSE events.
4152
4228
  */
4153
- handleCompletedItem(controller, item) {
4229
+ handleItemEvent(controller, item, phase, sessionId, emittedToolStarts) {
4154
4230
  const itemType = item.type;
4231
+ const ensureToolUse = (toolId, name, input) => {
4232
+ if (emittedToolStarts.has(toolId)) return;
4233
+ emittedToolStarts.add(toolId);
4234
+ controller.enqueue(sseEvent("tool_use", {
4235
+ id: toolId,
4236
+ name,
4237
+ input
4238
+ }));
4239
+ };
4155
4240
  switch (itemType) {
4156
4241
  case "agent_message": {
4242
+ if (phase !== "completed") break;
4157
4243
  const text2 = item.text || "";
4158
4244
  if (text2) {
4159
4245
  controller.enqueue(sseEvent("text", text2));
@@ -4165,12 +4251,11 @@ var init_codex_provider = __esm({
4165
4251
  const command = item.command || "";
4166
4252
  const output = item.aggregated_output || "";
4167
4253
  const exitCode = item.exit_code;
4254
+ const status = item.status;
4168
4255
  const isError = exitCode != null && exitCode !== 0;
4169
- controller.enqueue(sseEvent("tool_use", {
4170
- id: toolId,
4171
- name: "Bash",
4172
- input: { command }
4173
- }));
4256
+ const terminal = phase === "completed" || status === "completed" || status === "failed";
4257
+ ensureToolUse(toolId, "Bash", { command });
4258
+ if (!terminal) break;
4174
4259
  const resultContent = output || (isError ? `Exit code: ${exitCode}` : "Done");
4175
4260
  controller.enqueue(sseEvent("tool_result", {
4176
4261
  tool_use_id: toolId,
@@ -4180,14 +4265,11 @@ var init_codex_provider = __esm({
4180
4265
  break;
4181
4266
  }
4182
4267
  case "file_change": {
4268
+ if (phase !== "completed") break;
4183
4269
  const toolId = item.id || `tool-${Date.now()}`;
4184
4270
  const changes = item.changes || [];
4185
4271
  const summary = changes.map((c) => `${c.kind}: ${c.path}`).join("\n");
4186
- controller.enqueue(sseEvent("tool_use", {
4187
- id: toolId,
4188
- name: "Edit",
4189
- input: { files: changes }
4190
- }));
4272
+ ensureToolUse(toolId, "Edit", { files: changes });
4191
4273
  controller.enqueue(sseEvent("tool_result", {
4192
4274
  tool_use_id: toolId,
4193
4275
  content: summary || "File changes applied",
@@ -4202,13 +4284,11 @@ var init_codex_provider = __esm({
4202
4284
  const args = item.arguments;
4203
4285
  const result = item.result;
4204
4286
  const error = item.error;
4205
- const resultContent = result?.content ?? result?.structured_content;
4206
- const resultText = typeof resultContent === "string" ? resultContent : resultContent ? JSON.stringify(resultContent) : void 0;
4207
- controller.enqueue(sseEvent("tool_use", {
4208
- id: toolId,
4209
- name: `mcp__${server}__${tool}`,
4210
- input: args
4211
- }));
4287
+ const status = item.status;
4288
+ const terminal = phase === "completed" || status === "completed" || status === "failed";
4289
+ const resultText = extractMcpContentText(result?.content) || stringifyUnknown(result?.structured_content) || stringifyUnknown(result?.content);
4290
+ ensureToolUse(toolId, `mcp__${server}__${tool}`, args);
4291
+ if (!terminal) break;
4212
4292
  controller.enqueue(sseEvent("tool_result", {
4213
4293
  tool_use_id: toolId,
4214
4294
  content: error?.message || resultText || "Done",
@@ -4216,6 +4296,18 @@ var init_codex_provider = __esm({
4216
4296
  }));
4217
4297
  break;
4218
4298
  }
4299
+ case "web_search": {
4300
+ const toolId = item.id || `tool-${Date.now()}`;
4301
+ const query2 = item.query || "";
4302
+ ensureToolUse(toolId, "Web Search", { query: query2 });
4303
+ if (phase !== "completed") break;
4304
+ controller.enqueue(sseEvent("tool_result", {
4305
+ tool_use_id: toolId,
4306
+ content: query2 || "Search completed",
4307
+ is_error: false
4308
+ }));
4309
+ break;
4310
+ }
4219
4311
  case "reasoning": {
4220
4312
  const text2 = item.text || "";
4221
4313
  if (text2) {
@@ -4223,15 +4315,41 @@ var init_codex_provider = __esm({
4223
4315
  }
4224
4316
  break;
4225
4317
  }
4318
+ case "todo_list": {
4319
+ const tasks = mapTodoListItems(item.items);
4320
+ controller.enqueue(sseEvent("task_update", {
4321
+ session_id: sessionId,
4322
+ sdk_session_id: this.threadIds.get(sessionId) || void 0,
4323
+ tasks,
4324
+ todos: tasks
4325
+ }));
4326
+ break;
4327
+ }
4328
+ case "error": {
4329
+ this.clearCachedThreadId(sessionId);
4330
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(item.message || "Codex error")));
4331
+ break;
4332
+ }
4333
+ default: {
4334
+ const exhaustiveItem = item;
4335
+ console.warn(
4336
+ "[codex-provider] Unhandled thread item:",
4337
+ stringifyUnknown(exhaustiveItem)
4338
+ );
4339
+ break;
4340
+ }
4226
4341
  }
4227
4342
  }
4343
+ handleCompletedItem(controller, item) {
4344
+ this.handleItemEvent(controller, item, "completed", "test-session", /* @__PURE__ */ new Set());
4345
+ }
4228
4346
  };
4229
4347
  }
4230
4348
  });
4231
4349
 
4232
4350
  // src/main.ts
4233
- import fs15 from "node:fs";
4234
- import path16 from "node:path";
4351
+ import fs16 from "node:fs";
4352
+ import path17 from "node:path";
4235
4353
  import crypto10 from "node:crypto";
4236
4354
 
4237
4355
  // src/lib/bridge/context.ts
@@ -5515,6 +5633,14 @@ function buildToolProgressMarkdown(tools) {
5515
5633
  });
5516
5634
  return lines.join("\n");
5517
5635
  }
5636
+ function buildTaskProgressMarkdown(tasks) {
5637
+ if (tasks.length === 0) return "";
5638
+ return tasks.map((task) => {
5639
+ const icon = task.status === "completed" ? "\u2705" : task.status === "in_progress" ? "\u{1F504}" : "\u23F3";
5640
+ const label = task.status === "completed" ? "\u5DF2\u5B8C\u6210" : task.status === "in_progress" ? "\u6267\u884C\u4E2D" : "\u7B49\u5F85\u4E2D";
5641
+ return `${icon} ${task.text}\uFF08${label}\uFF09`;
5642
+ }).join("\n");
5643
+ }
5518
5644
  function formatElapsed(ms) {
5519
5645
  if (ms < 1e3) return `${ms}ms`;
5520
5646
  const sec = ms / 1e3;
@@ -5529,9 +5655,13 @@ function buildStreamingTextContent(text2) {
5529
5655
  function buildStreamingToolsContent(tools) {
5530
5656
  return buildToolProgressMarkdown(tools);
5531
5657
  }
5532
- function buildFinalCardJson(text2, tools, footer) {
5658
+ function buildStreamingTaskContent(tasks) {
5659
+ return buildTaskProgressMarkdown(tasks);
5660
+ }
5661
+ function buildFinalCardJson(text2, tasks, tools, footer) {
5533
5662
  const elements = [];
5534
5663
  const content = preprocessFeishuMarkdown(text2);
5664
+ const taskMd = buildTaskProgressMarkdown(tasks);
5535
5665
  const toolMd = buildToolProgressMarkdown(tools);
5536
5666
  if (content) {
5537
5667
  elements.push({
@@ -5541,6 +5671,17 @@ function buildFinalCardJson(text2, tools, footer) {
5541
5671
  text_size: "normal"
5542
5672
  });
5543
5673
  }
5674
+ if (taskMd) {
5675
+ if (elements.length > 0) {
5676
+ elements.push({ tag: "hr" });
5677
+ }
5678
+ elements.push({
5679
+ tag: "markdown",
5680
+ content: taskMd,
5681
+ text_align: "left",
5682
+ text_size: "normal"
5683
+ });
5684
+ }
5544
5685
  if (toolMd) {
5545
5686
  if (elements.length > 0) {
5546
5687
  elements.push({ tag: "hr" });
@@ -5626,7 +5767,9 @@ var DEDUP_MAX = 1e3;
5626
5767
  var MAX_FILE_SIZE = 20 * 1024 * 1024;
5627
5768
  var TYPING_EMOJI = "Typing";
5628
5769
  var CARD_THROTTLE_MS = 200;
5770
+ var CARD_REQUEST_TIMEOUT_MS = 15e3;
5629
5771
  var INITIAL_STREAMING_STATUS = "\u5904\u7406\u4E2D";
5772
+ var EMPTY_STREAMING_TASKS = "";
5630
5773
  var EMPTY_STREAMING_TOOLS = "";
5631
5774
  var MIME_BY_TYPE = {
5632
5775
  image: "image/png",
@@ -5657,6 +5800,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5657
5800
  cardCreatePromises = /* @__PURE__ */ new Map();
5658
5801
  /** Cached tenant token for upload APIs. */
5659
5802
  tenantTokenCache = null;
5803
+ cardRequestTimeoutMs = CARD_REQUEST_TIMEOUT_MS;
5660
5804
  constructor(instance) {
5661
5805
  super();
5662
5806
  this.channelType = instance?.id || "feishu";
@@ -5884,6 +6028,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5884
6028
  text_size: "normal",
5885
6029
  element_id: "streaming_content"
5886
6030
  },
6031
+ {
6032
+ tag: "markdown",
6033
+ content: EMPTY_STREAMING_TASKS,
6034
+ text_align: "left",
6035
+ text_size: "normal",
6036
+ element_id: "streaming_tasks"
6037
+ },
5887
6038
  {
5888
6039
  tag: "markdown",
5889
6040
  content: EMPTY_STREAMING_TOOLS,
@@ -5901,9 +6052,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5901
6052
  ]
5902
6053
  }
5903
6054
  };
5904
- const createResp = await cardkit.card.create({
6055
+ const createResp = await this.withFeishuRequestTimeout(cardKey, "card.create", () => cardkit.card.create({
5905
6056
  data: { type: "card_json", data: JSON.stringify(cardBody) }
5906
- });
6057
+ }));
5907
6058
  const cardId = createResp?.data?.card_id;
5908
6059
  if (!cardId) {
5909
6060
  console.warn("[feishu-adapter] Card create returned no card_id");
@@ -5912,19 +6063,19 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5912
6063
  const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } });
5913
6064
  let msgResp;
5914
6065
  if (replyToMessageId) {
5915
- msgResp = await this.restClient.im.message.reply({
6066
+ msgResp = await this.withFeishuRequestTimeout(cardKey, "im.message.reply:interactive", () => this.restClient.im.message.reply({
5916
6067
  path: { message_id: replyToMessageId },
5917
6068
  data: { content: cardContent, msg_type: "interactive" }
5918
- });
6069
+ }));
5919
6070
  } else {
5920
- msgResp = await this.restClient.im.message.create({
6071
+ msgResp = await this.withFeishuRequestTimeout(cardKey, "im.message.create:interactive", () => this.restClient.im.message.create({
5921
6072
  params: { receive_id_type: "chat_id" },
5922
6073
  data: {
5923
6074
  receive_id: chatId,
5924
6075
  msg_type: "interactive",
5925
6076
  content: cardContent
5926
6077
  }
5927
- });
6078
+ }));
5928
6079
  }
5929
6080
  const messageId = msgResp?.data?.message_id;
5930
6081
  if (!messageId) {
@@ -5937,17 +6088,25 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5937
6088
  messageId,
5938
6089
  sequence: 0,
5939
6090
  startTime: Date.now(),
6091
+ taskItems: [],
5940
6092
  toolCalls: [],
5941
6093
  thinking: true,
5942
6094
  pendingText: null,
6095
+ pendingTasksText: EMPTY_STREAMING_TASKS,
5943
6096
  pendingStatusText: INITIAL_STREAMING_STATUS,
5944
6097
  renderedText: "\u{1F4AD} Thinking...",
6098
+ renderedTasksText: EMPTY_STREAMING_TASKS,
5945
6099
  renderedToolsText: EMPTY_STREAMING_TOOLS,
5946
6100
  renderedStatusText: INITIAL_STREAMING_STATUS,
5947
6101
  lastUpdateAt: 0,
5948
6102
  throttleTimer: null,
5949
6103
  flushInFlight: null,
5950
- flushQueued: false
6104
+ flushQueued: false,
6105
+ lastFlushStartedAt: null,
6106
+ lastSuccessfulFlushAt: null,
6107
+ lastFlushErrorAt: null,
6108
+ lastFlushError: null,
6109
+ consecutiveFlushFailures: 0
5951
6110
  });
5952
6111
  console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
5953
6112
  return true;
@@ -5976,6 +6135,14 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5976
6135
  state.pendingStatusText = statusText || INITIAL_STREAMING_STATUS;
5977
6136
  this.scheduleCardFlush(cardKey);
5978
6137
  }
6138
+ updateTaskProgress(chatId, tasks, streamKey) {
6139
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6140
+ const state = this.activeCards.get(cardKey);
6141
+ if (!state) return;
6142
+ state.taskItems = tasks;
6143
+ state.pendingTasksText = buildStreamingTaskContent(tasks) || EMPTY_STREAMING_TASKS;
6144
+ this.scheduleCardFlush(cardKey);
6145
+ }
5979
6146
  enqueueCardFlush(streamKey) {
5980
6147
  const state = this.activeCards.get(streamKey);
5981
6148
  if (!state) return;
@@ -5983,6 +6150,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5983
6150
  state.flushQueued = true;
5984
6151
  return;
5985
6152
  }
6153
+ state.lastFlushStartedAt = Date.now();
5986
6154
  state.flushInFlight = this.flushCardUpdate(streamKey).catch((err) => {
5987
6155
  console.warn("[feishu-adapter] cardElement.content failed:", err instanceof Error ? err.message : err);
5988
6156
  }).finally(() => {
@@ -6023,6 +6191,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6023
6191
  const cardkit = this.restClient.cardkit?.v1;
6024
6192
  if (!cardkit?.cardElement?.content) return;
6025
6193
  const content = buildStreamingTextContent(state.pendingText || "");
6194
+ const tasksText = state.pendingTasksText || EMPTY_STREAMING_TASKS;
6026
6195
  const toolsText = buildStreamingToolsContent(state.toolCalls) || EMPTY_STREAMING_TOOLS;
6027
6196
  const statusText = state.pendingStatusText || INITIAL_STREAMING_STATUS;
6028
6197
  const updates = [];
@@ -6035,6 +6204,15 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6035
6204
  }
6036
6205
  });
6037
6206
  }
6207
+ if (tasksText !== state.renderedTasksText) {
6208
+ updates.push({
6209
+ elementId: "streaming_tasks",
6210
+ content: tasksText,
6211
+ onSuccess: () => {
6212
+ state.renderedTasksText = tasksText;
6213
+ }
6214
+ });
6215
+ }
6038
6216
  if (toolsText !== state.renderedToolsText) {
6039
6217
  updates.push({
6040
6218
  elementId: "streaming_tools",
@@ -6058,13 +6236,14 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6058
6236
  for (const update of updates) {
6059
6237
  state.sequence++;
6060
6238
  try {
6061
- await cardkit.cardElement.content({
6239
+ await this.withFeishuRequestTimeout(streamKey, `cardElement.content:${update.elementId}`, () => cardkit.cardElement.content({
6062
6240
  path: { card_id: cardId, element_id: update.elementId },
6063
6241
  data: { content: update.content, sequence: state.sequence }
6064
- });
6242
+ }));
6065
6243
  update.onSuccess();
6066
- state.lastUpdateAt = Date.now();
6244
+ this.markCardFlushSuccess(state);
6067
6245
  } catch (err) {
6246
+ this.markCardFlushFailure(state, err);
6068
6247
  console.warn(
6069
6248
  `[feishu-adapter] cardElement.content failed for ${update.elementId}:`,
6070
6249
  err instanceof Error ? err.message : err
@@ -6125,13 +6304,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6125
6304
  await this.awaitCardFlushCompletion(cardKey);
6126
6305
  try {
6127
6306
  state.sequence++;
6128
- await cardkit.card.settings({
6307
+ await this.withFeishuRequestTimeout(cardKey, "card.settings", () => cardkit.card.settings({
6129
6308
  path: { card_id: state.cardId },
6130
6309
  data: {
6131
6310
  settings: JSON.stringify({ streaming_mode: false }),
6132
6311
  sequence: state.sequence
6133
6312
  }
6134
- });
6313
+ }));
6135
6314
  const statusLabels = {
6136
6315
  completed: "\u2705 Completed",
6137
6316
  interrupted: "\u26A0\uFE0F Interrupted",
@@ -6151,15 +6330,15 @@ var FeishuAdapter = class extends BaseChannelAdapter {
6151
6330
 
6152
6331
  ${trimmedResponse}`;
6153
6332
  }
6154
- const finalCardJson = buildFinalCardJson(finalText, state.toolCalls, footer);
6333
+ const finalCardJson = buildFinalCardJson(finalText, state.taskItems, state.toolCalls, footer);
6155
6334
  state.sequence++;
6156
- await cardkit.card.update({
6335
+ await this.withFeishuRequestTimeout(cardKey, "card.update", () => cardkit.card.update({
6157
6336
  path: { card_id: state.cardId },
6158
6337
  data: {
6159
6338
  card: { type: "card_json", data: finalCardJson },
6160
6339
  sequence: state.sequence
6161
6340
  }
6162
- });
6341
+ }));
6163
6342
  console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${status}, elapsed=${formatElapsed(elapsedMs)}`);
6164
6343
  return true;
6165
6344
  } catch (err) {
@@ -6191,6 +6370,76 @@ ${trimmedResponse}`;
6191
6370
  hasActiveStreamingUi(chatId, streamKey) {
6192
6371
  return this.hasActiveCard(chatId, streamKey);
6193
6372
  }
6373
+ getStructuredStreamingUiSnapshot(chatId, streamKey) {
6374
+ const state = this.activeCards.get(this.resolveStreamKey(chatId, streamKey));
6375
+ if (!state) return null;
6376
+ return {
6377
+ active: true,
6378
+ lastAttemptAt: state.lastFlushStartedAt,
6379
+ lastUpdateAt: state.lastSuccessfulFlushAt ?? (state.lastUpdateAt > 0 ? state.lastUpdateAt : null),
6380
+ lastErrorAt: state.lastFlushErrorAt,
6381
+ lastError: state.lastFlushError,
6382
+ flushInFlight: Boolean(state.flushInFlight),
6383
+ flushInFlightSince: state.flushInFlight ? state.lastFlushStartedAt : null,
6384
+ consecutiveFailures: state.consecutiveFlushFailures
6385
+ };
6386
+ }
6387
+ getCardRequestTimeoutMs() {
6388
+ return Math.max(1, this.cardRequestTimeoutMs);
6389
+ }
6390
+ logRequestOperation(phase, scope, target, startedAt, detail) {
6391
+ const durationMs = Math.max(0, Date.now() - startedAt);
6392
+ const suffix = detail ? `, detail=${detail}` : "";
6393
+ const line = `[feishu-adapter] Request ${phase}: scope=${scope}, target=${target}, duration=${durationMs}ms${suffix}`;
6394
+ if (phase === "start" || phase === "success") {
6395
+ console.log(line);
6396
+ return;
6397
+ }
6398
+ console.warn(line);
6399
+ }
6400
+ async withFeishuRequestTimeout(scope, target, operation) {
6401
+ const startedAt = Date.now();
6402
+ const timeoutMs = this.getCardRequestTimeoutMs();
6403
+ this.logRequestOperation("start", scope, target, startedAt);
6404
+ let timeoutHandle = null;
6405
+ const timeoutPromise = new Promise((_, reject) => {
6406
+ timeoutHandle = setTimeout(() => {
6407
+ reject(new Error(`timeout after ${timeoutMs}ms`));
6408
+ }, timeoutMs);
6409
+ });
6410
+ const operationPromise = operation();
6411
+ operationPromise.catch(() => {
6412
+ });
6413
+ try {
6414
+ const result = await Promise.race([operationPromise, timeoutPromise]);
6415
+ this.logRequestOperation("success", scope, target, startedAt);
6416
+ return result;
6417
+ } catch (error) {
6418
+ const detail = error instanceof Error ? error.message : String(error);
6419
+ this.logRequestOperation(
6420
+ detail.startsWith("timeout after ") ? "timeout" : "error",
6421
+ scope,
6422
+ target,
6423
+ startedAt,
6424
+ detail
6425
+ );
6426
+ throw error;
6427
+ } finally {
6428
+ if (timeoutHandle) clearTimeout(timeoutHandle);
6429
+ }
6430
+ }
6431
+ markCardFlushFailure(state, error) {
6432
+ state.lastFlushErrorAt = Date.now();
6433
+ state.lastFlushError = error instanceof Error ? error.message : String(error);
6434
+ state.consecutiveFlushFailures += 1;
6435
+ }
6436
+ markCardFlushSuccess(state) {
6437
+ const now2 = Date.now();
6438
+ state.lastUpdateAt = now2;
6439
+ state.lastSuccessfulFlushAt = now2;
6440
+ state.lastFlushError = null;
6441
+ state.consecutiveFlushFailures = 0;
6442
+ }
6194
6443
  // ── Streaming adapter interface ────────────────────────────────
6195
6444
  /**
6196
6445
  * Called by bridge-manager on each text SSE event.
@@ -6218,8 +6467,30 @@ ${trimmedResponse}`;
6218
6467
  }
6219
6468
  onToolEvent(chatId, tools, streamKey) {
6220
6469
  if (!this.isStreamingEnabled()) return;
6470
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6471
+ if (!this.activeCards.has(cardKey)) {
6472
+ const messageId = this.lastIncomingMessageId.get(chatId);
6473
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
6474
+ if (ok) this.updateToolProgress(chatId, tools, cardKey);
6475
+ }).catch(() => {
6476
+ });
6477
+ return;
6478
+ }
6221
6479
  this.updateToolProgress(chatId, tools, streamKey);
6222
6480
  }
6481
+ onTaskEvent(chatId, tasks, streamKey) {
6482
+ if (!this.isStreamingEnabled()) return;
6483
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6484
+ if (!this.activeCards.has(cardKey)) {
6485
+ const messageId = this.lastIncomingMessageId.get(chatId);
6486
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
6487
+ if (ok) this.updateTaskProgress(chatId, tasks, cardKey);
6488
+ }).catch(() => {
6489
+ });
6490
+ return;
6491
+ }
6492
+ this.updateTaskProgress(chatId, tasks, streamKey);
6493
+ }
6223
6494
  onStreamStatus(chatId, statusText, streamKey) {
6224
6495
  if (!this.isStreamingEnabled()) return;
6225
6496
  const cardKey = this.resolveStreamKey(chatId, streamKey);
@@ -6369,17 +6640,17 @@ ${trimmedResponse}`;
6369
6640
  }
6370
6641
  async sendStructuredMessage(chatId, msgType, content, replyToMessageId) {
6371
6642
  try {
6372
- const res = replyToMessageId ? await this.restClient.im.message.reply({
6643
+ const res = replyToMessageId ? await this.withFeishuRequestTimeout(chatId, `im.message.reply:${msgType}`, () => this.restClient.im.message.reply({
6373
6644
  path: { message_id: replyToMessageId },
6374
6645
  data: { msg_type: msgType, content }
6375
- }) : await this.restClient.im.message.create({
6646
+ })) : await this.withFeishuRequestTimeout(chatId, `im.message.create:${msgType}`, () => this.restClient.im.message.create({
6376
6647
  params: { receive_id_type: "chat_id" },
6377
6648
  data: {
6378
6649
  receive_id: chatId,
6379
6650
  msg_type: msgType,
6380
6651
  content
6381
6652
  }
6382
- });
6653
+ }));
6383
6654
  if (res?.data?.message_id) {
6384
6655
  return { ok: true, messageId: res.data.message_id };
6385
6656
  }
@@ -6395,14 +6666,14 @@ ${trimmedResponse}`;
6395
6666
  async sendAsCard(chatId, text2) {
6396
6667
  const cardContent = buildCardContent(text2);
6397
6668
  try {
6398
- const res = await this.restClient.im.message.create({
6669
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:interactive-card", () => this.restClient.im.message.create({
6399
6670
  params: { receive_id_type: "chat_id" },
6400
6671
  data: {
6401
6672
  receive_id: chatId,
6402
6673
  msg_type: "interactive",
6403
6674
  content: cardContent
6404
6675
  }
6405
- });
6676
+ }));
6406
6677
  if (res?.data?.message_id) {
6407
6678
  return { ok: true, messageId: res.data.message_id };
6408
6679
  }
@@ -6419,14 +6690,14 @@ ${trimmedResponse}`;
6419
6690
  async sendAsPost(chatId, text2) {
6420
6691
  const postContent = buildPostContent(text2);
6421
6692
  try {
6422
- const res = await this.restClient.im.message.create({
6693
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:post", () => this.restClient.im.message.create({
6423
6694
  params: { receive_id_type: "chat_id" },
6424
6695
  data: {
6425
6696
  receive_id: chatId,
6426
6697
  msg_type: "post",
6427
6698
  content: postContent
6428
6699
  }
6429
- });
6700
+ }));
6430
6701
  if (res?.data?.message_id) {
6431
6702
  return { ok: true, messageId: res.data.message_id };
6432
6703
  }
@@ -6438,14 +6709,14 @@ ${trimmedResponse}`;
6438
6709
  }
6439
6710
  async sendAsPlainText(chatId, text2) {
6440
6711
  try {
6441
- const res = await this.restClient.im.message.create({
6712
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:text", () => this.restClient.im.message.create({
6442
6713
  params: { receive_id_type: "chat_id" },
6443
6714
  data: {
6444
6715
  receive_id: chatId,
6445
6716
  msg_type: "text",
6446
6717
  content: JSON.stringify({ text: text2 })
6447
6718
  }
6448
- });
6719
+ }));
6449
6720
  if (res?.data?.message_id) {
6450
6721
  return { ok: true, messageId: res.data.message_id };
6451
6722
  }
@@ -6470,14 +6741,14 @@ ${trimmedResponse}`;
6470
6741
  if (permId) {
6471
6742
  const cardJson2 = buildPermissionButtonCard(mdText, permId, chatId);
6472
6743
  try {
6473
- const res = await this.restClient.im.message.create({
6744
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-button-card", () => this.restClient.im.message.create({
6474
6745
  params: { receive_id_type: "chat_id" },
6475
6746
  data: {
6476
6747
  receive_id: chatId,
6477
6748
  msg_type: "interactive",
6478
6749
  content: cardJson2
6479
6750
  }
6480
- });
6751
+ }));
6481
6752
  if (res?.data?.message_id) {
6482
6753
  return { ok: true, messageId: res.data.message_id };
6483
6754
  }
@@ -6521,14 +6792,14 @@ ${trimmedResponse}`;
6521
6792
  }
6522
6793
  });
6523
6794
  try {
6524
- const res = await this.restClient.im.message.create({
6795
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-fallback-card", () => this.restClient.im.message.create({
6525
6796
  params: { receive_id_type: "chat_id" },
6526
6797
  data: {
6527
6798
  receive_id: chatId,
6528
6799
  msg_type: "interactive",
6529
6800
  content: cardJson
6530
6801
  }
6531
- });
6802
+ }));
6532
6803
  if (res?.data?.message_id) {
6533
6804
  return { ok: true, messageId: res.data.message_id };
6534
6805
  }
@@ -6545,14 +6816,14 @@ ${trimmedResponse}`;
6545
6816
  ...permCommands
6546
6817
  ].join("\n");
6547
6818
  try {
6548
- const res = await this.restClient.im.message.create({
6819
+ const res = await this.withFeishuRequestTimeout(chatId, "im.message.create:permission-fallback-text", () => this.restClient.im.message.create({
6549
6820
  params: { receive_id_type: "chat_id" },
6550
6821
  data: {
6551
6822
  receive_id: chatId,
6552
6823
  msg_type: "text",
6553
6824
  content: JSON.stringify({ text: plainText })
6554
6825
  }
6555
- });
6826
+ }));
6556
6827
  if (res?.data?.message_id) {
6557
6828
  return { ok: true, messageId: res.data.message_id };
6558
6829
  }
@@ -6898,20 +7169,20 @@ ${trimmedResponse}`;
6898
7169
  buffer = Buffer.concat(chunks);
6899
7170
  } catch (streamErr) {
6900
7171
  console.warn("[feishu-adapter] Stream read failed, falling back to writeFile:", streamErr instanceof Error ? streamErr.message : streamErr);
6901
- const fs16 = await import("fs");
7172
+ const fs17 = await import("fs");
6902
7173
  const os5 = await import("os");
6903
- const path17 = await import("path");
6904
- const tmpPath = path17.join(os5.tmpdir(), `feishu-dl-${crypto2.randomUUID()}`);
7174
+ const path18 = await import("path");
7175
+ const tmpPath = path18.join(os5.tmpdir(), `feishu-dl-${crypto2.randomUUID()}`);
6905
7176
  try {
6906
7177
  await res.writeFile(tmpPath);
6907
- buffer = fs16.readFileSync(tmpPath);
7178
+ buffer = fs17.readFileSync(tmpPath);
6908
7179
  if (buffer.length > MAX_FILE_SIZE) {
6909
7180
  console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`);
6910
7181
  return null;
6911
7182
  }
6912
7183
  } finally {
6913
7184
  try {
6914
- fs16.unlinkSync(tmpPath);
7185
+ fs17.unlinkSync(tmpPath);
6915
7186
  } catch {
6916
7187
  }
6917
7188
  }
@@ -15028,6 +15299,54 @@ function extractNormalizedStructuredText(value) {
15028
15299
  collectStructuredTextParts(value, parts);
15029
15300
  return parts.length > 0 ? normalizeStructuredText(parts.join("\n\n")) : "";
15030
15301
  }
15302
+ function parseJsonSafely(value) {
15303
+ if (!value) return null;
15304
+ try {
15305
+ return JSON.parse(value);
15306
+ } catch {
15307
+ return null;
15308
+ }
15309
+ }
15310
+ function normalizeTaskStatus(value) {
15311
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
15312
+ if (normalized === "in_progress" || normalized === "running" || normalized === "active") {
15313
+ return "in_progress";
15314
+ }
15315
+ if (normalized === "completed" || normalized === "complete" || normalized === "done") {
15316
+ return "completed";
15317
+ }
15318
+ return "pending";
15319
+ }
15320
+ function parseTaskProgressItems(value) {
15321
+ if (!Array.isArray(value)) return [];
15322
+ return value.map((item) => {
15323
+ const record = item;
15324
+ const text2 = extractNormalizedStructuredText(record.text ?? record.step);
15325
+ if (!text2) return null;
15326
+ return {
15327
+ text: text2,
15328
+ status: normalizeTaskStatus(record.status)
15329
+ };
15330
+ }).filter((item) => Boolean(item));
15331
+ }
15332
+ function parseUpdatePlanTasks(argumentsJson) {
15333
+ const parsed = parseJsonSafely(argumentsJson);
15334
+ if (!parsed || typeof parsed !== "object") return [];
15335
+ return parseTaskProgressItems(parsed.plan ?? parsed.tasks);
15336
+ }
15337
+ function extractToolOutputText(value) {
15338
+ if (typeof value !== "string") return extractNormalizedFreeText(value);
15339
+ const trimmed = value.trim();
15340
+ if (!trimmed) return "";
15341
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15342
+ const parsed = parseJsonSafely(trimmed);
15343
+ if (parsed && typeof parsed === "object") {
15344
+ const extracted = extractNormalizedFreeText(parsed.output ?? parsed);
15345
+ if (extracted) return extracted;
15346
+ }
15347
+ }
15348
+ return extractNormalizedFreeText(value);
15349
+ }
15031
15350
  function createDesktopEventSignature(rawLine) {
15032
15351
  return crypto7.createHash("sha1").update(rawLine).digest("hex");
15033
15352
  }
@@ -15059,6 +15378,17 @@ function isSessionMessageLine(line) {
15059
15378
  function isTurnContextLine(line) {
15060
15379
  return line.type === "turn_context";
15061
15380
  }
15381
+ function describeUnhandledMirrorLineKind(line) {
15382
+ if (isSessionEventLine(line)) {
15383
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
15384
+ return `event_msg:${payloadType || "<unknown>"}`;
15385
+ }
15386
+ if (isSessionMessageLine(line)) {
15387
+ const payloadType = typeof line.payload?.type === "string" ? line.payload.type.trim() : "";
15388
+ return `response_item:${payloadType || "<unknown>"}`;
15389
+ }
15390
+ return null;
15391
+ }
15062
15392
  function listDesktopSessions(limit) {
15063
15393
  const root = getCodexSessionsRoot();
15064
15394
  if (!fs4.existsSync(root)) return [];
@@ -15151,81 +15481,213 @@ function pushDesktopSessionEvent(events, parsed, rawLine) {
15151
15481
  });
15152
15482
  }
15153
15483
  }
15154
- function pushDesktopMirrorRecord(records, parsed, rawLine, activeTurnId) {
15155
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_started") {
15484
+ function pushDesktopMirrorRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
15485
+ if (isSessionEventLine(parsed)) {
15486
+ return pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId);
15487
+ }
15488
+ if (isSessionMessageLine(parsed)) {
15489
+ return pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds);
15490
+ }
15491
+ return false;
15492
+ }
15493
+ function pushDesktopMirrorEventRecord(records, parsed, rawLine, activeTurnId) {
15494
+ const signature = createDesktopEventSignature(rawLine);
15495
+ const timestamp = parsed.timestamp || "";
15496
+ if (parsed.payload?.type === "task_started") {
15156
15497
  records.push({
15157
- signature: createDesktopEventSignature(rawLine),
15498
+ signature,
15158
15499
  type: "task_started",
15159
15500
  content: "",
15160
- timestamp: parsed.timestamp || "",
15501
+ timestamp,
15161
15502
  turnId: parsed.payload.turn_id || ""
15162
15503
  });
15163
- return;
15504
+ return true;
15164
15505
  }
15165
- if (isSessionEventLine(parsed) && parsed.payload?.type === "user_message") {
15506
+ if (parsed.payload?.type === "turn_aborted") {
15507
+ records.push({
15508
+ signature,
15509
+ type: "task_aborted",
15510
+ content: extractNormalizedStructuredText(parsed.payload.reason),
15511
+ timestamp,
15512
+ ...activeTurnId ? { turnId: activeTurnId } : {}
15513
+ });
15514
+ return true;
15515
+ }
15516
+ if (parsed.payload?.type === "agent_reasoning") {
15517
+ const text2 = extractNormalizedStructuredText(parsed.payload.text);
15518
+ if (!text2) return true;
15519
+ records.push({
15520
+ signature,
15521
+ type: "reasoning",
15522
+ content: text2,
15523
+ timestamp,
15524
+ ...activeTurnId ? { turnId: activeTurnId } : {}
15525
+ });
15526
+ return true;
15527
+ }
15528
+ if (parsed.payload?.type === "web_search_end") {
15529
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15530
+ records.push({
15531
+ signature,
15532
+ type: "tool_finished",
15533
+ content: extractNormalizedStructuredText(parsed.payload.query),
15534
+ timestamp,
15535
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15536
+ toolId,
15537
+ toolName: "Web Search"
15538
+ });
15539
+ return true;
15540
+ }
15541
+ if (parsed.payload?.type === "mcp_tool_call_end") {
15542
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15543
+ const server = extractNormalizedFreeText(parsed.payload.invocation?.server);
15544
+ const tool = extractNormalizedFreeText(parsed.payload.invocation?.tool);
15545
+ const toolName = server && tool ? `mcp__${server}__${tool}` : "mcp_tool_call";
15546
+ records.push({
15547
+ signature,
15548
+ type: "tool_finished",
15549
+ content: "",
15550
+ timestamp,
15551
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15552
+ toolId,
15553
+ toolName,
15554
+ isError: false
15555
+ });
15556
+ return true;
15557
+ }
15558
+ if (parsed.payload?.type === "user_message") {
15166
15559
  const text2 = extractNormalizedStructuredText(parsed.payload.message);
15167
- if (!text2) return;
15560
+ if (!text2) return true;
15168
15561
  records.push({
15169
- signature: createDesktopEventSignature(rawLine),
15562
+ signature,
15170
15563
  type: "message",
15171
15564
  role: "user",
15172
15565
  content: text2,
15173
- timestamp: parsed.timestamp || "",
15566
+ timestamp,
15174
15567
  ...activeTurnId ? { turnId: activeTurnId } : {}
15175
15568
  });
15176
- return;
15569
+ return true;
15177
15570
  }
15178
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_complete") {
15571
+ if (parsed.payload?.type === "task_complete") {
15179
15572
  records.push({
15180
- signature: createDesktopEventSignature(rawLine),
15573
+ signature,
15181
15574
  type: "task_complete",
15182
15575
  role: "assistant",
15183
15576
  content: extractNormalizedStructuredText(parsed.payload.last_agent_message),
15184
- timestamp: parsed.timestamp || "",
15577
+ timestamp,
15185
15578
  turnId: parsed.payload.turn_id || ""
15186
15579
  });
15187
- return;
15580
+ return true;
15188
15581
  }
15189
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
15582
+ return false;
15583
+ }
15584
+ function pushDesktopMirrorResponseRecord(records, parsed, rawLine, activeTurnId, activeSpecialCallIds) {
15585
+ const signature = createDesktopEventSignature(rawLine);
15586
+ const timestamp = parsed.timestamp || "";
15587
+ if (parsed.payload?.type === "message" && parsed.payload.role === "assistant") {
15190
15588
  const text2 = extractDesktopMessageText(parsed);
15191
- if (!text2) return;
15589
+ if (!text2) return true;
15192
15590
  records.push({
15193
- signature: createDesktopEventSignature(rawLine),
15591
+ signature,
15194
15592
  type: "message",
15195
15593
  role: parsed.payload.phase === "commentary" ? "commentary" : "assistant",
15196
15594
  content: parsed.payload.phase === "commentary" ? text2.replace(/^\[commentary\]\n/, "") : text2,
15197
- timestamp: parsed.timestamp || "",
15595
+ timestamp,
15198
15596
  ...activeTurnId ? { turnId: activeTurnId } : {}
15199
15597
  });
15200
- return;
15598
+ return true;
15201
15599
  }
15202
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "function_call") {
15600
+ if (parsed.payload?.type === "function_call") {
15203
15601
  const toolName = extractNormalizedFreeText(parsed.payload.name);
15204
- const toolId = extractNormalizedFreeText(parsed.payload.call_id) || createDesktopEventSignature(rawLine);
15205
- if (!toolName) return;
15602
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15603
+ if (!toolName) return true;
15604
+ if (toolName === "update_plan") {
15605
+ const tasks = parseUpdatePlanTasks(parsed.payload.arguments);
15606
+ activeSpecialCallIds.add(toolId);
15607
+ records.push({
15608
+ signature,
15609
+ type: "plan_update",
15610
+ content: "",
15611
+ timestamp,
15612
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15613
+ tasks
15614
+ });
15615
+ return true;
15616
+ }
15206
15617
  records.push({
15207
- signature: createDesktopEventSignature(rawLine),
15618
+ signature,
15208
15619
  type: "tool_started",
15209
15620
  content: "",
15210
- timestamp: parsed.timestamp || "",
15621
+ timestamp,
15211
15622
  ...activeTurnId ? { turnId: activeTurnId } : {},
15212
15623
  toolId,
15213
15624
  toolName
15214
15625
  });
15215
- return;
15626
+ return true;
15216
15627
  }
15217
- if (isSessionMessageLine(parsed) && parsed.payload?.type === "function_call_output") {
15218
- const toolId = extractNormalizedFreeText(parsed.payload.call_id) || createDesktopEventSignature(rawLine);
15628
+ if (parsed.payload?.type === "custom_tool_call") {
15629
+ const toolName = extractNormalizedFreeText(parsed.payload.name);
15630
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15631
+ if (!toolName) return true;
15632
+ if (toolName === "update_plan") {
15633
+ const tasks = parseUpdatePlanTasks(typeof parsed.payload.input === "string" ? parsed.payload.input : void 0);
15634
+ activeSpecialCallIds.add(toolId);
15635
+ records.push({
15636
+ signature,
15637
+ type: "plan_update",
15638
+ content: "",
15639
+ timestamp,
15640
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15641
+ tasks
15642
+ });
15643
+ return true;
15644
+ }
15219
15645
  records.push({
15220
- signature: createDesktopEventSignature(rawLine),
15646
+ signature,
15647
+ type: "tool_started",
15648
+ content: "",
15649
+ timestamp,
15650
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15651
+ toolId,
15652
+ toolName
15653
+ });
15654
+ return true;
15655
+ }
15656
+ if (parsed.payload?.type === "function_call_output") {
15657
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15658
+ if (activeSpecialCallIds.has(toolId)) {
15659
+ activeSpecialCallIds.delete(toolId);
15660
+ return true;
15661
+ }
15662
+ records.push({
15663
+ signature,
15221
15664
  type: "tool_finished",
15222
- content: extractNormalizedFreeText(parsed.payload.output),
15223
- timestamp: parsed.timestamp || "",
15665
+ content: extractToolOutputText(parsed.payload.output),
15666
+ timestamp,
15224
15667
  ...activeTurnId ? { turnId: activeTurnId } : {},
15225
15668
  toolId,
15226
15669
  isError: parsed.payload.is_error === true
15227
15670
  });
15671
+ return true;
15228
15672
  }
15673
+ if (parsed.payload?.type === "custom_tool_call_output") {
15674
+ const toolId = extractNormalizedFreeText(parsed.payload.call_id) || signature;
15675
+ if (activeSpecialCallIds.has(toolId)) {
15676
+ activeSpecialCallIds.delete(toolId);
15677
+ return true;
15678
+ }
15679
+ records.push({
15680
+ signature,
15681
+ type: "tool_finished",
15682
+ content: extractToolOutputText(parsed.payload.output),
15683
+ timestamp,
15684
+ ...activeTurnId ? { turnId: activeTurnId } : {},
15685
+ toolId,
15686
+ isError: parsed.payload.is_error === true
15687
+ });
15688
+ return true;
15689
+ }
15690
+ return false;
15229
15691
  }
15230
15692
  function parseDesktopSessionEventText(content, leadingText = "", flushTrailingText = false) {
15231
15693
  const combined = `${leadingText}${content}`;
@@ -15261,14 +15723,16 @@ function parseDesktopSessionEventText(content, leadingText = "", flushTrailingTe
15261
15723
  trailingText
15262
15724
  };
15263
15725
  }
15264
- function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingText = false, initialTurnId = null) {
15726
+ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingText = false, initialTurnId = null, initialSpecialCallIds = []) {
15265
15727
  const combined = `${leadingText}${content}`;
15266
15728
  if (!combined) {
15267
15729
  return {
15268
15730
  records: [],
15269
15731
  nextOffset: 0,
15270
15732
  trailingText: "",
15271
- nextTurnId: initialTurnId
15733
+ nextTurnId: initialTurnId,
15734
+ nextSpecialCallIds: [],
15735
+ unknownKinds: []
15272
15736
  };
15273
15737
  }
15274
15738
  const hasTrailingNewline = combined.endsWith("\n") || combined.endsWith("\r");
@@ -15280,6 +15744,8 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
15280
15744
  }
15281
15745
  const records = [];
15282
15746
  let activeTurnId = initialTurnId;
15747
+ const activeSpecialCallIds = new Set(initialSpecialCallIds);
15748
+ const unknownKinds = /* @__PURE__ */ new Set();
15283
15749
  for (const line of rawLines) {
15284
15750
  const trimmed = line.trim();
15285
15751
  if (!trimmed) continue;
@@ -15297,20 +15763,27 @@ function parseDesktopMirrorRecordText(content, leadingText = "", flushTrailingTe
15297
15763
  const eventPayload = parsed.payload;
15298
15764
  activeTurnId = eventPayload?.turn_id || activeTurnId;
15299
15765
  }
15300
- pushDesktopMirrorRecord(records, parsed, trimmed, activeTurnId);
15301
- if (isSessionEventLine(parsed) && parsed.payload?.type === "task_complete") {
15766
+ const handled = pushDesktopMirrorRecord(records, parsed, trimmed, activeTurnId, activeSpecialCallIds);
15767
+ if (!handled) {
15768
+ const unknownKind = describeUnhandledMirrorLineKind(parsed);
15769
+ if (unknownKind) unknownKinds.add(unknownKind);
15770
+ }
15771
+ if (isSessionEventLine(parsed) && (parsed.payload?.type === "task_complete" || parsed.payload?.type === "turn_aborted")) {
15302
15772
  const eventPayload = parsed.payload;
15303
15773
  const completedTurnId = eventPayload?.turn_id || activeTurnId;
15304
15774
  if (!completedTurnId || completedTurnId === activeTurnId) {
15305
15775
  activeTurnId = null;
15306
15776
  }
15777
+ activeSpecialCallIds.clear();
15307
15778
  }
15308
15779
  }
15309
15780
  return {
15310
15781
  records,
15311
15782
  nextOffset: 0,
15312
15783
  trailingText,
15313
- nextTurnId: activeTurnId
15784
+ nextTurnId: activeTurnId,
15785
+ nextSpecialCallIds: Array.from(activeSpecialCallIds),
15786
+ unknownKinds: Array.from(unknownKinds)
15314
15787
  };
15315
15788
  }
15316
15789
  function readDesktopSessionMessages(threadId, limit = 8) {
@@ -15331,7 +15804,7 @@ function readDesktopSessionEventStreamByFilePath(filePath) {
15331
15804
  }
15332
15805
  return parseDesktopSessionEventText(content, "", true).events;
15333
15806
  }
15334
- function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null) {
15807
+ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
15335
15808
  let content = "";
15336
15809
  try {
15337
15810
  content = readFileUtf8Range(filePath, startOffset, endOffset);
@@ -15340,15 +15813,19 @@ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, en
15340
15813
  records: [],
15341
15814
  nextOffset: startOffset,
15342
15815
  trailingText,
15343
- nextTurnId: currentTurnId
15816
+ nextTurnId: currentTurnId,
15817
+ nextSpecialCallIds: Array.from(currentSpecialCallIds),
15818
+ unknownKinds: []
15344
15819
  };
15345
15820
  }
15346
- const parsed = parseDesktopMirrorRecordText(content, trailingText, false, currentTurnId);
15821
+ const parsed = parseDesktopMirrorRecordText(content, trailingText, false, currentTurnId, currentSpecialCallIds);
15347
15822
  return {
15348
15823
  records: parsed.records,
15349
15824
  nextOffset: Math.max(startOffset, endOffset),
15350
15825
  trailingText: parsed.trailingText,
15351
- nextTurnId: parsed.nextTurnId
15826
+ nextTurnId: parsed.nextTurnId,
15827
+ nextSpecialCallIds: parsed.nextSpecialCallIds,
15828
+ unknownKinds: parsed.unknownKinds
15352
15829
  };
15353
15830
  }
15354
15831
  function readDesktopSessionEventStream(threadId) {
@@ -16453,6 +16930,8 @@ function formatHealthStatusLabel(healthStatus) {
16453
16930
  return "\u957F\u65F6\u8FD0\u884C\uFF0C\u5F85\u89C2\u5BDF";
16454
16931
  case "suspected_stall":
16455
16932
  return "\u7591\u4F3C\u5361\u4F4F";
16933
+ case "suspected_stream_ui_stall":
16934
+ return "\u6D41\u5F0F UI \u7591\u4F3C\u5361\u4F4F";
16456
16935
  case "suspected_detached":
16457
16936
  return "\u7591\u4F3C\u8131\u6302";
16458
16937
  case "completed":
@@ -16507,8 +16986,10 @@ function buildHealthCommandResponse(title, diagnosis, markdown = false) {
16507
16986
  ["\u5065\u5EB7\u72B6\u6001", formatHealthStatusLabel(diagnosis.healthStatus)],
16508
16987
  ["\u5F53\u524D\u9636\u6BB5", currentStage],
16509
16988
  ["\u6700\u540E\u8FDB\u5C55", formatCommandTimestamp(diagnosis.lastProgressAt)],
16989
+ ["\u6D41\u5F0F UI \u5237\u65B0", formatCommandTimestamp(diagnosis.lastStreamUiUpdateAt)],
16510
16990
  ["\u5DE5\u5177\u5F00\u59CB", formatCommandTimestamp(diagnosis.activeToolStartedAt)],
16511
16991
  ["\u6700\u8FD1\u5DE5\u5177\u5B8C\u6210", formatCommandTimestamp(diagnosis.lastToolFinishedAt)],
16992
+ ["\u6D41\u5F0F UI \u9519\u8BEF", diagnosis.lastStreamUiErrorAt ? `${formatCommandTimestamp(diagnosis.lastStreamUiErrorAt)}${diagnosis.lastStreamUiError ? ` \xB7 ${diagnosis.lastStreamUiError}` : ""}` : "-"],
16512
16993
  ["\u672C\u5730\u8FDB\u7A0B", formatHealthProcessProbe(diagnosis)]
16513
16994
  ],
16514
16995
  [diagnosis.healthReason],
@@ -16630,11 +17111,13 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
16630
17111
  lastActivityAt: safeTimestamp,
16631
17112
  lastStatusText: null,
16632
17113
  lastStatusAt: 0,
17114
+ statusNote: null,
16633
17115
  userText: null,
16634
17116
  lastAssistantText: null,
16635
17117
  lastCommentaryText: null,
16636
17118
  streamedText: "",
16637
17119
  streamStarted: false,
17120
+ taskItems: [],
16638
17121
  toolCalls: /* @__PURE__ */ new Map()
16639
17122
  };
16640
17123
  }
@@ -16682,7 +17165,7 @@ function finalizeMirrorTurn(subscription, signature, timestamp, status, preferre
16682
17165
  pendingTurn.lastCommentaryText
16683
17166
  ].map((value) => (value || "").trim()).find(Boolean) || "";
16684
17167
  const userText = pendingTurn.userText?.trim() || null;
16685
- if (!text2 && !userText && pendingTurn.toolCalls.size === 0) return null;
17168
+ if (!text2 && !userText && pendingTurn.toolCalls.size === 0 && pendingTurn.taskItems.length === 0) return null;
16686
17169
  return {
16687
17170
  streamKey: pendingTurn.streamKey,
16688
17171
  userText,
@@ -16721,6 +17204,12 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
16721
17204
  if (completed) finalized.push(completed);
16722
17205
  continue;
16723
17206
  }
17207
+ if (record.type === "task_aborted") {
17208
+ ensureMirrorTurnState(subscription, record);
17209
+ const interrupted = finalizeMirrorTurn(subscription, record.signature, record.timestamp, "interrupted");
17210
+ if (interrupted) finalized.push(interrupted);
17211
+ continue;
17212
+ }
16724
17213
  if (record.type === "message" && record.role === "user") {
16725
17214
  const pendingTurn = ensureMirrorTurnState(subscription, record);
16726
17215
  const text2 = record.content.trim();
@@ -16749,6 +17238,20 @@ function consumeMirrorRecords(subscription, records, hooks = {}) {
16749
17238
  }
16750
17239
  continue;
16751
17240
  }
17241
+ if (record.type === "reasoning") {
17242
+ const pendingTurn = ensureMirrorTurnState(subscription, record);
17243
+ const text2 = record.content.trim();
17244
+ if (!text2) continue;
17245
+ pendingTurn.statusNote = text2;
17246
+ hooks.onStatusProgress?.(subscription, pendingTurn);
17247
+ continue;
17248
+ }
17249
+ if (record.type === "plan_update") {
17250
+ const pendingTurn = ensureMirrorTurnState(subscription, record);
17251
+ pendingTurn.taskItems = record.tasks || [];
17252
+ hooks.onTaskProgress?.(subscription, pendingTurn);
17253
+ continue;
17254
+ }
16752
17255
  if (record.type === "tool_started") {
16753
17256
  const pendingTurn = ensureMirrorTurnState(subscription, record);
16754
17257
  const toolId = record.toolId || record.signature;
@@ -16791,8 +17294,30 @@ function flushTimedOutMirrorTurn(subscription, idleTimeoutMs, nowMs = Date.now()
16791
17294
  "interrupted"
16792
17295
  );
16793
17296
  }
17297
+ function enqueuePendingMirrorDeliveries(subscription, turns) {
17298
+ if (turns.length === 0) return;
17299
+ const existingSignatures = new Set(subscription.pendingDeliveries.map((turn) => turn.signature));
17300
+ for (const turn of turns) {
17301
+ if (existingSignatures.has(turn.signature)) continue;
17302
+ subscription.pendingDeliveries.push(turn);
17303
+ existingSignatures.add(turn.signature);
17304
+ }
17305
+ }
17306
+ function removePendingMirrorDeliveries(subscription, turns) {
17307
+ if (turns.length === 0 || subscription.pendingDeliveries.length === 0) return;
17308
+ const deliveredSignatures = new Set(turns.map((turn) => turn.signature));
17309
+ subscription.pendingDeliveries = subscription.pendingDeliveries.filter(
17310
+ (turn) => !deliveredSignatures.has(turn.signature)
17311
+ );
17312
+ }
17313
+ function selectPendingMirrorDeliveries(subscription, blocked) {
17314
+ if (!blocked) {
17315
+ return subscription.pendingDeliveries.slice();
17316
+ }
17317
+ return subscription.pendingDeliveries.filter((turn) => turn.timedOut);
17318
+ }
16794
17319
  function hasPendingMirrorWork(subscription) {
16795
- return subscription.bufferedRecords.length > 0 || subscription.pendingTurn !== null;
17320
+ return subscription.bufferedRecords.length > 0 || subscription.pendingTurn !== null || subscription.pendingDeliveries.length > 0;
16796
17321
  }
16797
17322
  function consumeBufferedMirrorTurns(subscription, idleTimeoutMs, nowMs = Date.now(), hooks = {}) {
16798
17323
  const bufferedRecords = subscription.bufferedRecords;
@@ -18613,7 +19138,7 @@ function buildConversationPromptText(text2, files = []) {
18613
19138
 
18614
19139
  ${attachmentSupplement}` : attachmentSupplement;
18615
19140
  }
18616
- async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onPromptPrepared) {
19141
+ async function processMessage(binding, text2, onPermissionRequest, abortSignal, files, onPartialText, onToolEvent, onTaskEvent, onStatusNote, onPromptPrepared) {
18617
19142
  const { store, llm } = getBridgeContext();
18618
19143
  const sessionId = binding.codepilotSessionId;
18619
19144
  const lockId = crypto8.randomBytes(8).toString("hex");
@@ -18732,14 +19257,22 @@ async function processMessage(binding, text2, onPermissionRequest, abortSignal,
18732
19257
  }
18733
19258
  }
18734
19259
  });
18735
- return await consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent);
19260
+ return await consumeStream(
19261
+ stream,
19262
+ sessionId,
19263
+ onPermissionRequest,
19264
+ onPartialText,
19265
+ onToolEvent,
19266
+ onTaskEvent,
19267
+ onStatusNote
19268
+ );
18736
19269
  } finally {
18737
19270
  clearInterval(renewalInterval);
18738
19271
  store.releaseSessionLock(sessionId, lockId);
18739
19272
  store.setSessionRuntimeStatus(sessionId, "idle");
18740
19273
  }
18741
19274
  }
18742
- async function consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent) {
19275
+ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialText, onToolEvent, onTaskEvent, onStatusNote) {
18743
19276
  const { store } = getBridgeContext();
18744
19277
  const contentBlocks = [];
18745
19278
  let currentText = "";
@@ -18848,6 +19381,12 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
18848
19381
  if (statusData.model) {
18849
19382
  store.updateSessionModel(sessionId, statusData.model);
18850
19383
  }
19384
+ if (typeof statusData.reasoning === "string" && onStatusNote) {
19385
+ try {
19386
+ onStatusNote(statusData.reasoning);
19387
+ } catch {
19388
+ }
19389
+ }
18851
19390
  } catch {
18852
19391
  }
18853
19392
  break;
@@ -18855,8 +19394,15 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
18855
19394
  case "task_update": {
18856
19395
  try {
18857
19396
  const taskData = JSON.parse(event.data);
18858
- if (taskData.session_id && taskData.todos) {
18859
- store.syncSdkTasks(taskData.session_id, taskData.todos);
19397
+ const tasks = Array.isArray(taskData.tasks) ? taskData.tasks : Array.isArray(taskData.todos) ? taskData.todos : null;
19398
+ if (tasks) {
19399
+ store.syncSdkTasks(sessionId, tasks);
19400
+ if (onTaskEvent) {
19401
+ try {
19402
+ onTaskEvent(tasks);
19403
+ } catch {
19404
+ }
19405
+ }
18860
19406
  }
18861
19407
  } catch {
18862
19408
  }
@@ -18954,6 +19500,14 @@ function pushStreamFeedbackTools(target, tools) {
18954
19500
  } catch {
18955
19501
  }
18956
19502
  }
19503
+ function pushStreamFeedbackTasks(target, tasks) {
19504
+ if (typeof target.adapter.onTaskEvent !== "function") return;
19505
+ target.ensureStarted?.();
19506
+ try {
19507
+ target.adapter.onTaskEvent(target.chatId, tasks, target.streamKey);
19508
+ } catch {
19509
+ }
19510
+ }
18957
19511
  function pushStreamFeedbackStatus(target, text2) {
18958
19512
  if (typeof target.adapter.onStreamStatus !== "function") return;
18959
19513
  target.ensureStarted?.();
@@ -19032,12 +19586,15 @@ function formatRuntimeDuration(ms) {
19032
19586
  if (seconds === 0) return `${hours}h ${minutes}m`;
19033
19587
  return `${hours}h ${minutes}m ${seconds}s`;
19034
19588
  }
19035
- function formatInteractiveRuntimeStatus(elapsedMs, silentMs) {
19589
+ function formatInteractiveRuntimeStatus(elapsedMs, silentMs, statusNote) {
19036
19590
  const parts = [elapsedMs < 1e3 ? "\u5904\u7406\u4E2D" : `\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
19037
19591
  if (typeof silentMs === "number" && silentMs >= 0) {
19038
19592
  parts.push(`\u6700\u8FD1 ${formatRuntimeDuration(silentMs)} \u65E0\u65B0\u8F93\u51FA`);
19039
19593
  }
19040
- return parts.join("\uFF0C");
19594
+ const runtimeText = parts.join("\uFF0C");
19595
+ const note = (statusNote || "").trim();
19596
+ return note ? `\u5F53\u524D\u6B65\u9AA4\uFF1A${note}
19597
+ ${runtimeText}` : runtimeText;
19041
19598
  }
19042
19599
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19043
19600
  const binding = resolve(msg.address);
@@ -19132,23 +19689,35 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19132
19689
  adapter,
19133
19690
  channelType: adapter.channelType,
19134
19691
  chatId: msg.address.chatId,
19135
- streamKey
19692
+ streamKey,
19693
+ ensureStarted: () => {
19694
+ adapter.onMessageStart?.(msg.address.chatId, streamKey);
19695
+ }
19136
19696
  };
19137
19697
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
19138
19698
  const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
19699
+ let latestStatusNote = null;
19700
+ let latestTasks = [];
19139
19701
  const syncStructuredStreamUiState = () => {
19140
19702
  if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
19141
19703
  if (adapter.hasActiveStreamingUi?.(msg.address.chatId, streamKey)) {
19142
19704
  taskState.structuredStreamUiActive = true;
19143
19705
  }
19144
19706
  };
19707
+ const syncStructuredStreamUiSnapshot = () => {
19708
+ if (!supportsStructuredStreamUi) return;
19709
+ syncStructuredStreamUiState();
19710
+ const snapshot = adapter.getStructuredStreamingUiSnapshot?.(msg.address.chatId, streamKey);
19711
+ if (!snapshot) return;
19712
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, snapshot);
19713
+ };
19145
19714
  const pushRunningStatus = (silentMs) => {
19146
19715
  if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
19147
19716
  pushStreamFeedbackStatus(
19148
19717
  streamFeedbackTarget,
19149
- formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, silentMs)
19718
+ formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, silentMs, latestStatusNote)
19150
19719
  );
19151
- syncStructuredStreamUiState();
19720
+ syncStructuredStreamUiSnapshot();
19152
19721
  };
19153
19722
  let streamStatusHeartbeat = null;
19154
19723
  let streamStatusUpdatesClosed = false;
@@ -19182,7 +19751,24 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19182
19751
  pushStreamFeedbackTools(streamFeedbackTarget, Array.from(toolCallTracker.values()));
19183
19752
  }
19184
19753
  pushRunningStatus(null);
19185
- syncStructuredStreamUiState();
19754
+ syncStructuredStreamUiSnapshot();
19755
+ };
19756
+ const onTaskEvent = (tasks) => {
19757
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
19758
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19759
+ latestTasks = tasks;
19760
+ if (hasStreamingCards) {
19761
+ pushStreamFeedbackTasks(streamFeedbackTarget, latestTasks);
19762
+ }
19763
+ pushRunningStatus(null);
19764
+ syncStructuredStreamUiSnapshot();
19765
+ };
19766
+ const onStatusNote = (note) => {
19767
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
19768
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19769
+ latestStatusNote = (note || "").trim() || null;
19770
+ pushRunningStatus(null);
19771
+ syncStructuredStreamUiSnapshot();
19186
19772
  };
19187
19773
  const onPartialText = (fullText) => {
19188
19774
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
@@ -19191,7 +19777,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19191
19777
  previewOnPartialText?.(fullText);
19192
19778
  onStreamCardText?.(fullText);
19193
19779
  pushRunningStatus(null);
19194
- syncStructuredStreamUiState();
19780
+ syncStructuredStreamUiSnapshot();
19195
19781
  };
19196
19782
  if (supportsStructuredStreamUi) {
19197
19783
  pushRunningStatus(null);
@@ -19205,11 +19791,10 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19205
19791
  return;
19206
19792
  }
19207
19793
  const elapsedMs = nowMs() - taskStartedAt;
19208
- if (elapsedMs < streamStatusIdleDetectionStartMs) return;
19209
19794
  const silentMs = nowMs() - taskState.lastActivityAt;
19210
- if (silentMs < streamStatusHeartbeatMs) return;
19211
- pushRunningStatus(silentMs);
19212
- syncStructuredStreamUiState();
19795
+ const showSilentDuration = elapsedMs >= streamStatusIdleDetectionStartMs && silentMs >= streamStatusHeartbeatMs ? silentMs : null;
19796
+ pushRunningStatus(showSilentDuration);
19797
+ syncStructuredStreamUiSnapshot();
19213
19798
  }, streamStatusHeartbeatMs);
19214
19799
  }
19215
19800
  let finalOutcome = "failed";
@@ -19236,11 +19821,16 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19236
19821
  "permission_wait",
19237
19822
  `\u5F53\u524D\u6B63\u5728\u7B49\u5F85\u5DE5\u5177 ${perm.toolName} \u7684\u6743\u9650\u786E\u8BA4\u3002`
19238
19823
  );
19824
+ deps.touchInteractiveTask(binding.codepilotSessionId, taskId);
19825
+ pushRunningStatus(null);
19826
+ syncStructuredStreamUiSnapshot();
19239
19827
  },
19240
19828
  taskAbort.signal,
19241
19829
  attachments && attachments.length > 0 ? attachments : void 0,
19242
19830
  onPartialText,
19243
19831
  onToolEvent,
19832
+ onTaskEvent,
19833
+ onStatusNote,
19244
19834
  (preparedPrompt) => {
19245
19835
  if (!taskState.mirrorSuppressionId) {
19246
19836
  taskState.mirrorSuppressionId = deps.beginMirrorSuppression(binding.codepilotSessionId, preparedPrompt);
@@ -19263,14 +19853,16 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19263
19853
  }
19264
19854
  if (result.responseText || result.outboundAttachments.length > 0) {
19265
19855
  const textToDeliver = cardFinalized ? "" : result.responseText;
19266
- await deps.deliverResponse(
19267
- adapter,
19268
- msg.address,
19269
- textToDeliver,
19270
- binding.codepilotSessionId,
19271
- msg.messageId,
19272
- result.outboundAttachments
19273
- );
19856
+ if (!cardFinalized || result.outboundAttachments.length > 0) {
19857
+ await deps.deliverResponse(
19858
+ adapter,
19859
+ msg.address,
19860
+ textToDeliver,
19861
+ binding.codepilotSessionId,
19862
+ msg.messageId,
19863
+ result.outboundAttachments
19864
+ );
19865
+ }
19274
19866
  } else if (result.hasError) {
19275
19867
  await deps.deliverResponse(
19276
19868
  adapter,
@@ -19289,6 +19881,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19289
19881
  finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
19290
19882
  } finally {
19291
19883
  stopStructuredStreamStatusUpdates();
19884
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
19292
19885
  if (previewState) {
19293
19886
  if (previewState.throttleTimer) {
19294
19887
  clearTimeout(previewState.throttleTimer);
@@ -19504,6 +20097,7 @@ function resetMirrorReadState(subscription) {
19504
20097
  subscription.fileIdentity = null;
19505
20098
  subscription.trailingText = "";
19506
20099
  subscription.activeMirrorTurnId = null;
20100
+ subscription.activeSpecialCallIds.clear();
19507
20101
  subscription.bufferedRecords = [];
19508
20102
  }
19509
20103
  function createMirrorSubscription(input) {
@@ -19527,8 +20121,11 @@ function createMirrorSubscription(input) {
19527
20121
  fileIdentity: null,
19528
20122
  trailingText: "",
19529
20123
  activeMirrorTurnId: null,
20124
+ activeSpecialCallIds: /* @__PURE__ */ new Set(),
19530
20125
  bufferedRecords: [],
19531
20126
  pendingTurn: null,
20127
+ pendingDeliveries: [],
20128
+ unknownMirrorKindsSeen: /* @__PURE__ */ new Set(),
19532
20129
  missingThreadPolls: 0,
19533
20130
  consecutiveFailures: 0,
19534
20131
  suspendedUntil: null
@@ -19539,6 +20136,8 @@ function resetMirrorSubscriptionForThreadChange(subscription, lastDeliveredAt) {
19539
20136
  subscription.lastDeliveredAt = lastDeliveredAt;
19540
20137
  subscription.dirty = true;
19541
20138
  subscription.pendingTurn = null;
20139
+ subscription.pendingDeliveries = [];
20140
+ subscription.unknownMirrorKindsSeen.clear();
19542
20141
  subscription.missingThreadPolls = 0;
19543
20142
  subscription.consecutiveFailures = 0;
19544
20143
  subscription.suspendedUntil = null;
@@ -19735,6 +20334,7 @@ function isMirrorSnapshotUnchanged(subscription, snapshot) {
19735
20334
  }
19736
20335
  function readMirrorDeliverableRecords(subscription, snapshot) {
19737
20336
  let deliverableRecords = [];
20337
+ let unknownKinds = [];
19738
20338
  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;
19739
20339
  if (requiresFullRecover) {
19740
20340
  const previousCursor = subscription.cursor;
@@ -19743,7 +20343,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19743
20343
  0,
19744
20344
  snapshot.size,
19745
20345
  "",
19746
- null
20346
+ null,
20347
+ []
19747
20348
  );
19748
20349
  const delta = reconcileDesktopMirrorCursor(subscription.cursor, fullDelta.records);
19749
20350
  subscription.cursor = delta.nextCursor;
@@ -19751,6 +20352,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19751
20352
  subscription.trailingText = "";
19752
20353
  subscription.fileOffset = snapshot.size;
19753
20354
  subscription.activeMirrorTurnId = fullDelta.nextTurnId;
20355
+ subscription.activeSpecialCallIds = new Set(fullDelta.nextSpecialCallIds);
20356
+ unknownKinds = fullDelta.unknownKinds;
19754
20357
  } else if (snapshot.size > subscription.fileOffset || subscription.trailingText) {
19755
20358
  const previousCursor = subscription.cursor;
19756
20359
  const delta = readDesktopSessionMirrorRecordDeltaByFilePath(
@@ -19758,19 +20361,25 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
19758
20361
  subscription.fileOffset,
19759
20362
  snapshot.size,
19760
20363
  subscription.trailingText,
19761
- subscription.activeMirrorTurnId
20364
+ subscription.activeMirrorTurnId,
20365
+ subscription.activeSpecialCallIds
19762
20366
  );
19763
20367
  deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.records);
19764
20368
  subscription.cursor = advanceDesktopMirrorCursor(subscription.cursor, delta.records);
19765
20369
  subscription.trailingText = delta.trailingText;
19766
20370
  subscription.fileOffset = delta.nextOffset;
19767
20371
  subscription.activeMirrorTurnId = delta.nextTurnId;
20372
+ subscription.activeSpecialCallIds = new Set(delta.nextSpecialCallIds);
20373
+ unknownKinds = delta.unknownKinds;
19768
20374
  }
19769
20375
  subscription.fileSize = snapshot.size;
19770
20376
  subscription.fileMtimeMs = snapshot.mtimeMs;
19771
20377
  subscription.fileIdentity = snapshot.identity;
19772
20378
  subscription.dirty = false;
19773
- return deliverableRecords;
20379
+ return {
20380
+ records: deliverableRecords,
20381
+ unknownKinds
20382
+ };
19774
20383
  }
19775
20384
 
19776
20385
  // src/lib/bridge/mirror-delivery-plan.ts
@@ -19785,7 +20394,7 @@ function buildMirrorDeliveryPlan(subscription, deliverableRecords, options) {
19785
20394
  if (options.blocked) {
19786
20395
  return {
19787
20396
  syncReason: "mirror reconcile active task",
19788
- turnsToDeliver: timedOutTurn ? [timedOutTurn] : []
20397
+ finalizedTurns: timedOutTurn ? [timedOutTurn] : []
19789
20398
  };
19790
20399
  }
19791
20400
  const finalizedTurns = timedOutTurn ? [timedOutTurn] : [];
@@ -19793,12 +20402,12 @@ function buildMirrorDeliveryPlan(subscription, deliverableRecords, options) {
19793
20402
  if (finalizedTurns.length === 0) {
19794
20403
  return {
19795
20404
  syncReason: "mirror reconcile no finalized turns",
19796
- turnsToDeliver: []
20405
+ finalizedTurns: []
19797
20406
  };
19798
20407
  }
19799
20408
  return {
19800
20409
  syncReason: "mirror reconcile delivered turns",
19801
- turnsToDeliver: finalizedTurns
20410
+ finalizedTurns
19802
20411
  };
19803
20412
  }
19804
20413
 
@@ -20022,21 +20631,36 @@ function createMirrorRuntime(getState2, options, deps) {
20022
20631
  deps.syncMirrorSessionStateSafe(subscription.sessionId, "mirror reconcile unchanged snapshot");
20023
20632
  return "processed";
20024
20633
  }
20025
- const deliverableRecords = readMirrorDeliverableRecords(subscription, snapshot);
20634
+ const readResult = readMirrorDeliverableRecords(subscription, snapshot);
20635
+ const deliverableRecords = readResult.records;
20636
+ for (const kind of readResult.unknownKinds) {
20637
+ if (subscription.unknownMirrorKindsSeen.has(kind)) continue;
20638
+ subscription.unknownMirrorKindsSeen.add(kind);
20639
+ console.warn(
20640
+ `[bridge-manager] Unhandled desktop mirror event for thread ${subscription.threadId}: ${kind}`
20641
+ );
20642
+ }
20026
20643
  if (deliverableRecords.length > 0) {
20027
20644
  deps.observeSessionHealthRecords(subscription.sessionId, subscription.threadId, deliverableRecords);
20028
20645
  }
20646
+ const blocked = getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId);
20029
20647
  const deliveryPlan = buildMirrorDeliveryPlan(subscription, deliverableRecords, {
20030
- blocked: getState2().activeTasks.has(subscription.sessionId) || deps.isMirrorSuppressed(subscription.sessionId),
20648
+ blocked,
20031
20649
  filterSuppressedRecords: deps.filterSuppressedMirrorRecords,
20032
20650
  flushTimedOutTurn: (currentSubscription) => deps.flushTimedOutMirrorTurn(currentSubscription),
20033
20651
  consumeBufferedTurns: (currentSubscription) => deps.consumeBufferedMirrorTurns(currentSubscription)
20034
20652
  });
20035
- if (deliveryPlan.turnsToDeliver.length > 0) {
20036
- try {
20037
- await deps.deliverMirrorTurns(subscription, deliveryPlan.turnsToDeliver);
20038
- } catch (error) {
20039
- subscription.dirty = true;
20653
+ if (deliveryPlan.finalizedTurns.length > 0) {
20654
+ enqueuePendingMirrorDeliveries(subscription, deliveryPlan.finalizedTurns);
20655
+ }
20656
+ const turnsToAttempt = selectPendingMirrorDeliveries(subscription, blocked);
20657
+ if (turnsToAttempt.length > 0) {
20658
+ const deliveryResult = await deps.deliverMirrorTurns(subscription, turnsToAttempt);
20659
+ if (deliveryResult.deliveredCount > 0) {
20660
+ removePendingMirrorDeliveries(subscription, turnsToAttempt.slice(0, deliveryResult.deliveredCount));
20661
+ }
20662
+ if (deliveryResult.error) {
20663
+ const error = deliveryResult.error;
20040
20664
  console.warn("[bridge-manager] Mirror delivery failed:", error instanceof Error ? error.message : error);
20041
20665
  }
20042
20666
  }
@@ -20210,11 +20834,13 @@ var HEALTH_RECENT_PROGRESS_MS = 10 * 60 * 1e3;
20210
20834
  var HEALTH_SLOW_OBSERVED_MS = 30 * 60 * 1e3;
20211
20835
  var HEALTH_PROGRESS_PERSIST_THROTTLE_MS = 15 * 1e3;
20212
20836
  var HEALTH_PROCESS_PROBE_CACHE_MS = 30 * 1e3;
20837
+ var HEALTH_STREAM_UI_STALL_MS = 60 * 1e3;
20213
20838
  var RUNNING_HEALTH_STATUSES = /* @__PURE__ */ new Set([
20214
20839
  "running_active",
20215
20840
  "waiting_tool",
20216
20841
  "slow_observed",
20217
20842
  "suspected_stall",
20843
+ "suspected_stream_ui_stall",
20218
20844
  "suspected_detached"
20219
20845
  ]);
20220
20846
  function parseIsoMs(value) {
@@ -20282,6 +20908,10 @@ function buildProgressReason(type, detail) {
20282
20908
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u684C\u9762\u4F1A\u8BDD\u6D88\u606F\u3002";
20283
20909
  case "commentary":
20284
20910
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u6267\u884C\u8FDB\u5C55\u8BF4\u660E\u3002";
20911
+ case "reasoning":
20912
+ return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u601D\u8003/\u72B6\u6001\u8BF4\u660E\u3002";
20913
+ case "plan_update":
20914
+ return "\u6700\u8FD1\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
20285
20915
  case "text":
20286
20916
  return "\u6700\u8FD1\u6536\u5230\u4E86\u65B0\u7684\u6B63\u6587\u8F93\u51FA\u3002";
20287
20917
  case "permission_wait":
@@ -20329,6 +20959,12 @@ function computeBaseDiagnosis(session, nowMs) {
20329
20959
  const activeToolName = summarizeActiveTools(activeTools) || trimOrNull(session.active_tool_name);
20330
20960
  const activeToolStartedAt = getActiveToolStartedAt(activeTools) || trimOrNull(session.active_tool_started_at);
20331
20961
  const lastToolFinishedAt = trimOrNull(session.last_tool_finished_at);
20962
+ const lastStreamUiAttemptAt = trimOrNull(session.last_stream_ui_attempt_at);
20963
+ const lastStreamUiUpdateAt = trimOrNull(session.last_stream_ui_update_at);
20964
+ const streamUiFlushStartedAt = trimOrNull(session.stream_ui_flush_started_at);
20965
+ const lastStreamUiErrorAt = trimOrNull(session.last_stream_ui_error_at);
20966
+ const lastStreamUiError = trimOrNull(session.last_stream_ui_error);
20967
+ 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;
20332
20968
  const sdkSessionId = trimOrNull(session.sdk_session_id);
20333
20969
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
20334
20970
  const previousStatus = session.health_status || "idle";
@@ -20344,6 +20980,12 @@ function computeBaseDiagnosis(session, nowMs) {
20344
20980
  activeToolName,
20345
20981
  activeToolStartedAt,
20346
20982
  lastToolFinishedAt,
20983
+ lastStreamUiAttemptAt,
20984
+ lastStreamUiUpdateAt,
20985
+ streamUiFlushStartedAt,
20986
+ lastStreamUiErrorAt,
20987
+ lastStreamUiError,
20988
+ streamUiConsecutiveFailures,
20347
20989
  sdkSessionId
20348
20990
  };
20349
20991
  }
@@ -20375,6 +21017,12 @@ function computeBaseDiagnosis(session, nowMs) {
20375
21017
  activeToolName,
20376
21018
  activeToolStartedAt,
20377
21019
  lastToolFinishedAt,
21020
+ lastStreamUiAttemptAt,
21021
+ lastStreamUiUpdateAt,
21022
+ streamUiFlushStartedAt,
21023
+ lastStreamUiErrorAt,
21024
+ lastStreamUiError,
21025
+ streamUiConsecutiveFailures,
20378
21026
  sdkSessionId
20379
21027
  };
20380
21028
  }
@@ -20415,11 +21063,75 @@ function applyProcessProbeDiagnosis(diagnosis, processProbe) {
20415
21063
  processProbe
20416
21064
  };
20417
21065
  }
21066
+ function applyStreamUiDiagnosis(diagnosis, nowMs) {
21067
+ if (!isRunningRuntimeStatus(diagnosis.runtimeStatus)) {
21068
+ return diagnosis;
21069
+ }
21070
+ const lastProgressMs = parseIsoMs(diagnosis.lastProgressAt || void 0);
21071
+ if (!lastProgressMs || nowMs - lastProgressMs > HEALTH_RECENT_PROGRESS_MS) {
21072
+ return diagnosis;
21073
+ }
21074
+ const lastStreamUiUpdateMs = parseIsoMs(diagnosis.lastStreamUiUpdateAt || void 0);
21075
+ const lastStreamUiAttemptMs = parseIsoMs(diagnosis.lastStreamUiAttemptAt || void 0);
21076
+ const streamUiFlushStartedMs = parseIsoMs(diagnosis.streamUiFlushStartedAt || void 0);
21077
+ const lastStreamUiErrorText = diagnosis.lastStreamUiError?.trim();
21078
+ if (streamUiFlushStartedMs && nowMs - streamUiFlushStartedMs >= HEALTH_STREAM_UI_STALL_MS) {
21079
+ 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"];
21080
+ if (diagnosis.streamUiConsecutiveFailures > 0) {
21081
+ details.push(`\u6700\u8FD1\u8FDE\u7EED\u5931\u8D25 ${diagnosis.streamUiConsecutiveFailures} \u6B21\u3002`);
21082
+ }
21083
+ if (lastStreamUiErrorText) {
21084
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21085
+ }
21086
+ return {
21087
+ ...diagnosis,
21088
+ healthStatus: "suspected_stream_ui_stall",
21089
+ healthReason: details.join(" ")
21090
+ };
21091
+ }
21092
+ if (lastStreamUiUpdateMs && lastProgressMs - lastStreamUiUpdateMs >= HEALTH_STREAM_UI_STALL_MS) {
21093
+ 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"];
21094
+ if (lastStreamUiErrorText) {
21095
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21096
+ }
21097
+ return {
21098
+ ...diagnosis,
21099
+ healthStatus: "suspected_stream_ui_stall",
21100
+ healthReason: details.join(" ")
21101
+ };
21102
+ }
21103
+ if (!lastStreamUiUpdateMs && lastStreamUiAttemptMs && lastProgressMs - lastStreamUiAttemptMs >= HEALTH_STREAM_UI_STALL_MS) {
21104
+ 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"];
21105
+ if (lastStreamUiErrorText) {
21106
+ details.push(`\u6700\u8FD1\u9519\u8BEF\uFF1A${lastStreamUiErrorText}`);
21107
+ }
21108
+ return {
21109
+ ...diagnosis,
21110
+ healthStatus: "suspected_stream_ui_stall",
21111
+ healthReason: details.join(" ")
21112
+ };
21113
+ }
21114
+ return diagnosis;
21115
+ }
20418
21116
 
20419
21117
  // src/lib/bridge/session-health-runtime.ts
20420
21118
  function createSessionHealthRuntime(deps) {
20421
21119
  const lastProgressPersistAt = /* @__PURE__ */ new Map();
20422
21120
  const processProbeCache = /* @__PURE__ */ new Map();
21121
+ function summarizePlanUpdate(tasks) {
21122
+ if (!Array.isArray(tasks) || tasks.length === 0) {
21123
+ return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u66F4\u65B0\u4E86\u4EFB\u52A1\u8BA1\u5212\u3002";
21124
+ }
21125
+ let inProgress = 0;
21126
+ let pending = 0;
21127
+ let completed = 0;
21128
+ for (const task of tasks) {
21129
+ if (task?.status === "completed") completed += 1;
21130
+ else if (task?.status === "in_progress") inProgress += 1;
21131
+ else pending += 1;
21132
+ }
21133
+ 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`;
21134
+ }
20423
21135
  function updateSessionHealth(sessionId, updates, options) {
20424
21136
  const store = deps.getStore();
20425
21137
  const session = store.getSession(sessionId);
@@ -20462,7 +21174,13 @@ function createSessionHealthRuntime(deps) {
20462
21174
  active_tools_json: void 0,
20463
21175
  active_tool_name: void 0,
20464
21176
  active_tool_started_at: void 0,
20465
- last_tool_finished_at: void 0
21177
+ last_tool_finished_at: void 0,
21178
+ last_stream_ui_attempt_at: void 0,
21179
+ last_stream_ui_update_at: void 0,
21180
+ stream_ui_flush_started_at: void 0,
21181
+ last_stream_ui_error_at: void 0,
21182
+ last_stream_ui_error: void 0,
21183
+ stream_ui_consecutive_failures: void 0
20466
21184
  });
20467
21185
  }
20468
21186
  function recordInteractiveProgress(sessionId, type, detail) {
@@ -20530,11 +21248,29 @@ function createSessionHealthRuntime(deps) {
20530
21248
  active_tools_json: void 0,
20531
21249
  active_tool_name: void 0,
20532
21250
  active_tool_started_at: void 0,
21251
+ stream_ui_flush_started_at: void 0,
20533
21252
  last_health_check_at: nowIso4
20534
21253
  }, { force: true });
20535
21254
  lastProgressPersistAt.set(sessionId, Date.now());
20536
21255
  processProbeCache.delete(sessionId);
20537
21256
  }
21257
+ function toIso(value) {
21258
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
21259
+ return new Date(value).toISOString();
21260
+ }
21261
+ function recordStructuredStreamUi(sessionId, snapshot) {
21262
+ const updates = {
21263
+ stream_ui_flush_started_at: snapshot.active && snapshot.flushInFlight ? toIso(snapshot.flushInFlightSince ?? snapshot.lastAttemptAt) : void 0
21264
+ };
21265
+ if (snapshot.active) {
21266
+ updates.last_stream_ui_attempt_at = toIso(snapshot.lastAttemptAt);
21267
+ updates.last_stream_ui_update_at = toIso(snapshot.lastUpdateAt);
21268
+ updates.last_stream_ui_error_at = toIso(snapshot.lastErrorAt);
21269
+ updates.last_stream_ui_error = snapshot.lastError?.trim() || void 0;
21270
+ updates.stream_ui_consecutive_failures = snapshot.consecutiveFailures && snapshot.consecutiveFailures > 0 ? snapshot.consecutiveFailures : void 0;
21271
+ }
21272
+ updateSessionHealth(sessionId, updates);
21273
+ }
20538
21274
  function observeDesktopMirrorRecords(sessionId, _threadId, records) {
20539
21275
  for (const record of records) {
20540
21276
  if (record.type === "task_started") {
@@ -20545,6 +21281,10 @@ function createSessionHealthRuntime(deps) {
20545
21281
  recordInteractiveEnd(sessionId, "completed", "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002");
20546
21282
  continue;
20547
21283
  }
21284
+ if (record.type === "task_aborted") {
21285
+ recordInteractiveEnd(sessionId, "aborted", "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002");
21286
+ continue;
21287
+ }
20548
21288
  if (record.type === "tool_started") {
20549
21289
  recordToolState(sessionId, record.toolId || record.signature, record.toolName || "tool", "running");
20550
21290
  continue;
@@ -20558,6 +21298,22 @@ function createSessionHealthRuntime(deps) {
20558
21298
  );
20559
21299
  continue;
20560
21300
  }
21301
+ if (record.type === "reasoning") {
21302
+ recordInteractiveProgress(
21303
+ sessionId,
21304
+ "reasoning",
21305
+ "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u65B0\u7684\u601D\u8003/\u72B6\u6001\u8BF4\u660E\u3002"
21306
+ );
21307
+ continue;
21308
+ }
21309
+ if (record.type === "plan_update") {
21310
+ recordInteractiveProgress(
21311
+ sessionId,
21312
+ "plan_update",
21313
+ summarizePlanUpdate(record.tasks)
21314
+ );
21315
+ continue;
21316
+ }
20561
21317
  if (record.type === "message") {
20562
21318
  recordInteractiveProgress(
20563
21319
  sessionId,
@@ -20572,7 +21328,10 @@ function createSessionHealthRuntime(deps) {
20572
21328
  const store = deps.getStore();
20573
21329
  for (const session of store.listSessions()) {
20574
21330
  if (!shouldTrackSession(session)) continue;
20575
- const diagnosis = computeBaseDiagnosis(session, nowMs);
21331
+ const diagnosis = applyStreamUiDiagnosis(
21332
+ { ...computeBaseDiagnosis(session, nowMs), processProbe: null },
21333
+ nowMs
21334
+ );
20576
21335
  updateSessionHealth(session.id, {
20577
21336
  health_status: diagnosis.healthStatus,
20578
21337
  health_reason: diagnosis.healthReason
@@ -20600,7 +21359,10 @@ function createSessionHealthRuntime(deps) {
20600
21359
  if (!session) return null;
20601
21360
  const base = computeBaseDiagnosis(session, Date.now());
20602
21361
  const processProbe = await loadProcessProbe(session);
20603
- const diagnosis = applyProcessProbeDiagnosis(base, processProbe);
21362
+ const diagnosis = applyStreamUiDiagnosis(
21363
+ applyProcessProbeDiagnosis(base, processProbe),
21364
+ Date.now()
21365
+ );
20604
21366
  updateSessionHealth(sessionId, {
20605
21367
  health_status: diagnosis.healthStatus,
20606
21368
  health_reason: diagnosis.healthReason,
@@ -20618,6 +21380,7 @@ function createSessionHealthRuntime(deps) {
20618
21380
  recordInteractiveStart,
20619
21381
  recordInteractiveProgress,
20620
21382
  recordToolState,
21383
+ recordStructuredStreamUi,
20621
21384
  recordInteractiveEnd,
20622
21385
  observeDesktopMirrorRecords,
20623
21386
  reconcileSessionHealth,
@@ -20871,7 +21634,8 @@ function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
20871
21634
  }
20872
21635
  const statusText = formatInteractiveRuntimeStatus(
20873
21636
  Math.max(0, nowMs - startedAtMs),
20874
- options.silentMs
21637
+ options.silentMs,
21638
+ turnState.statusNote
20875
21639
  );
20876
21640
  if (turnState.lastStatusText === statusText) return;
20877
21641
  pushStreamFeedbackStatus(
@@ -20920,6 +21684,20 @@ function updateMirrorToolProgress(subscription, turnState) {
20920
21684
  );
20921
21685
  pushMirrorStreamingStatus(subscription, turnState);
20922
21686
  }
21687
+ function updateMirrorTaskProgress(subscription, turnState) {
21688
+ const adapter = getMirrorStreamingAdapter(subscription);
21689
+ if (!adapter) return;
21690
+ pushStreamFeedbackTasks(
21691
+ createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
21692
+ turnState.taskItems
21693
+ );
21694
+ pushMirrorStreamingStatus(subscription, turnState);
21695
+ }
21696
+ function updateMirrorStatusProgress(subscription, turnState) {
21697
+ const adapter = getMirrorStreamingAdapter(subscription);
21698
+ if (!adapter) return;
21699
+ pushMirrorStreamingStatus(subscription, turnState);
21700
+ }
20923
21701
  function stopMirrorStreaming(subscription, status = "interrupted") {
20924
21702
  const adapter = getMirrorStreamingAdapter(subscription);
20925
21703
  const pendingTurn = subscription.pendingTurn;
@@ -20980,12 +21758,21 @@ async function deliverMirrorTurn(subscription, turn) {
20980
21758
  subscription.lastDeliveredAt = turn.timestamp || nowIso3();
20981
21759
  }
20982
21760
  async function deliverMirrorTurns(subscription, turns) {
20983
- for (const turn of turns.slice(-MIRROR_EVENT_BATCH_LIMIT)) {
20984
- await deliverMirrorTurn(subscription, turn);
21761
+ let deliveredCount = 0;
21762
+ for (const turn of turns.slice(0, MIRROR_EVENT_BATCH_LIMIT)) {
21763
+ try {
21764
+ await deliverMirrorTurn(subscription, turn);
21765
+ deliveredCount += 1;
21766
+ } catch (error) {
21767
+ return { deliveredCount, error };
21768
+ }
20985
21769
  }
21770
+ return { deliveredCount };
20986
21771
  }
20987
21772
  var MIRROR_TURN_HOOKS = {
20988
21773
  onStreamText: updateMirrorStreaming,
21774
+ onStatusProgress: updateMirrorStatusProgress,
21775
+ onTaskProgress: updateMirrorTaskProgress,
20989
21776
  onToolProgress: updateMirrorToolProgress
20990
21777
  };
20991
21778
  function consumeMirrorRecords2(subscription, records) {
@@ -21272,6 +22059,9 @@ async function handleMessage(adapter, msg) {
21272
22059
  recordInteractiveHealthTool: (sessionId, toolId, toolName, status) => {
21273
22060
  SESSION_HEALTH_RUNTIME.recordToolState(sessionId, toolId, toolName, status);
21274
22061
  },
22062
+ recordInteractiveStreamUiSnapshot: (sessionId, snapshot) => {
22063
+ SESSION_HEALTH_RUNTIME.recordStructuredStreamUi(sessionId, snapshot);
22064
+ },
21275
22065
  recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
21276
22066
  beginMirrorSuppression: beginMirrorSuppression2,
21277
22067
  abortMirrorSuppression: abortMirrorSuppression2,
@@ -22424,10 +23214,87 @@ function setupLogger() {
22424
23214
  console.warn = (...args) => write("WARN", args);
22425
23215
  }
22426
23216
 
23217
+ // src/bridge-instance-lock.ts
23218
+ import fs14 from "node:fs";
23219
+ import path15 from "node:path";
23220
+ var runtimeDir = path15.join(CTI_HOME, "runtime");
23221
+ var bridgeInstanceLockFile = path15.join(runtimeDir, "bridge.instance.lock");
23222
+ function readJsonFile(filePath, fallback) {
23223
+ try {
23224
+ return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
23225
+ } catch {
23226
+ return fallback;
23227
+ }
23228
+ }
23229
+ function isProcessAlive(pid) {
23230
+ if (!pid) return false;
23231
+ try {
23232
+ process.kill(pid, 0);
23233
+ return true;
23234
+ } catch {
23235
+ return false;
23236
+ }
23237
+ }
23238
+ function readBridgeInstanceLock(filePath = bridgeInstanceLockFile) {
23239
+ const parsed = readJsonFile(filePath, null);
23240
+ const pid = Number(parsed?.pid);
23241
+ const createdAt = typeof parsed?.createdAt === "string" ? parsed.createdAt : "";
23242
+ if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
23243
+ return { pid, createdAt };
23244
+ }
23245
+ function tryAcquireBridgeInstanceLock(options = {}) {
23246
+ const filePath = options.filePath ?? bridgeInstanceLockFile;
23247
+ const ownerPid = options.ownerPid ?? process.pid;
23248
+ const nowMs = options.nowMs ?? Date.now();
23249
+ const isAlive = options.isAlive ?? isProcessAlive;
23250
+ const payload = {
23251
+ pid: ownerPid,
23252
+ createdAt: new Date(nowMs).toISOString()
23253
+ };
23254
+ for (let attempt = 0; attempt < 2; attempt += 1) {
23255
+ try {
23256
+ fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
23257
+ fs14.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
23258
+ return { acquired: true };
23259
+ } catch (error) {
23260
+ const code2 = error.code;
23261
+ if (code2 !== "EEXIST") throw error;
23262
+ const existing2 = readBridgeInstanceLock(filePath);
23263
+ if (existing2 && existing2.pid !== ownerPid && isAlive(existing2.pid)) {
23264
+ return { acquired: false, holderPid: existing2.pid };
23265
+ }
23266
+ try {
23267
+ fs14.unlinkSync(filePath);
23268
+ } catch {
23269
+ }
23270
+ }
23271
+ }
23272
+ const existing = readBridgeInstanceLock(filePath);
23273
+ if (existing && existing.pid !== ownerPid && isAlive(existing.pid)) {
23274
+ return { acquired: false, holderPid: existing.pid };
23275
+ }
23276
+ return existing?.pid === ownerPid ? { acquired: true } : { acquired: false, holderPid: existing?.pid };
23277
+ }
23278
+ function releaseBridgeInstanceLock(filePath = bridgeInstanceLockFile, ownerPid = process.pid) {
23279
+ const existing = readBridgeInstanceLock(filePath);
23280
+ if (!existing) {
23281
+ try {
23282
+ fs14.unlinkSync(filePath);
23283
+ } catch {
23284
+ }
23285
+ return;
23286
+ }
23287
+ if (existing.pid !== ownerPid) return;
23288
+ try {
23289
+ fs14.unlinkSync(filePath);
23290
+ } catch {
23291
+ }
23292
+ }
23293
+
22427
23294
  // src/main.ts
22428
- var RUNTIME_DIR = path16.join(CTI_HOME, "runtime");
22429
- var STATUS_FILE = path16.join(RUNTIME_DIR, "status.json");
22430
- var PID_FILE = path16.join(RUNTIME_DIR, "bridge.pid");
23295
+ var RUNTIME_DIR = path17.join(CTI_HOME, "runtime");
23296
+ var STATUS_FILE = path17.join(RUNTIME_DIR, "status.json");
23297
+ var PID_FILE = path17.join(RUNTIME_DIR, "bridge.pid");
22431
23298
  async function resolveProvider(config2, pendingPerms) {
22432
23299
  const runtime = config2.runtime;
22433
23300
  if (runtime === "codex") {
@@ -22477,16 +23344,16 @@ async function resolveProvider(config2, pendingPerms) {
22477
23344
  return new SDKLLMProvider(pendingPerms, cliPath, config2.autoApprove);
22478
23345
  }
22479
23346
  function writeStatus(info) {
22480
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
23347
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
22481
23348
  let existing = {};
22482
23349
  try {
22483
- existing = JSON.parse(fs15.readFileSync(STATUS_FILE, "utf-8"));
23350
+ existing = JSON.parse(fs16.readFileSync(STATUS_FILE, "utf-8"));
22484
23351
  } catch {
22485
23352
  }
22486
23353
  const merged = { ...existing, ...info };
22487
23354
  const tmp = STATUS_FILE + ".tmp";
22488
- fs15.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
22489
- fs15.renameSync(tmp, STATUS_FILE);
23355
+ fs16.writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf-8");
23356
+ fs16.renameSync(tmp, STATUS_FILE);
22490
23357
  }
22491
23358
  function getRunningChannels() {
22492
23359
  return getStatus().adapters.map((adapter) => adapter.channelType).sort();
@@ -22495,6 +23362,24 @@ function getAdapterStatuses() {
22495
23362
  return getStatus().adapters;
22496
23363
  }
22497
23364
  async function main() {
23365
+ const lockState = tryAcquireBridgeInstanceLock();
23366
+ if (!lockState.acquired) {
23367
+ const holderPid = lockState.holderPid;
23368
+ writeStatus({
23369
+ running: true,
23370
+ ...Number.isFinite(holderPid) && holderPid ? { pid: holderPid } : {}
23371
+ });
23372
+ console.log(
23373
+ `[codex-to-im] Another bridge daemon is already running${holderPid ? ` (PID: ${holderPid})` : ""}. Exiting duplicate launcher.`
23374
+ );
23375
+ process.exit(0);
23376
+ }
23377
+ let instanceLockHeld = true;
23378
+ const releaseInstanceLock = () => {
23379
+ if (!instanceLockHeld) return;
23380
+ releaseBridgeInstanceLock(void 0, process.pid);
23381
+ instanceLockHeld = false;
23382
+ };
22498
23383
  const config2 = loadConfig();
22499
23384
  setupLogger();
22500
23385
  const runId = crypto10.randomUUID();
@@ -22513,8 +23398,8 @@ async function main() {
22513
23398
  permissions: gateway,
22514
23399
  lifecycle: {
22515
23400
  onBridgeStart: () => {
22516
- fs15.mkdirSync(RUNTIME_DIR, { recursive: true });
22517
- fs15.writeFileSync(PID_FILE, String(process.pid), "utf-8");
23401
+ fs16.mkdirSync(RUNTIME_DIR, { recursive: true });
23402
+ fs16.writeFileSync(PID_FILE, String(process.pid), "utf-8");
22518
23403
  const channels = getRunningChannels();
22519
23404
  writeStatus({
22520
23405
  running: true,
@@ -22537,6 +23422,7 @@ async function main() {
22537
23422
  console.log(`[codex-to-im] Active channels updated: ${channels.join(", ") || "none"}`);
22538
23423
  },
22539
23424
  onBridgeStop: () => {
23425
+ releaseInstanceLock();
22540
23426
  writeStatus({ running: false, channels: [], adapters: [] });
22541
23427
  console.log("[codex-to-im] Bridge stopped");
22542
23428
  }
@@ -22551,6 +23437,7 @@ async function main() {
22551
23437
  console.log(`[codex-to-im] Shutting down (${reason})...`);
22552
23438
  pendingPerms.denyAll();
22553
23439
  await stop();
23440
+ releaseInstanceLock();
22554
23441
  writeStatus({ running: false, lastExitReason: reason });
22555
23442
  process.exit(0);
22556
23443
  };
@@ -22563,6 +23450,7 @@ async function main() {
22563
23450
  });
22564
23451
  process.on("uncaughtException", (err) => {
22565
23452
  console.error("[codex-to-im] uncaughtException:", err.stack || err.message);
23453
+ releaseInstanceLock();
22566
23454
  writeStatus({ running: false, lastExitReason: `uncaughtException: ${err.message}` });
22567
23455
  process.exit(1);
22568
23456
  });
@@ -22570,6 +23458,7 @@ async function main() {
22570
23458
  console.log(`[codex-to-im] beforeExit (code: ${code2})`);
22571
23459
  });
22572
23460
  process.on("exit", (code2) => {
23461
+ releaseInstanceLock();
22573
23462
  console.log(`[codex-to-im] exit (code: ${code2})`);
22574
23463
  });
22575
23464
  setInterval(() => {
@@ -22577,6 +23466,7 @@ async function main() {
22577
23466
  }
22578
23467
  main().catch((err) => {
22579
23468
  console.error("[codex-to-im] Fatal error:", err instanceof Error ? err.stack || err.message : err);
23469
+ releaseBridgeInstanceLock(void 0, process.pid);
22580
23470
  try {
22581
23471
  writeStatus({ running: false, lastExitReason: `fatal: ${err instanceof Error ? err.message : String(err)}` });
22582
23472
  } catch {