codex-to-im 1.0.17 → 1.0.19

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/cli.mjs CHANGED
@@ -22,6 +22,29 @@ function resolveDefaultCtiHome() {
22
22
  }
23
23
  var CTI_HOME = process.env.CTI_HOME || resolveDefaultCtiHome();
24
24
  var CONFIG_PATH = path.join(CTI_HOME, "config.env");
25
+ function parseEnvFile(content) {
26
+ const entries = /* @__PURE__ */ new Map();
27
+ for (const line of content.split("\n")) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed || trimmed.startsWith("#")) continue;
30
+ const eqIdx = trimmed.indexOf("=");
31
+ if (eqIdx === -1) continue;
32
+ const key = trimmed.slice(0, eqIdx).trim();
33
+ let value = trimmed.slice(eqIdx + 1).trim();
34
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
35
+ value = value.slice(1, -1);
36
+ }
37
+ entries.set(key, value);
38
+ }
39
+ return entries;
40
+ }
41
+ function loadRawConfigEnv() {
42
+ try {
43
+ return parseEnvFile(fs.readFileSync(CONFIG_PATH, "utf-8"));
44
+ } catch {
45
+ return /* @__PURE__ */ new Map();
46
+ }
47
+ }
25
48
 
26
49
  // src/service-manager.ts
27
50
  var moduleDir = path2.dirname(fileURLToPath(import.meta.url));
@@ -104,6 +127,24 @@ function getUiServerStatus() {
104
127
  port: status.port ?? uiPort
105
128
  };
106
129
  }
