codex-to-im 1.0.43 → 1.0.44

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
@@ -1408,8 +1408,12 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1408
1408
  botIds = /* @__PURE__ */ new Set();
1409
1409
  /** Track last incoming message ID per chat for typing indicator. */
1410
1410
  lastIncomingMessageId = /* @__PURE__ */ new Map();
1411
- /** Track active typing reaction IDs per chat for cleanup. */
1411
+ /** Track active typing reaction IDs per stream key for cleanup. */
1412
1412
  typingReactions = /* @__PURE__ */ new Map();
1413
+ /** Track in-flight typing reaction creates so repeated status updates stay idempotent. */
1414
+ typingReactionCreatePromises = /* @__PURE__ */ new Map();
1415
+ /** Track streams that ended before the async reaction create finished. */
1416
+ typingReactionCleanupRequested = /* @__PURE__ */ new Set();
1413
1417
  /** Active streaming card state per stream key. */
1414
1418
  activeCards = /* @__PURE__ */ new Map();
1415
1419
  /** In-flight card creation promises per stream key — prevents duplicate creation. */
@@ -1515,6 +1519,8 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1515
1519
  this.seenMessageIds.clear();
1516
1520
  this.lastIncomingMessageId.clear();
1517
1521
  this.typingReactions.clear();
1522
+ this.typingReactionCreatePromises.clear();
1523
+ this.typingReactionCleanupRequested.clear();
1518
1524
  console.log("[feishu-adapter] Stopped");
1519
1525
  }