130
+ function buildDaemonEnv() {
131
+ const env = { ...process.env };
132
+ env.CTI_HOME = CTI_HOME;
133
+ for (const [key, value] of loadRawConfigEnv()) {
134
+ env[key] = value;
135
+ }
136
+ delete env.CLAUDECODE;
137
+ return env;
138
+ }
139
+ async function waitForBridgeRunning(timeoutMs = 2e4) {
140
+ const startedAt = Date.now();
141
+ while (Date.now() - startedAt < timeoutMs) {
142
+ const status = getBridgeStatus();
143
+ if (status.running) return status;
144
+ await sleep(500);
145
+ }
146
+ return getBridgeStatus();
147
+ }
107
148
  async function waitForUiServer(timeoutMs = 15e3) {
108
149
  const startedAt = Date.now();
109
150
  while (Date.now() - startedAt < timeoutMs) {
@@ -119,6 +160,30 @@ async function waitForUiServer(timeoutMs = 15e3) {
119
160
  }
120
161
  return getUiServerStatus();
121
162
  }
163
+ async function startBridge() {
164
+ ensureDirs();
165
+ const current = getBridgeStatus();
166
+ if (current.running) return current;
167
+ const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
168
+ if (!fs2.existsSync(daemonEntry)) {
169
+ throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
170
+ }
171
+ const stdoutFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.out.log"), "a");
172
+ const stderrFd = fs2.openSync(path2.join(logsDir, "bridge-launcher.err.log"), "a");
173
+ const child = spawn(process.execPath, [daemonEntry], {
174
+ cwd: packageRoot,
175
+ detached: true,
176
+ env: buildDaemonEnv(),
177
+ stdio: ["ignore", stdoutFd, stderrFd],
178
+ ...WINDOWS_HIDE
179
+ });
180
+ child.unref();
181
+ const status = await waitForBridgeRunning();
182
+ if (!status.running) {
183
+ throw new Error(status.lastExitReason || "Bridge failed to report running=true.");
184
+ }
185
+ return status;
186
+ }
122
187
  async function stopBridge() {
123
188
  const status = getBridgeStatus();
124
189
  if (!status.pid || !isProcessAlive(status.pid)) {
@@ -238,8 +303,19 @@ async function main() {
238
303
  const status = await ensureUiServerRunning();
239
304
  const url = getUiServerUrl(status.port);
240
305
  openBrowser(url);
241
- process.stdout.write(`Codex to IM is available at ${url}
306
+ try {
307
+ await startBridge();
308
+ process.stdout.write(`Codex to IM is available at ${url}
242
309
  `);
310
+ } catch (error) {
311
+ process.stdout.write(`Codex to IM UI is available at ${url}
312
+ `);
313
+ process.stderr.write(
314
+ `Bridge failed to start. Open the UI and check logs/config first: ${error instanceof Error ? error.message : String(error)}
315
+ `
316
+ );
317
+ process.exitCode = 1;
318
+ }
243
319
  return;
244
320
  }
245
321
  case "url": {
@@ -260,14 +336,6 @@ async function main() {
260
336
  process.exitCode = 1;
261
337
  return;
262
338
  }
263
- case "share-feishu": {
264
- const status = await ensureUiServerRunning();
265
- const url = `${getUiServerUrl(status.port)}/#desktop`;
266
- openBrowser(url);
267
- process.stdout.write(`Opened Feishu handoff entry at ${url}
268
- `);
269
- return;
270
- }
271
339
  case "stop": {
272
340
  const bridge = await stopBridge();
273
341
  const ui = await stopUiServer();
@@ -290,7 +358,7 @@ async function main() {
290
358
  return;
291
359
  }
292
360
  default:
293
- process.stdout.write("Usage: codex-to-im [open|url|share-feishu|stop|status]\n");
361
+ process.stdout.write("Usage: codex-to-im [open|url|stop|status]\n");
294
362
  }
295
363
  }
296
364
  main().catch((error) => {
package/dist/daemon.mjs CHANGED
@@ -4047,7 +4047,9 @@ var init_codex_provider = __esm({
4047
4047
  }
4048
4048
  let sawAnyEvent = false;
4049
4049
  try {
4050
- const { events } = await thread.runStreamed(input);
4050
+ const { events } = await thread.runStreamed(input, {
4051
+ signal: params.abortController?.signal
4052
+ });
4051
4053
  for await (const event of events) {
4052
4054
  sawAnyEvent = true;
4053
4055
  if (params.abortController?.signal.aborted) {
@@ -5118,9 +5120,27 @@ function htmlToFeishuMarkdown(html) {
5118
5120
  }
5119
5121
  function buildToolProgressMarkdown(tools) {
5120
5122
  if (tools.length === 0) return "";
5121
- const lines = tools.map((tc) => {
5122
- const icon = tc.status === "running" ? "\u{1F504}" : tc.status === "complete" ? "\u2705" : "\u274C";
5123
- return `${icon} \`${tc.name}\``;
5123
+ const grouped = /* @__PURE__ */ new Map();
5124
+ for (const tool of tools) {
5125
+ const key = tool.name || "tool";
5126
+ const bucket = grouped.get(key) || { running: 0, complete: 0, error: 0 };
5127
+ if (tool.status === "running") bucket.running += 1;
5128
+ else if (tool.status === "error") bucket.error += 1;
5129
+ else bucket.complete += 1;
5130
+ grouped.set(key, bucket);
5131
+ }
5132
+ const lines = Array.from(grouped.entries()).map(([name, counts]) => {
5133
+ const total = counts.running + counts.complete + counts.error;
5134
+ const icon = counts.running > 0 ? "\u{1F504}" : counts.error > 0 ? "\u274C" : "\u2705";
5135
+ const countSuffix = total > 1 ? ` \xD7${total}` : "";
5136
+ const detailParts = [];
5137
+ if (counts.running > 0) detailParts.push(`\u8FD0\u884C\u4E2D ${counts.running}`);
5138
+ if (counts.error > 0) detailParts.push(`\u5F02\u5E38 ${counts.error}`);
5139
+ if ((counts.running > 0 || counts.error > 0) && counts.complete > 0) {
5140
+ detailParts.push(`\u5B8C\u6210 ${counts.complete}`);
5141
+ }
5142
+ const detailSuffix = detailParts.length > 0 ? `\uFF08${detailParts.join(" / ")}\uFF09` : "";
5143
+ return `${icon} \`${name}\`${countSuffix}${detailSuffix}`;
5124
5144
  });
5125
5145
  return lines.join("\n");
5126
5146
  }
@@ -5253,15 +5273,18 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5253
5273
  lastIncomingMessageId = /* @__PURE__ */ new Map();
5254
5274
  /** Track active typing reaction IDs per chat for cleanup. */
5255
5275
  typingReactions = /* @__PURE__ */ new Map();
5256
- /** Active streaming card state per chatId. */
5276
+ /** Active streaming card state per stream key. */
5257
5277
  activeCards = /* @__PURE__ */ new Map();
5258
- /** In-flight card creation promises per chatId — prevents duplicate creation. */
5278
+ /** In-flight card creation promises per stream key — prevents duplicate creation. */
5259
5279
  cardCreatePromises = /* @__PURE__ */ new Map();
5260
5280
  /** Cached tenant token for upload APIs. */
5261
5281
  tenantTokenCache = null;
5262
5282
  isStreamingEnabled() {
5263
5283
  return getBridgeContext().store.getSetting("bridge_feishu_streaming_enabled") !== "false";
5264
5284
  }
5285
+ resolveStreamKey(chatId, streamKey) {
5286
+ return streamKey?.trim() || chatId;
5287
+ }
5265
5288
  // ── Lifecycle ───────────────────────────────────────────────
5266
5289
  async start() {
5267
5290
  if (this.running) return;
@@ -5366,10 +5389,10 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5366
5389
  * Add a "Typing" emoji reaction to the user's message and create streaming card.
5367
5390
  * Called by bridge-manager via onMessageStart().
5368
5391
  */
5369
- onMessageStart(chatId) {
5392
+ onMessageStart(chatId, streamKey) {
5370
5393
  const messageId = this.lastIncomingMessageId.get(chatId);
5371
5394
  if (messageId && this.isStreamingEnabled()) {
5372
- this.createStreamingCard(chatId, messageId).catch(() => {
5395
+ this.createStreamingCard(chatId, messageId, streamKey).catch(() => {
5373
5396
  });
5374
5397
  }
5375
5398
  if (!messageId || !this.restClient) return;
@@ -5392,8 +5415,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5392
5415
  * Remove the "Typing" emoji reaction and clean up card state.
5393
5416
  * Called by bridge-manager via onMessageEnd().
5394
5417
  */
5395
- onMessageEnd(chatId) {
5396
- this.cleanupCard(chatId);
5418
+ onMessageEnd(chatId, streamKey) {
5419
+ this.cleanupCard(chatId, streamKey);
5397
5420
  const reactionId = this.typingReactions.get(chatId);
5398
5421
  const messageId = this.lastIncomingMessageId.get(chatId);
5399
5422
  if (!reactionId || !messageId || !this.restClient) return;
@@ -5444,17 +5467,19 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5444
5467
  * Create a new streaming card and send it as a message.
5445
5468
  * Returns true if card was created successfully.
5446
5469
  */
5447
- createStreamingCard(chatId, replyToMessageId) {
5448
- if (!this.restClient || this.activeCards.has(chatId)) return Promise.resolve(false);
5449
- const existing = this.cardCreatePromises.get(chatId);
5470
+ createStreamingCard(chatId, replyToMessageId, streamKey) {
5471
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5472
+ if (!this.restClient || this.activeCards.has(cardKey)) return Promise.resolve(false);
5473
+ const existing = this.cardCreatePromises.get(cardKey);
5450
5474
  if (existing) return existing;
5451
- const promise = this._doCreateStreamingCard(chatId, replyToMessageId);
5452
- this.cardCreatePromises.set(chatId, promise);
5453
- promise.finally(() => this.cardCreatePromises.delete(chatId));
5475
+ const promise = this._doCreateStreamingCard(chatId, replyToMessageId, cardKey);
5476
+ this.cardCreatePromises.set(cardKey, promise);
5477
+ promise.finally(() => this.cardCreatePromises.delete(cardKey));
5454
5478
  return promise;
5455
5479
  }
5456
- async _doCreateStreamingCard(chatId, replyToMessageId) {
5480
+ async _doCreateStreamingCard(chatId, replyToMessageId, streamKey) {
5457
5481
  if (!this.restClient) return false;
5482
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5458
5483
  const cardkit = this.restClient.cardkit?.v1;
5459
5484
  if (!cardkit?.card) {
5460
5485
  console.warn("[feishu-adapter] CardKit v1 API is unavailable in the current Feishu SDK client");
@@ -5508,7 +5533,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5508
5533
  console.warn("[feishu-adapter] Card message send returned no message_id");
5509
5534
  return false;
5510
5535
  }
5511
- this.activeCards.set(chatId, {
5536
+ this.activeCards.set(cardKey, {
5537
+ chatId,
5512
5538
  cardId,
5513
5539
  messageId,
5514
5540
  sequence: 0,
@@ -5519,7 +5545,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5519
5545
  lastUpdateAt: 0,
5520
5546
  throttleTimer: null
5521
5547
  });
5522
- console.log(`[feishu-adapter] Streaming card created: cardId=${cardId}, msgId=${messageId}`);
5548
+ console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
5523
5549
  return true;
5524
5550
  } catch (err) {
5525
5551
  console.warn("[feishu-adapter] Failed to create streaming card:", err instanceof Error ? err.message : err);
@@ -5529,8 +5555,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5529
5555
  /**
5530
5556
  * Update streaming card content with throttling.
5531
5557
  */
5532
- updateCardContent(chatId, text2) {
5533
- const state = this.activeCards.get(chatId);
5558
+ updateCardContent(chatId, text2, streamKey) {
5559
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5560
+ const state = this.activeCards.get(cardKey);
5534
5561
  if (!state || !this.restClient) return;
5535
5562
  if (state.thinking && text2.trim()) {
5536
5563
  state.thinking = false;
@@ -5541,7 +5568,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5541
5568
  if (!state.throttleTimer) {
5542
5569
  state.throttleTimer = setTimeout(() => {
5543
5570
  state.throttleTimer = null;
5544
- this.flushCardUpdate(chatId);
5571
+ this.flushCardUpdate(cardKey);
5545
5572
  }, CARD_THROTTLE_MS - elapsed);
5546
5573
  }
5547
5574
  return;
@@ -5550,13 +5577,13 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5550
5577
  clearTimeout(state.throttleTimer);
5551
5578
  state.throttleTimer = null;
5552
5579
  }
5553
- this.flushCardUpdate(chatId);
5580
+ this.flushCardUpdate(cardKey);
5554
5581
  }
5555
5582
  /**
5556
5583
  * Flush pending card update to Feishu API.
5557
5584
  */
5558
- flushCardUpdate(chatId) {
5559
- const state = this.activeCards.get(chatId);
5585
+ flushCardUpdate(streamKey) {
5586
+ const state = this.activeCards.get(streamKey);
5560
5587
  if (!state || !this.restClient) return;
5561
5588
  const cardkit = this.restClient.cardkit?.v1;
5562
5589
  if (!cardkit?.cardElement?.content) return;
@@ -5576,24 +5603,26 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5576
5603
  /**
5577
5604
  * Update tool progress in the streaming card.
5578
5605
  */
5579
- updateToolProgress(chatId, tools) {
5580
- const state = this.activeCards.get(chatId);
5606
+ updateToolProgress(chatId, tools, streamKey) {
5607
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5608
+ const state = this.activeCards.get(cardKey);
5581
5609
  if (!state) return;
5582
5610
  state.toolCalls = tools;
5583
- this.updateCardContent(chatId, state.pendingText || "");
5611
+ this.updateCardContent(chatId, state.pendingText || "", cardKey);
5584
5612
  }
5585
5613
  /**
5586
5614
  * Finalize the streaming card: close streaming mode, update with final content + footer.
5587
5615
  */
5588
- async finalizeCard(chatId, status, responseText) {
5589
- const pending = this.cardCreatePromises.get(chatId);
5616
+ async finalizeCard(chatId, status, responseText, streamKey) {
5617
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5618
+ const pending = this.cardCreatePromises.get(cardKey);
5590
5619
  if (pending) {
5591
5620
  try {
5592
5621
  await pending;
5593
5622
  } catch {
5594
5623
  }
5595
5624
  }
5596
- const state = this.activeCards.get(chatId);
5625
+ const state = this.activeCards.get(cardKey);
5597
5626
  if (!state || !this.restClient) return false;
5598
5627
  const cardkit = this.restClient.cardkit?.v1;
5599
5628
  if (!cardkit?.card?.settings || !cardkit?.card?.update) return false;
@@ -5629,63 +5658,66 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5629
5658
  sequence: state.sequence
5630
5659
  }
5631
5660
  });
5632
- console.log(`[feishu-adapter] Card finalized: cardId=${state.cardId}, status=${status}, elapsed=${formatElapsed(elapsedMs)}`);
5661
+ console.log(`[feishu-adapter] Card finalized: streamKey=${cardKey}, cardId=${state.cardId}, status=${status}, elapsed=${formatElapsed(elapsedMs)}`);
5633
5662
  return true;
5634
5663
  } catch (err) {
5635
5664
  console.warn("[feishu-adapter] Card finalize failed:", err instanceof Error ? err.message : err);
5636
5665
  return false;
5637
5666
  } finally {
5638
- this.activeCards.delete(chatId);
5667
+ this.activeCards.delete(cardKey);
5639
5668
  }
5640
5669
  }
5641
5670
  /**
5642
5671
  * Clean up card state without finalizing (e.g. on unexpected errors).
5643
5672
  */
5644
- cleanupCard(chatId) {
5645
- this.cardCreatePromises.delete(chatId);
5646
- const state = this.activeCards.get(chatId);
5673
+ cleanupCard(chatId, streamKey) {
5674
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5675
+ this.cardCreatePromises.delete(cardKey);
5676
+ const state = this.activeCards.get(cardKey);
5647
5677
  if (!state) return;
5648
5678
  if (state.throttleTimer) {
5649
5679
  clearTimeout(state.throttleTimer);
5650
5680
  }
5651
- this.activeCards.delete(chatId);
5681
+ this.activeCards.delete(cardKey);
5652
5682
  }
5653
5683
  /**
5654
5684
  * Check if there is an active streaming card for a given chat.
5655
5685
  */
5656
- hasActiveCard(chatId) {
5657
- return this.activeCards.has(chatId);
5686
+ hasActiveCard(chatId, streamKey) {
5687
+ return this.activeCards.has(this.resolveStreamKey(chatId, streamKey));
5658
5688
  }
5659
5689
  // ── Streaming adapter interface ────────────────────────────────
5660
5690
  /**
5661
5691
  * Called by bridge-manager on each text SSE event.
5662
5692
  * Creates streaming card on first call, then updates content.
5663
5693
  */
5664
- onStreamText(chatId, fullText) {
5694
+ onStreamText(chatId, fullText, streamKey) {
5665
5695
  if (!this.isStreamingEnabled()) return;
5666
- if (!this.activeCards.has(chatId)) {
5696
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5697
+ if (!this.activeCards.has(cardKey)) {
5667
5698
  const messageId = this.lastIncomingMessageId.get(chatId);
5668
- this.createStreamingCard(chatId, messageId).then((ok) => {
5669
- if (ok) this.updateCardContent(chatId, fullText);
5699
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
5700
+ if (ok) this.updateCardContent(chatId, fullText, cardKey);
5670
5701
  }).catch(() => {
5671
5702
  });
5672
5703
  return;
5673
5704
  }
5674
- this.updateCardContent(chatId, fullText);
5705
+ this.updateCardContent(chatId, fullText, cardKey);
5675
5706
  }
5676
- onMirrorStreamStart(chatId) {
5707
+ onMirrorStreamStart(chatId, streamKey) {
5677
5708
  if (!this.isStreamingEnabled()) return;
5678
- if (this.activeCards.has(chatId)) return;
5679
- this.createStreamingCard(chatId, void 0).catch(() => {
5709
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5710
+ if (this.activeCards.has(cardKey)) return;
5711
+ this.createStreamingCard(chatId, void 0, cardKey).catch(() => {
5680
5712
  });
5681
5713
  }
5682
- onToolEvent(chatId, tools) {
5714
+ onToolEvent(chatId, tools, streamKey) {
5683
5715
  if (!this.isStreamingEnabled()) return;
5684
- this.updateToolProgress(chatId, tools);
5716
+ this.updateToolProgress(chatId, tools, streamKey);
5685
5717
  }
5686
- async onStreamEnd(chatId, status, responseText) {
5718
+ async onStreamEnd(chatId, status, responseText, streamKey) {
5687
5719
  if (!this.isStreamingEnabled()) return false;
5688
- return this.finalizeCard(chatId, status, responseText);
5720
+ return this.finalizeCard(chatId, status, responseText, streamKey);
5689
5721
  }
5690
5722
  // ── Send ────────────────────────────────────────────────────
5691
5723
  async send(message) {
@@ -16800,8 +16832,6 @@ function resolveCommandAlias(rawCommand, args) {
16800
16832
  return "/help";
16801
16833
  case "/t":
16802
16834
  return !args ? "/threads" : /^(all|n\b)/i.test(args.trim()) ? "/threads" : "/thread";
16803
- case "/s":
16804
- return args ? "/use" : "/sessions";
16805
16835
  case "/n":
16806
16836
  return "/new";
16807
16837
  case "/m":
@@ -16872,6 +16902,12 @@ function getSessionDisplayName(session, fallbackDirectory) {
16872
16902
  function nowIso() {
16873
16903
  return (/* @__PURE__ */ new Date()).toISOString();
16874
16904
  }
16905
+ function buildInteractiveStreamKey(sessionId, messageId) {
16906
+ return `im:${sessionId}:${messageId}`;
16907
+ }
16908
+ function buildMirrorStreamKey(sessionId, turnId, startedAt) {
16909
+ return `mirror:${sessionId}:${turnId || startedAt}`;
16910
+ }
16875
16911
  function getWorkspaceRoot() {
16876
16912
  const { store } = getBridgeContext();
16877
16913
  return store.getSetting("bridge_default_workspace_root") || DEFAULT_WORKSPACE_ROOT;
@@ -17811,6 +17847,16 @@ function formatMirrorMessage(threadTitle, userText, assistantText, markdown = fa
17811
17847
  sections.unshift(buildMirrorTitle(threadTitle, markdown));
17812
17848
  return sections.join("\n\n").trim();
17813
17849
  }
17850
+ function buildMirrorTimeoutNotice(markdown = false) {
17851
+ return markdown ? "> \u8D85\u65F6\u63D0\u9192\uFF1A\u957F\u65F6\u95F4\u6CA1\u6709\u6536\u5230\u65B0\u7684\u684C\u9762\u4F1A\u8BDD\u8F93\u51FA\uFF0C\u672C\u6B21\u6D41\u5F0F\u540C\u6B65\u5DF2\u5148\u7ED3\u675F\uFF1B\u5982\u679C\u684C\u9762\u540E\u7EED\u7EE7\u7EED\u4EA7\u51FA\u5185\u5BB9\uFF0C\u4F1A\u91CD\u65B0\u5F00\u59CB\u65B0\u4E00\u8F6E\u540C\u6B65\u3002" : "\u8D85\u65F6\u63D0\u9192\uFF1A\u957F\u65F6\u95F4\u6CA1\u6709\u6536\u5230\u65B0\u7684\u684C\u9762\u4F1A\u8BDD\u8F93\u51FA\uFF0C\u672C\u6B21\u6D41\u5F0F\u540C\u6B65\u5DF2\u5148\u7ED3\u675F\uFF1B\u5982\u679C\u684C\u9762\u540E\u7EED\u7EE7\u7EED\u4EA7\u51FA\u5185\u5BB9\uFF0C\u4F1A\u91CD\u65B0\u5F00\u59CB\u65B0\u4E00\u8F6E\u540C\u6B65\u3002";
17852
+ }
17853
+ function appendMirrorTimeoutNotice(text2, markdown = false) {
17854
+ const notice = buildMirrorTimeoutNotice(markdown);
17855
+ const normalized = text2.trim();
17856
+ return normalized ? `${normalized}
17857
+
17858
+ ${notice}` : notice;
17859
+ }
17814
17860
  function getMirrorStreamingAdapter(subscription) {
17815
17861
  const state = getState();
17816
17862
  const adapter = state.adapters.get(subscription.channelType);
@@ -17837,9 +17883,9 @@ function startMirrorStreaming(subscription, turnState) {
17837
17883
  const adapter = getMirrorStreamingAdapter(subscription);
17838
17884
  if (!adapter || turnState.streamStarted) return;
17839
17885
  try {
17840
- adapter.onMirrorStreamStart?.(subscription.chatId);
17886
+ adapter.onMirrorStreamStart?.(subscription.chatId, turnState.streamKey);
17841
17887
  if (!adapter.onMirrorStreamStart) {
17842
- adapter.onStreamText?.(subscription.chatId, "");
17888
+ adapter.onStreamText?.(subscription.chatId, "", turnState.streamKey);
17843
17889
  }
17844
17890
  turnState.streamStarted = true;
17845
17891
  } catch {
@@ -17855,7 +17901,7 @@ function updateMirrorStreaming(subscription, turnState) {
17855
17901
  );
17856
17902
  if (!text2) return;
17857
17903
  try {
17858
- adapter.onStreamText?.(subscription.chatId, text2);
17904
+ adapter.onStreamText?.(subscription.chatId, text2, turnState.streamKey);
17859
17905
  } catch {
17860
17906
  }
17861
17907
  }
@@ -17864,7 +17910,7 @@ function updateMirrorToolProgress(subscription, turnState) {
17864
17910
  if (!adapter || typeof adapter.onToolEvent !== "function") return;
17865
17911
  startMirrorStreaming(subscription, turnState);
17866
17912
  try {
17867
- adapter.onToolEvent(subscription.chatId, Array.from(turnState.toolCalls.values()));
17913
+ adapter.onToolEvent(subscription.chatId, Array.from(turnState.toolCalls.values()), turnState.streamKey);
17868
17914
  } catch {
17869
17915
  }
17870
17916
  }
@@ -17876,7 +17922,7 @@ function stopMirrorStreaming(subscription, status = "interrupted") {
17876
17922
  subscription.channelType,
17877
17923
  getMirrorStreamingText(subscription, pendingTurn)
17878
17924
  );
17879
- void adapter.onStreamEnd(subscription.chatId, status, text2).catch(() => {
17925
+ void adapter.onStreamEnd(subscription.chatId, status, text2, pendingTurn.streamKey).catch(() => {
17880
17926
  });
17881
17927
  }
17882
17928
  async function deliverMirrorTurn(subscription, turn) {
@@ -17886,8 +17932,10 @@ async function deliverMirrorTurn(subscription, turn) {
17886
17932
  const title = getDesktopThreadTitle(subscription.threadId)?.trim() || "\u684C\u9762\u7EBF\u7A0B";
17887
17933
  const responseParseMode = getFeedbackParseMode(subscription.channelType);
17888
17934
  const markdown = responseParseMode === "Markdown";
17889
- const renderedText = formatMirrorMessage(title, turn.userText, turn.text, markdown);
17890
- const renderedStreamText = formatMirrorMessage(title, turn.userText, turn.text, markdown, true);
17935
+ const renderedTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown);
17936
+ const renderedStreamTextBase = formatMirrorMessage(title, turn.userText, turn.text, markdown, true);
17937
+ const renderedText = turn.timedOut ? appendMirrorTimeoutNotice(renderedTextBase || buildMirrorTitle(title, markdown), markdown) : renderedTextBase;
17938
+ const renderedStreamText = turn.timedOut ? appendMirrorTimeoutNotice(renderedStreamTextBase || buildMirrorTitle(title, markdown), markdown) : renderedStreamTextBase;
17891
17939
  const text2 = renderedText ? renderFeedbackText(renderedText, responseParseMode) : "";
17892
17940
  const streamText = renderFeedbackText(
17893
17941
  renderedStreamText || buildMirrorTitle(title, markdown),
@@ -17898,7 +17946,8 @@ async function deliverMirrorTurn(subscription, turn) {
17898
17946
  const finalized = await adapter.onStreamEnd(
17899
17947
  subscription.chatId,
17900
17948
  turn.status,
17901
- streamText
17949
+ streamText,
17950
+ turn.streamKey
17902
17951
  );
17903
17952
  if (finalized) {
17904
17953
  subscription.lastDeliveredAt = turn.timestamp || nowIso();
@@ -17930,10 +17979,11 @@ async function deliverMirrorTurns(subscription, turns) {
17930
17979
  await deliverMirrorTurn(subscription, turn);
17931
17980
  }
17932
17981
  }
17933
- function createMirrorTurnState(timestamp, turnId) {
17982
+ function createMirrorTurnState(sessionId, timestamp, turnId) {
17934
17983
  const safeTimestamp = timestamp || nowIso();
17935
17984
  return {
17936
17985
  turnId: turnId || null,
17986
+ streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
17937
17987
  startedAt: safeTimestamp,
17938
17988
  lastActivityAt: safeTimestamp,
17939
17989
  userText: null,
@@ -17967,7 +18017,7 @@ ${normalized}` : normalized;
17967
18017
  }
17968
18018
  function ensureMirrorTurnState(subscription, record) {
17969
18019
  if (!subscription.pendingTurn) {
17970
- subscription.pendingTurn = createMirrorTurnState(record.timestamp, record.turnId);
18020
+ subscription.pendingTurn = createMirrorTurnState(subscription.sessionId, record.timestamp, record.turnId);
17971
18021
  return subscription.pendingTurn;
17972
18022
  }
17973
18023
  if (!subscription.pendingTurn.turnId && record.turnId) {
@@ -17990,11 +18040,13 @@ function finalizeMirrorTurn(subscription, signature, timestamp, status, preferre
17990
18040
  const userText = pendingTurn.userText?.trim() || null;
17991
18041
  if (!text2 && !userText && pendingTurn.toolCalls.size === 0) return null;
17992
18042
  return {
18043
+ streamKey: pendingTurn.streamKey,
17993
18044
  userText,
17994
18045
  text: text2,
17995
18046
  signature,
17996
18047
  timestamp: timestamp || pendingTurn.lastActivityAt || nowIso(),
17997
- status
18048
+ status,
18049
+ ...signature.startsWith("timeout:") ? { timedOut: true } : {}
17998
18050
  };
17999
18051
  }
18000
18052
  function consumeMirrorRecords(subscription, records) {
@@ -18008,7 +18060,7 @@ function consumeMirrorRecords(subscription, records) {
18008
18060
  if (superseded) finalized.push(superseded);
18009
18061
  }
18010
18062
  if (!subscription.pendingTurn) {
18011
- subscription.pendingTurn = createMirrorTurnState(record.timestamp, record.turnId);
18063
+ subscription.pendingTurn = createMirrorTurnState(subscription.sessionId, record.timestamp, record.turnId);
18012
18064
  } else {
18013
18065
  if (!subscription.pendingTurn.turnId && record.turnId) {
18014
18066
  subscription.pendingTurn.turnId = record.turnId;
@@ -18287,11 +18339,21 @@ async function reconcileMirrorSubscription(subscription) {
18287
18339
  subscription.bufferedRecords.push(...filteredRecords);
18288
18340
  }
18289
18341
  }
18342
+ const timedOutTurn = flushTimedOutMirrorTurn(subscription);
18290
18343
  if (getState().activeTasks.has(subscription.sessionId) || isMirrorSuppressed(subscription.sessionId)) {
18344
+ if (timedOutTurn) {
18345
+ try {
18346
+ await deliverMirrorTurns(subscription, [timedOutTurn]);
18347
+ } catch (error) {
18348
+ subscription.dirty = true;
18349
+ console.warn("[bridge-manager] Mirror delivery failed:", error instanceof Error ? error.message : error);
18350
+ }
18351
+ }
18291
18352
  syncMirrorSessionState(subscription.sessionId);
18292
18353
  return;
18293
18354
  }
18294
- const finalizedTurns = consumeBufferedMirrorTurns(subscription);
18355
+ const finalizedTurns = timedOutTurn ? [timedOutTurn] : [];
18356
+ finalizedTurns.push(...consumeBufferedMirrorTurns(subscription));
18295
18357
  if (finalizedTurns.length === 0) {
18296
18358
  syncMirrorSessionState(subscription.sessionId);
18297
18359
  return;
@@ -18694,7 +18756,8 @@ async function handleMessage(adapter, msg) {
18694
18756
  return;
18695
18757
  }
18696
18758
  const binding = resolve(msg.address);
18697
- adapter.onMessageStart?.(msg.address.chatId);
18759
+ const streamKey = buildInteractiveStreamKey(binding.codepilotSessionId, msg.messageId);
18760
+ adapter.onMessageStart?.(msg.address.chatId, streamKey);
18698
18761
  const taskAbort = new AbortController();
18699
18762
  const state = getState();
18700
18763
  resetMirrorSessionForInteractiveRun(binding.codepilotSessionId);
@@ -18755,7 +18818,7 @@ async function handleMessage(adapter, msg) {
18755
18818
  stripOutboundArtifactBlocksForStreaming(fullText)
18756
18819
  );
18757
18820
  try {
18758
- adapter.onStreamText(msg.address.chatId, rendered);
18821
+ adapter.onStreamText(msg.address.chatId, rendered, streamKey);
18759
18822
  } catch {
18760
18823
  }
18761
18824
  } : void 0;
@@ -18767,7 +18830,7 @@ async function handleMessage(adapter, msg) {
18767
18830
  if (existing) existing.status = status;
18768
18831
  }
18769
18832
  try {
18770
- adapter.onToolEvent(msg.address.chatId, Array.from(toolCallTracker.values()));
18833
+ adapter.onToolEvent(msg.address.chatId, Array.from(toolCallTracker.values()), streamKey);
18771
18834
  } catch {
18772
18835
  }
18773
18836
  } : void 0;
@@ -18800,7 +18863,8 @@ async function handleMessage(adapter, msg) {
18800
18863
  cardFinalized = await adapter.onStreamEnd(
18801
18864
  msg.address.chatId,
18802
18865
  status,
18803
- renderFeedbackTextForChannel(adapter.channelType, result.responseText)
18866
+ renderFeedbackTextForChannel(adapter.channelType, result.responseText),
18867
+ streamKey
18804
18868
  );
18805
18869
  } catch (err) {
18806
18870
  console.warn("[bridge-manager] Card finalize failed:", err instanceof Error ? err.message : err);
@@ -18845,7 +18909,7 @@ async function handleMessage(adapter, msg) {
18845
18909
  }
18846
18910
  if (hasStreamingCards && adapter.onStreamEnd && taskAbort.signal.aborted) {
18847
18911
  try {
18848
- await adapter.onStreamEnd(msg.address.chatId, "interrupted", "");
18912
+ await adapter.onStreamEnd(msg.address.chatId, "interrupted", "", streamKey);
18849
18913
  } catch {
18850
18914
  }
18851
18915
  }
@@ -18854,7 +18918,7 @@ async function handleMessage(adapter, msg) {
18854
18918
  }
18855
18919
  state.activeTasks.delete(binding.codepilotSessionId);
18856
18920
  syncSessionRuntimeState(binding.codepilotSessionId);
18857
- adapter.onMessageEnd?.(msg.address.chatId);
18921
+ adapter.onMessageEnd?.(msg.address.chatId, streamKey);
18858
18922
  ack();
18859
18923
  }
18860
18924
  }
@@ -18912,8 +18976,6 @@ async function handleCommand(adapter, msg, text2) {
18912
18976
  const oldTask = st.activeTasks.get(currentBinding.codepilotSessionId);
18913
18977
  if (oldTask) {
18914
18978
  oldTask.abort();
18915
- st.activeTasks.delete(currentBinding.codepilotSessionId);
18916
- syncSessionRuntimeState(currentBinding.codepilotSessionId);
18917
18979
  }
18918
18980
  }
18919
18981
  const workDir = resolved.workDir;
@@ -19359,8 +19421,6 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
19359
19421
  const taskAbort = st.activeTasks.get(binding.codepilotSessionId);
19360
19422
  if (taskAbort) {
19361
19423
  taskAbort.abort();
19362
- st.activeTasks.delete(binding.codepilotSessionId);
19363
- syncSessionRuntimeState(binding.codepilotSessionId);
19364
19424
  response = "\u6B63\u5728\u505C\u6B62\u5F53\u524D\u4EFB\u52A1...";
19365
19425
  } else {
19366
19426
  response = "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
@@ -5045,7 +5045,9 @@ var CodexProvider = class {
5045
5045
  }
5046
5046
  let sawAnyEvent = false;
5047
5047
  try {
5048
- const { events } = await thread.runStreamed(input);
5048
+ const { events } = await thread.runStreamed(input, {
5049
+ signal: params.abortController?.signal
5050
+ });
5049
5051
  for await (const event of events) {
5050
5052
  sawAnyEvent = true;
5051
5053
  if (params.abortController?.signal.aborted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",