1520
1526
  isRunning() {
@@ -1531,25 +1537,38 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1531
1537
  */
1532
1538
  onMessageStart(chatId, streamKey) {
1533
1539
  const messageId = this.lastIncomingMessageId.get(chatId);
1540
+ const reactionKey = this.resolveStreamKey(chatId, streamKey);
1534
1541
  if (messageId && this.isStreamingEnabled()) {
1535
1542
  this.createStreamingCard(chatId, messageId, streamKey).catch(() => {
1536
1543
  });
1537
1544
  }
1538
1545
  if (!messageId || !this.restClient) return;
1539
- this.restClient.im.messageReaction.create({
1546
+ if (this.typingReactions.has(reactionKey) || this.typingReactionCreatePromises.has(reactionKey)) {
1547
+ return;
1548
+ }
1549
+ const createPromise = this.restClient.im.messageReaction.create({
1540
1550
  path: { message_id: messageId },
1541
1551
  data: { reaction_type: { emoji_type: TYPING_EMOJI } }
1542
1552
  }).then((res) => {
1543
1553
  const reactionId = res?.data?.reaction_id;
1544
1554
  if (reactionId) {
1545
- this.typingReactions.set(chatId, reactionId);
1555
+ this.typingReactions.set(reactionKey, { messageId, reactionId });
1556
+ if (this.typingReactionCleanupRequested.delete(reactionKey)) {
1557
+ this.removeTypingReaction(reactionKey);
1558
+ }
1546
1559
  }
1547
1560
  }).catch((err) => {
1548
1561
  const code2 = err?.code;
1549
1562
  if (code2 !== 99991400 && code2 !== 99991403) {
1550
1563
  console.warn("[feishu-adapter] Typing indicator failed:", err instanceof Error ? err.message : err);
1551
1564
  }
1565
+ }).finally(() => {
1566
+ this.typingReactionCreatePromises.delete(reactionKey);
1567
+ if (!this.typingReactions.has(reactionKey)) {
1568
+ this.typingReactionCleanupRequested.delete(reactionKey);
1569
+ }
1552
1570
  });
1571
+ this.typingReactionCreatePromises.set(reactionKey, createPromise);
1553
1572
  }
1554
1573
  /**
1555
1574
  * Remove the "Typing" emoji reaction and clean up card state.
@@ -1557,12 +1576,19 @@ var FeishuAdapter = class extends BaseChannelAdapter {
1557
1576
  */
1558
1577
  onMessageEnd(chatId, streamKey) {
1559
1578
  this.cleanupCard(chatId, streamKey);
1560
- const reactionId = this.typingReactions.get(chatId);
1561
- const messageId = this.lastIncomingMessageId.get(chatId);
1562
- if (!reactionId || !messageId || !this.restClient) return;
1563
- this.typingReactions.delete(chatId);
1579
+ const reactionKey = this.resolveStreamKey(chatId, streamKey);
1580
+ if (this.typingReactionCreatePromises.has(reactionKey) && !this.typingReactions.has(reactionKey)) {
1581
+ this.typingReactionCleanupRequested.add(reactionKey);
1582
+ return;
1583
+ }
1584
+ this.removeTypingReaction(reactionKey);
1585
+ }
1586
+ removeTypingReaction(reactionKey) {
1587
+ const reaction = this.typingReactions.get(reactionKey);
1588
+ if (!reaction || !this.restClient) return;
1589
+ this.typingReactions.delete(reactionKey);
1564
1590
  this.restClient.im.messageReaction.delete({
1565
- path: { message_id: messageId, reaction_id: reactionId }
1591
+ path: { message_id: reaction.messageId, reaction_id: reaction.reactionId }
1566
1592
  }).catch(() => {
1567
1593
  });
1568
1594
  }
@@ -11526,6 +11552,7 @@ function buildHealthCommandResponse(title, diagnosis, markdown = false) {
11526
11552
  title,
11527
11553
  [
11528
11554
  ["Session", diagnosis.sessionId],
11555
+ ["\u68C0\u67E5\u65F6\u95F4", formatCommandTimestamp(diagnosis.checkedAt)],
11529
11556
  ["\u8FD0\u884C\u72B6\u6001", formatRuntimeStatus({ runtime_status: diagnosis.runtimeStatus, queued_count: 0 })],
11530
11557
  ["\u5065\u5EB7\u72B6\u6001", formatHealthStatusLabel(diagnosis.healthStatus)],
11531
11558
  ["\u5F53\u524D\u9636\u6BB5", currentStage],
@@ -11546,6 +11573,7 @@ function buildHealthListResponse(diagnoses, markdown = false) {
11546
11573
  diagnoses.map((diagnosis) => ({
11547
11574
  heading: diagnosis.sessionId,
11548
11575
  details: [
11576
+ `\u68C0\u67E5\u65F6\u95F4\uFF1A${formatCommandTimestamp(diagnosis.checkedAt)}`,
11549
11577
  `\u5065\u5EB7\u72B6\u6001\uFF1A${formatHealthStatusLabel(diagnosis.healthStatus)}`,
11550
11578
  `\u5F53\u524D\u9636\u6BB5\uFF1A${diagnosis.activeToolName ? `\u5DE5\u5177 \xB7 ${diagnosis.activeToolName}` : diagnosis.lastProgressType || "-"}`,
11551
11579
  `\u6700\u540E\u8FDB\u5C55\uFF1A${formatCommandTimestamp(diagnosis.lastProgressAt)}`,
@@ -13214,14 +13242,15 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
13214
13242
  break;
13215
13243
  }
13216
13244
  const binding = currentBinding || resolve(msg.address);
13217
- const targetSessionId = args.trim() || binding.codepilotSessionId;
13245
+ const explicitTargetSessionId = args.trim();
13246
+ const targetSessionId = explicitTargetSessionId || binding.codepilotSessionId;
13218
13247
  const diagnosis = await deps.diagnoseSessionHealth(targetSessionId);
13219
13248
  if (!diagnosis) {
13220
13249
  response = `\u6CA1\u6709\u627E\u5230\u4F1A\u8BDD ${targetSessionId}\u3002`;
13221
13250
  break;
13222
13251
  }
13223
13252
  response = buildHealthCommandResponse(
13224
- "\u5F53\u524D\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5",
13253
+ explicitTargetSessionId ? "\u6307\u5B9A\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5" : "\u5F53\u524D\u4F1A\u8BDD\u5065\u5EB7\u68C0\u67E5",
13225
13254
  diagnosis,
13226
13255
  responseParseMode === "Markdown"
13227
13256
  );
@@ -13961,10 +13990,31 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13961
13990
  1e3,
13962
13991
  deps.streamStatusHeartbeatMs ?? structuredStreamStatusConfig.heartbeatMs
13963
13992
  );
13964
- adapter.onMessageStart?.(msg.address.chatId, streamKey);
13993
+ let messageStartCalled = false;
13994
+ const ensureMessageStarted = () => {
13995
+ if (messageStartCalled) return;
13996
+ adapter.onMessageStart?.(msg.address.chatId, streamKey);
13997
+ messageStartCalled = true;
13998
+ };
13999
+ ensureMessageStarted();
13965
14000
  const taskAbort = new AbortController();
13966
14001
  const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
13967
14002
  const taskStartedAt = nowMs();
14003
+ let externalTerminalRequest = null;
14004
+ let resolveExternalTerminal = null;
14005
+ const externalTerminalPromise = new Promise((resolve2) => {
14006
+ resolveExternalTerminal = resolve2;
14007
+ });
14008
+ let resolveExternalTerminalCompletion = null;
14009
+ let externalTerminalCompletionSettled = false;
14010
+ const externalTerminalCompletion = new Promise((resolve2) => {
14011
+ resolveExternalTerminalCompletion = resolve2;
14012
+ });
14013
+ const settleExternalTerminalCompletion = (finalized) => {
14014
+ if (!externalTerminalRequest || externalTerminalCompletionSettled) return;
14015
+ externalTerminalCompletionSettled = true;
14016
+ resolveExternalTerminalCompletion?.(finalized);
14017
+ };
13968
14018
  deps.resetMirrorSessionForInteractiveRun(binding.codepilotSessionId);
13969
14019
  const taskState = {
13970
14020
  id: taskId,
@@ -13978,10 +14028,19 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
13978
14028
  structuredStreamUiActive: false,
13979
14029
  lastActivityAt: taskStartedAt,
13980
14030
  lastResponseAt: null,
13981
- idleReminderSent: false,
13982
14031
  streamFinalized: false,
13983
14032
  uiEnded: false,
13984
- mirrorSuppressionId: null
14033
+ mirrorSuppressionId: null,
14034
+ finalizeFromExternalTerminal: async (outcome, detail, finalText) => {
14035
+ if (externalTerminalRequest) return externalTerminalCompletion;
14036
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return false;
14037
+ externalTerminalRequest = { outcome, detail, finalText };
14038
+ resolveExternalTerminal?.(externalTerminalRequest);
14039
+ if (!taskAbort.signal.aborted) {
14040
+ taskAbort.abort();
14041
+ }
14042
+ return externalTerminalCompletion;
14043
+ }
13985
14044
  };
13986
14045
  deps.registerInteractiveTask(taskState);
13987
14046
  deps.recordInteractiveHealthStart(binding.codepilotSessionId);
@@ -14040,7 +14099,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14040
14099
  chatId: msg.address.chatId,
14041
14100
  streamKey,
14042
14101
  ensureStarted: () => {
14043
- adapter.onMessageStart?.(msg.address.chatId, streamKey);
14102
+ ensureMessageStarted();
14044
14103
  }
14045
14104
  };
14046
14105
  const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
@@ -14083,6 +14142,44 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14083
14142
  streamStatusUpdatesClosed = true;
14084
14143
  clearStreamStatusHeartbeat();
14085
14144
  };
14145
+ let structuredStreamInactiveRecorded = false;
14146
+ const recordStructuredStreamInactiveOnce = () => {
14147
+ if (structuredStreamInactiveRecorded) return;
14148
+ structuredStreamInactiveRecorded = true;
14149
+ taskState.structuredStreamUiActive = false;
14150
+ deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
14151
+ };
14152
+ let previewEnded = false;
14153
+ const endPreviewOnce = () => {
14154
+ if (previewEnded) return;
14155
+ previewEnded = true;
14156
+ if (!previewState) return;
14157
+ if (previewState.throttleTimer) {
14158
+ clearTimeout(previewState.throttleTimer);
14159
+ previewState.throttleTimer = null;
14160
+ }
14161
+ adapter.endPreview?.(msg.address.chatId, previewState.draftId);
14162
+ };
14163
+ let streamUiFinalizeAttempted = false;
14164
+ const finalizeStreamUiOnce = async (status, responseText) => {
14165
+ stopStructuredStreamStatusUpdates();
14166
+ recordStructuredStreamInactiveOnce();
14167
+ endPreviewOnce();
14168
+ if (hasStreamingCards && !streamUiFinalizeAttempted) {
14169
+ streamUiFinalizeAttempted = true;
14170
+ taskState.streamFinalized = await finalizeStreamFeedback(
14171
+ streamFeedbackTarget,
14172
+ status,
14173
+ responseText
14174
+ );
14175
+ }
14176
+ return taskState.streamFinalized;
14177
+ };
14178
+ const endMessageUiOnce = () => {
14179
+ if (taskState.uiEnded) return;
14180
+ adapter.onMessageEnd?.(msg.address.chatId, streamKey);
14181
+ taskState.uiEnded = true;
14182
+ };
14086
14183
  const onStreamCardText = hasStreamingCards ? (fullText) => {
14087
14184
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
14088
14185
  pushStreamFeedbackText(
@@ -14159,7 +14256,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14159
14256
  let shouldRecordHealthEnd = true;
14160
14257
  try {
14161
14258
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
14162
- const result = await processMessageImpl(
14259
+ const processPromise = processMessageImpl(
14163
14260
  binding,
14164
14261
  promptText,
14165
14262
  async (perm) => {
@@ -14194,6 +14291,38 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14194
14291
  }
14195
14292
  }
14196
14293
  );
14294
+ let raced;
14295
+ try {
14296
+ raced = await Promise.race([
14297
+ processPromise.then((result2) => ({ kind: "process", result: result2 })),
14298
+ externalTerminalPromise.then((terminal) => ({ kind: "external", terminal }))
14299
+ ]);
14300
+ } catch (error) {
14301
+ if (!externalTerminalRequest) throw error;
14302
+ raced = { kind: "external", terminal: externalTerminalRequest };
14303
+ }
14304
+ if (raced.kind === "external") {
14305
+ processPromise.catch(() => {
14306
+ });
14307
+ finalOutcome = raced.terminal.outcome;
14308
+ finalOutcomeDetail = raced.terminal.detail;
14309
+ const streamEndStatus = raced.terminal.outcome === "completed" ? "completed" : raced.terminal.outcome === "aborted" ? "interrupted" : "error";
14310
+ const staleTaskNotice2 = buildStaleTaskCompletionNotice(msg.address, binding);
14311
+ const terminalText = staleTaskNotice2 || raced.terminal.finalText || "";
14312
+ const cardFinalized2 = await finalizeStreamUiOnce(streamEndStatus, terminalText);
14313
+ if (!cardFinalized2 && terminalText) {
14314
+ await deps.deliverResponse(
14315
+ adapter,
14316
+ msg.address,
14317
+ terminalText,
14318
+ binding.codepilotSessionId,
14319
+ msg.messageId,
14320
+ []
14321
+ );
14322
+ }
14323
+ return;
14324
+ }
14325
+ const result = raced.result;
14197
14326
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
14198
14327
  shouldRecordHealthEnd = false;
14199
14328
  return;
@@ -14201,14 +14330,11 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14201
14330
  let cardFinalized = false;
14202
14331
  const staleTaskNotice = buildStaleTaskCompletionNotice(msg.address, binding);
14203
14332
  if (hasStreamingCards) {
14204
- stopStructuredStreamStatusUpdates();
14205
14333
  const streamEndStatus = taskAbort.signal.aborted ? "interrupted" : result.hasError ? "error" : "completed";
14206
- cardFinalized = await finalizeStreamFeedback(
14207
- streamFeedbackTarget,
14334
+ cardFinalized = await finalizeStreamUiOnce(
14208
14335
  streamEndStatus,
14209
14336
  staleTaskNotice || (streamEndStatus === "interrupted" ? "" : result.responseText)
14210
14337
  );
14211
- taskState.streamFinalized = cardFinalized;
14212
14338
  }
14213
14339
  if (staleTaskNotice) {
14214
14340
  if (!cardFinalized) {
@@ -14250,24 +14376,12 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14250
14376
  finalOutcome = result.hasError ? "failed" : "completed";
14251
14377
  finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
14252
14378
  } finally {
14253
- stopStructuredStreamStatusUpdates();
14254
- deps.recordInteractiveStreamUiSnapshot?.(binding.codepilotSessionId, { active: false });
14255
- if (previewState) {
14256
- if (previewState.throttleTimer) {
14257
- clearTimeout(previewState.throttleTimer);
14258
- previewState.throttleTimer = null;
14259
- }
14260
- adapter.endPreview?.(msg.address.chatId, previewState.draftId);
14261
- }
14262
- if (hasStreamingCards && taskAbort.signal.aborted && !taskState.streamFinalized) {
14263
- taskState.streamFinalized = await finalizeStreamFeedback(
14264
- streamFeedbackTarget,
14265
- "interrupted",
14266
- ""
14267
- );
14268
- }
14379
+ await finalizeStreamUiOnce(
14380
+ taskAbort.signal.aborted ? "interrupted" : finalOutcome === "completed" ? "completed" : "error",
14381
+ ""
14382
+ );
14269
14383
  if (taskState.mirrorSuppressionId) {
14270
- if (taskAbort.signal.aborted) {
14384
+ if (finalOutcome === "aborted") {
14271
14385
  deps.abortMirrorSuppression(binding.codepilotSessionId, taskState.mirrorSuppressionId);
14272
14386
  } else {
14273
14387
  deps.settleMirrorSuppression(binding.codepilotSessionId, taskState.mirrorSuppressionId);
@@ -14275,17 +14389,15 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
14275
14389
  taskState.mirrorSuppressionId = null;
14276
14390
  }
14277
14391
  if (shouldRecordHealthEnd) {
14278
- if (taskAbort.signal.aborted) {
14392
+ if (taskAbort.signal.aborted && !externalTerminalRequest) {
14279
14393
  finalOutcome = "aborted";
14280
14394
  finalOutcomeDetail = "\u4EFB\u52A1\u5DF2\u6536\u5230\u505C\u6B62\u8BF7\u6C42\u3002";
14281
14395
  }
14282
14396
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
14283
14397
  }
14284
14398
  deps.releaseInteractiveTask(binding.codepilotSessionId, taskId);
14285
- if (!taskState.uiEnded) {
14286
- adapter.onMessageEnd?.(msg.address.chatId, streamKey);
14287
- taskState.uiEnded = true;
14288
- }
14399
+ endMessageUiOnce();
14400
+ settleExternalTerminalCompletion(taskState.streamFinalized || !hasStreamingCards);
14289
14401
  }
14290
14402
  }
14291
14403
 
@@ -14298,16 +14410,13 @@ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
14298
14410
  function isTerminalSessionHealthStatus(status) {
14299
14411
  return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
14300
14412
  }
14301
- function buildInteractiveIdleReminderNotice() {
14302
- return [
14303
- "\u63D0\u9192\uFF1A\u8FD9\u8F6E\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5DF2\u7ECF\u8D85\u8FC7 10 \u5206\u949F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8F93\u51FA\u3002",
14304
- "\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u7EC8\u6B62\u5B83\uFF1B\u5982\u679C\u4F60\u4ECD\u5728\u5BF9\u5E94\u7EBF\u7A0B\uFF0C\u53EF\u53D1\u9001 `/stop` \u4E3B\u52A8\u505C\u6B62\uFF1B\u5982\u679C\u5DF2\u7ECF\u5207\u5230\u522B\u7684\u7EBF\u7A0B\uFF0C\u9700\u8981\u5148\u5207\u56DE\u5BF9\u5E94\u7EBF\u7A0B\u3002"
14305
- ].join("\n");
14306
- }
14307
- function shouldSkipIdleReminder(task) {
14308
- return task.adapter.provider === "feishu" && task.structuredStreamUiActive;
14413
+ function terminalOutcomeFromHealthStatus(status) {
14414
+ if (status === "completed") return "completed";
14415
+ if (status === "failed") return "failed";
14416
+ if (status === "aborted") return "aborted";
14417
+ return null;
14309
14418
  }
14310
- function createInteractiveRuntime(getState2, options, deps) {
14419
+ function createInteractiveRuntime(getState2, deps) {
14311
14420
  function getQueuedCount(sessionId) {
14312
14421
  return getState2().queuedCounts.get(sessionId) || 0;
14313
14422
  }
@@ -14341,7 +14450,6 @@ function createInteractiveRuntime(getState2, options, deps) {
14341
14450
  const task = getState2().activeTasks.get(sessionId);
14342
14451
  if (task?.id !== taskId) return;
14343
14452
  task.lastActivityAt = Date.now();
14344
- task.idleReminderSent = false;
14345
14453
  }
14346
14454
  function releaseInteractiveTask(sessionId, taskId) {
14347
14455
  const state = getState2();
@@ -14350,35 +14458,26 @@ function createInteractiveRuntime(getState2, options, deps) {
14350
14458
  state.activeTasks.delete(sessionId);
14351
14459
  syncSessionRuntimeState(sessionId);
14352
14460
  }
14353
- async function remindIdleInteractiveTask(task) {
14354
- if (!isCurrentInteractiveTask(task.sessionId, task.id) || task.idleReminderSent) return;
14355
- task.idleReminderSent = true;
14356
- try {
14357
- await deliverBridgeNotice(task.adapter, task.address, buildInteractiveIdleReminderNotice(), {
14358
- sessionId: task.sessionId,
14359
- replyToMessageId: task.requestMessageId
14360
- });
14361
- } catch {
14362
- }
14363
- }
14364
- async function reconcileIdleInteractiveTasks() {
14365
- const now2 = Date.now();
14366
- const tasks = Array.from(getState2().activeTasks.values());
14367
- for (const task of tasks) {
14368
- if (shouldSkipIdleReminder(task)) continue;
14369
- if (task.idleReminderSent) continue;
14370
- if (now2 - task.lastActivityAt < options.idleReminderMs) continue;
14371
- await remindIdleInteractiveTask(task);
14372
- }
14461
+ async function finalizeTerminalActiveTask(sessionId, outcome, detail, finalText) {
14462
+ const task = getState2().activeTasks.get(sessionId);
14463
+ if (!task?.finalizeFromExternalTerminal) return false;
14464
+ return task.finalizeFromExternalTerminal(outcome, detail, finalText);
14373
14465
  }
14374
- function reconcileTerminalSessionRuntimeState() {
14466
+ async function reconcileTerminalSessionRuntimeState() {
14375
14467
  const store = deps.getStore();
14376
14468
  for (const session of store.listSessions()) {
14469
+ if (!isTerminalSessionHealthStatus(session.health_status)) continue;
14470
+ const activeTask = getState2().activeTasks.get(session.id);
14471
+ if (activeTask) {
14472
+ const outcome = terminalOutcomeFromHealthStatus(session.health_status);
14473
+ if (outcome) {
14474
+ await finalizeTerminalActiveTask(session.id, outcome, session.health_reason || void 0);
14475
+ }
14476
+ continue;
14477
+ }
14377
14478
  const queuedCount = getQueuedCount(session.id);
14378
14479
  const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
14379
- const hasActiveTask = getState2().activeTasks.has(session.id);
14380
- if (hasActiveTask || queuedCount > 0) continue;
14381
- if (!isTerminalSessionHealthStatus(session.health_status)) continue;
14480
+ if (queuedCount > 0) continue;
14382
14481
  if (persistedQueuedCount === 0 && session.runtime_status !== "running" && session.runtime_status !== "queued") {
14383
14482
  continue;
14384
14483
  }
@@ -14449,7 +14548,7 @@ function createInteractiveRuntime(getState2, options, deps) {
14449
14548
  touchInteractiveTask,
14450
14549
  releaseInteractiveTask,
14451
14550
  syncSessionRuntimeState,
14452
- reconcileIdleInteractiveTasks,
14551
+ finalizeTerminalActiveTask,
14453
14552
  reconcileTerminalSessionRuntimeState,
14454
14553
  resetPersistedInteractiveRuntimeState,
14455
14554
  processWithSessionLock
@@ -15336,12 +15435,14 @@ function computeBaseDiagnosis(session, nowMs) {
15336
15435
  const lastStreamUiError = trimOrNull(session.last_stream_ui_error);
15337
15436
  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;
15338
15437
  const sdkSessionId = trimOrNull(session.sdk_session_id);
15438
+ const checkedAt = trimOrNull(session.last_health_check_at);
15339
15439
  const lastProgressMs = parseIsoMs(lastProgressAt || void 0);
15340
15440
  const previousStatus = session.health_status || "idle";
15341
15441
  if (!lastProgressMs) {
15342
15442
  const fallbackStatus = isRunningRuntimeStatus(runtimeStatus) ? "running_active" : previousStatus;
15343
15443
  return {
15344
15444
  sessionId: session.id,
15445
+ checkedAt,
15345
15446
  runtimeStatus,
15346
15447
  healthStatus: fallbackStatus,
15347
15448
  healthReason: session.health_reason?.trim() || (fallbackStatus === "idle" ? "\u5F53\u524D\u6CA1\u6709\u8BB0\u5F55\u5230\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002" : "\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u8BE6\u7EC6\u8FDB\u5C55\u3002"),
@@ -15379,6 +15480,7 @@ function computeBaseDiagnosis(session, nowMs) {
15379
15480
  }
15380
15481
  return {
15381
15482
  sessionId: session.id,
15483
+ checkedAt,
15382
15484
  runtimeStatus,
15383
15485
  healthStatus,
15384
15486
  healthReason,
@@ -15516,7 +15618,7 @@ function createSessionHealthRuntime(deps) {
15516
15618
  }
15517
15619
  }
15518
15620
  if (!changed) return;
15519
- store.updateSession(sessionId, next);
15621
+ store.updateSession(sessionId, next, { touch: options?.touch });
15520
15622
  }
15521
15623
  function maybePersistProgress(sessionId, updates, progressType) {
15522
15624
  const nowMs = Date.now();
@@ -15619,6 +15721,11 @@ function createSessionHealthRuntime(deps) {
15619
15721
  active_tool_name: void 0,
15620
15722
  active_tool_started_at: void 0,
15621
15723
  stream_ui_flush_started_at: void 0,
15724
+ last_stream_ui_attempt_at: void 0,
15725
+ last_stream_ui_update_at: void 0,
15726
+ last_stream_ui_error_at: void 0,
15727
+ last_stream_ui_error: void 0,
15728
+ stream_ui_consecutive_failures: void 0,
15622
15729
  last_health_check_at: nowIso4
15623
15730
  }, { force: true });
15624
15731
  lastProgressPersistAt.set(sessionId, Date.now());
@@ -15638,6 +15745,12 @@ function createSessionHealthRuntime(deps) {
15638
15745
  updates.last_stream_ui_error_at = toIso(snapshot.lastErrorAt);
15639
15746
  updates.last_stream_ui_error = snapshot.lastError?.trim() || void 0;
15640
15747
  updates.stream_ui_consecutive_failures = snapshot.consecutiveFailures && snapshot.consecutiveFailures > 0 ? snapshot.consecutiveFailures : void 0;
15748
+ } else {
15749
+ updates.last_stream_ui_attempt_at = void 0;
15750
+ updates.last_stream_ui_update_at = void 0;
15751
+ updates.last_stream_ui_error_at = void 0;
15752
+ updates.last_stream_ui_error = void 0;
15753
+ updates.stream_ui_consecutive_failures = void 0;
15641
15754
  }
15642
15755
  updateSessionHealth(sessionId, updates);
15643
15756
  }
@@ -15705,7 +15818,7 @@ function createSessionHealthRuntime(deps) {
15705
15818
  updateSessionHealth(session.id, {
15706
15819
  health_status: diagnosis.healthStatus,
15707
15820
  health_reason: diagnosis.healthReason
15708
- });
15821
+ }, { touch: false });
15709
15822
  }
15710
15823
  }
15711
15824
  async function loadProcessProbe(session) {
@@ -15729,16 +15842,21 @@ function createSessionHealthRuntime(deps) {
15729
15842
  if (!session) return null;
15730
15843
  const base = computeBaseDiagnosis(session, Date.now());
15731
15844
  const processProbe = await loadProcessProbe(session);
15845
+ const checkedAt = deps.nowIso();
15732
15846
  const diagnosis = applyStreamUiDiagnosis(
15733
15847
  applyProcessProbeDiagnosis(base, processProbe),
15734
15848
  Date.now()
15735
15849
  );
15850
+ const checkedDiagnosis = {
15851
+ ...diagnosis,
15852
+ checkedAt
15853
+ };
15736
15854
  updateSessionHealth(sessionId, {
15737
- health_status: diagnosis.healthStatus,
15738
- health_reason: diagnosis.healthReason,
15739
- last_health_check_at: deps.nowIso()
15740
- });
15741
- return diagnosis;
15855
+ health_status: checkedDiagnosis.healthStatus,
15856
+ health_reason: checkedDiagnosis.healthReason,
15857
+ last_health_check_at: checkedAt
15858
+ }, { touch: false });
15859
+ return checkedDiagnosis;
15742
15860
  }
15743
15861
  async function diagnoseAllActiveSessions() {
15744
15862
  const store = deps.getStore();
@@ -15769,10 +15887,9 @@ var MIRROR_WATCH_DEBOUNCE_MS = 350;
15769
15887
  var MIRROR_EVENT_BATCH_LIMIT = 8;
15770
15888
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
15771
15889
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
15772
- var INTERACTIVE_IDLE_REMINDER_MS = 6e5;
15773
15890
  var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
15774
15891
  var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
15775
- var MIRROR_IDLE_TIMEOUT_MS = 6e5;
15892
+ var MIRROR_TURN_BUFFER_TIMEOUT_MS = 6e5;
15776
15893
  function describeUnknownError(error) {
15777
15894
  if (error instanceof Error) {
15778
15895
  return error.stack || `${error.name}: ${error.message}`;
@@ -15854,8 +15971,6 @@ function getState() {
15854
15971
  return g[GLOBAL_KEY];
15855
15972
  }
15856
15973
  var INTERACTIVE_RUNTIME = createInteractiveRuntime(getState, {
15857
- idleReminderMs: INTERACTIVE_IDLE_REMINDER_MS
15858
- }, {
15859
15974
  getStore: () => getBridgeContext().store,
15860
15975
  nowIso: nowIso3
15861
15976
  });
@@ -16149,13 +16264,35 @@ function consumeMirrorRecords2(subscription, records) {
16149
16264
  return consumeMirrorRecords(subscription, records, MIRROR_TURN_HOOKS);
16150
16265
  }
16151
16266
  function flushTimedOutMirrorTurn2(subscription, nowMs = Date.now()) {
16152
- return flushTimedOutMirrorTurn(subscription, MIRROR_IDLE_TIMEOUT_MS, nowMs);
16267
+ return flushTimedOutMirrorTurn(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs);
16153
16268
  }
16154
16269
  function hasPendingMirrorWork2(subscription) {
16155
16270
  return hasPendingMirrorWork(subscription);
16156
16271
  }
16157
16272
  function consumeBufferedMirrorTurns2(subscription, nowMs = Date.now()) {
16158
- return consumeBufferedMirrorTurns(subscription, MIRROR_IDLE_TIMEOUT_MS, nowMs, MIRROR_TURN_HOOKS);
16273
+ return consumeBufferedMirrorTurns(subscription, MIRROR_TURN_BUFFER_TIMEOUT_MS, nowMs, MIRROR_TURN_HOOKS);
16274
+ }
16275
+ function finalizeInteractiveTaskFromMirrorRecords(sessionId, records) {
16276
+ let terminalRecord = null;
16277
+ for (let index = records.length - 1; index >= 0; index -= 1) {
16278
+ const record = records[index];
16279
+ if (record.type === "task_complete" || record.type === "task_aborted") {
16280
+ terminalRecord = record;
16281
+ break;
16282
+ }
16283
+ }
16284
+ if (!terminalRecord) return Promise.resolve(false);
16285
+ const outcome = terminalRecord.type === "task_complete" ? "completed" : "aborted";
16286
+ const detail = terminalRecord.type === "task_complete" ? "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u3002" : "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
16287
+ return INTERACTIVE_RUNTIME.finalizeTerminalActiveTask(
16288
+ sessionId,
16289
+ outcome,
16290
+ detail,
16291
+ terminalRecord.content
16292
+ ).catch((error) => {
16293
+ console.error("[bridge-manager] Failed to finalize terminal interactive task:", describeUnknownError(error));
16294
+ return false;
16295
+ });
16159
16296
  }
16160
16297
  var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16161
16298
  watchDebounceMs: MIRROR_WATCH_DEBOUNCE_MS,
@@ -16171,6 +16308,7 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
16171
16308
  filterSuppressedMirrorRecords: filterSuppressedMirrorRecords2,
16172
16309
  observeSessionHealthRecords: (sessionId, threadId, records) => {
16173
16310
  SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
16311
+ void finalizeInteractiveTaskFromMirrorRecords(sessionId, records);
16174
16312
  },
16175
16313
  consumeMirrorRecords: consumeMirrorRecords2,
16176
16314
  flushTimedOutMirrorTurn: (subscription) => flushTimedOutMirrorTurn2(subscription),
@@ -16229,15 +16367,14 @@ async function start() {
16229
16367
  void ADAPTER_RUNTIME.syncConfiguredAdapters({ startLoops: true }).catch((err) => {
16230
16368
  console.error("[bridge-manager] Adapter reconcile failed:", err);
16231
16369
  });
16232
- void INTERACTIVE_RUNTIME.reconcileIdleInteractiveTasks().catch((err) => {
16233
- console.error("[bridge-manager] Interactive idle reminder reconcile failed:", err);
16234
- });
16235
16370
  try {
16236
16371
  SESSION_HEALTH_RUNTIME.reconcileSessionHealth();
16237
- INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState();
16238
16372
  } catch (err) {
16239
16373
  console.error("[bridge-manager] Session health reconcile failed:", describeUnknownError(err));
16240
16374
  }
16375
+ void INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState().catch((err) => {
16376
+ console.error("[bridge-manager] Terminal interactive reconcile failed:", describeUnknownError(err));
16377
+ });
16241
16378
  }, 5e3);
16242
16379
  state.mirrorPollTimer = setInterval(() => {
16243
16380
  void reconcileMirrorSubscriptions().catch((err) => {
@@ -16786,7 +16923,7 @@ var JsonFileStore = class {
16786
16923
  this.persistSessions();
16787
16924
  }
16788
16925
  }
16789
- updateSession(sessionId, updates) {
16926
+ updateSession(sessionId, updates, options) {
16790
16927
  this.reloadSessions();
16791
16928
  const session = this.sessions.get(sessionId);
16792
16929
  if (!session) return;
@@ -16794,7 +16931,7 @@ var JsonFileStore = class {
16794
16931
  ...session,
16795
16932
  ...updates,
16796
16933
  id: session.id,
16797
- updated_at: now()
16934
+ updated_at: options?.touch === false ? session.updated_at : now()
16798
16935
  };
16799
16936
  this.sessions.set(sessionId, next);
16800
16937
  this.persistSessions();
@@ -6532,7 +6532,7 @@ var JsonFileStore = class {
6532
6532
  this.persistSessions();
6533
6533
  }
6534
6534
  }
6535
- updateSession(sessionId, updates) {
6535
+ updateSession(sessionId, updates, options) {
6536
6536
  this.reloadSessions();
6537
6537
  const session = this.sessions.get(sessionId);
6538
6538
  if (!session) return;
@@ -6540,7 +6540,7 @@ var JsonFileStore = class {
6540
6540
  ...session,
6541
6541
  ...updates,
6542
6542
  id: session.id,
6543
- updated_at: now()
6543
+ updated_at: options?.touch === false ? session.updated_at : now()
6544
6544
  };
6545
6545
  this.sessions.set(sessionId, next);
6546
6546
  this.persistSessions();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
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",