codex-to-im 1.0.64 → 1.0.66

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
@@ -1280,6 +1280,16 @@ function normalizeCodexErrorMessage(message) {
1280
1280
  }
1281
1281
  return trimmed;
1282
1282
  }
1283
+ function isCodexProcessRecoveryErrorMessage(message) {
1284
+ const lower = (message || "").trim().toLowerCase();
1285
+ return lower.includes("timeout waiting for child process to exit") || lower.includes("reconnecting...");
1286
+ }
1287
+ function enqueueCodexError(controller, message) {
1288
+ if (isCodexProcessRecoveryErrorMessage(message)) {
1289
+ controller.enqueue(sseEvent("status", { error_code: "desktop_transport_lost" }));
1290
+ }
1291
+ controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
1292
+ }
1283
1293
  function getTerminalDrainTimeoutMs() {
1284
1294
  const configured = parseInt(process.env.CTI_CODEX_TERMINAL_DRAIN_TIMEOUT_MS || "", 10);
1285
1295
  if (Number.isFinite(configured) && configured >= 10) {
@@ -1452,6 +1462,10 @@ var init_codex_provider = __esm({
1452
1462
  clearCachedThreadId(sessionId) {
1453
1463
  this.threadIds.delete(sessionId);
1454
1464
  }
1465
+ recycleSdkClient(sessionId) {
1466
+ this.clearCachedThreadId(sessionId);
1467
+ this.codex = null;
1468
+ }
1455
1469
  /**
1456
1470
  * Lazily load the Codex SDK. Throws a clear error if the installation is incomplete.
1457
1471
  */
@@ -1641,15 +1655,25 @@ var init_codex_provider = __esm({
1641
1655
  }
1642
1656
  case "turn.failed": {
1643
1657
  const error = event.error?.message;
1644
- self.clearCachedThreadId(params.sessionId);
1645
- controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Turn failed")));
1658
+ const message = error || "Turn failed";
1659
+ if (isCodexProcessRecoveryErrorMessage(message)) {
1660
+ self.recycleSdkClient(params.sessionId);
1661
+ } else {
1662
+ self.clearCachedThreadId(params.sessionId);
1663
+ }
1664
+ enqueueCodexError(controller, message);
1646
1665
  sawTerminalEvent = true;
1647
1666
  break;
1648
1667
  }
1649
1668
  case "error": {
1650
1669
  const error = event.message;
1651
- self.clearCachedThreadId(params.sessionId);
1652
- controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(error || "Thread error")));
1670
+ const message = error || "Thread error";
1671
+ if (isCodexProcessRecoveryErrorMessage(message)) {
1672
+ self.recycleSdkClient(params.sessionId);
1673
+ } else {
1674
+ self.clearCachedThreadId(params.sessionId);
1675
+ }
1676
+ enqueueCodexError(controller, message);
1653
1677
  sawTerminalEvent = true;
1654
1678
  break;
1655
1679
  }
@@ -1681,6 +1705,12 @@ var init_codex_provider = __esm({
1681
1705
  console.warn("[codex-provider] Suppressed Codex SDK Windows process cleanup parse noise:", message);
1682
1706
  break;
1683
1707
  }
1708
+ if (isCodexProcessRecoveryErrorMessage(message)) {
1709
+ if (!runAbortController.signal.aborted) {
1710
+ runAbortController.abort();
1711
+ }
1712
+ self.recycleSdkClient(params.sessionId);
1713
+ }
1684
1714
  if (savedThreadId && !retryFresh && !sawAnyEvent && shouldRetryFreshThread(message)) {
1685
1715
  console.warn("[codex-provider] Resume failed, retrying with a fresh thread:", message);
1686
1716
  self.clearCachedThreadId(params.sessionId);
@@ -1699,9 +1729,13 @@ var init_codex_provider = __esm({
1699
1729
  } catch (err) {
1700
1730
  const message = err instanceof Error ? err.message : String(err);
1701
1731
  console.error("[codex-provider] Error:", err instanceof Error ? err.stack || err.message : err);
1702
- self.clearCachedThreadId(params.sessionId);
1732
+ if (isCodexProcessRecoveryErrorMessage(message)) {
1733
+ self.recycleSdkClient(params.sessionId);
1734
+ } else {
1735
+ self.clearCachedThreadId(params.sessionId);
1736
+ }
1703
1737
  try {
1704
- controller.enqueue(sseEvent("error", normalizeCodexErrorMessage(message)));
1738
+ enqueueCodexError(controller, message);
1705
1739
  controller.close();
1706
1740
  } catch {
1707
1741
  }
@@ -11867,7 +11901,7 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds, vi
11867
11901
  }
11868
11902
  const lastEventAt = visibleThread?.updatedAtMs ? new Date(visibleThread.updatedAtMs).toISOString() : stat.mtime.toISOString();
11869
11903
  const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
11870
- const title = visibleThread?.title || threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
11904
+ const title = threadIndexEntries.get(threadId)?.title || visibleThread?.title || buildFallbackTitle(threadId, filePath, cwd);
11871
11905
  return {
11872
11906
  threadId,
11873
11907
  filePath,
@@ -12047,30 +12081,53 @@ function formatDesktopToolName(namespaceValue, nameValue) {
12047
12081
  function createDesktopEventSignature(rawLine) {
12048
12082
  return crypto7.createHash("sha1").update(rawLine).digest("hex");
12049
12083
  }
12050
- function readCompleteUtf8LineRange(filePath, startOffset, endOffset) {
12084
+ function readCompleteUtf8LineRange(filePath, startOffset, endOffset, maxBytes) {
12051
12085
  const safeStart = Math.max(0, startOffset);
12052
12086
  const safeEnd = Math.max(safeStart, endOffset);
12053
- const bytesToRead = safeEnd - safeStart;
12054
- if (bytesToRead <= 0) return { content: "", nextOffset: safeStart };
12087
+ const availableBytes = safeEnd - safeStart;
12088
+ if (availableBytes <= 0) return { content: "", nextOffset: safeStart };
12089
+ const boundedBytes = typeof maxBytes === "number" && Number.isFinite(maxBytes) ? Math.max(1, Math.min(availableBytes, Math.floor(maxBytes))) : availableBytes;
12055
12090
  const fd = fs5.openSync(filePath, "r");
12056
12091
  try {
12057
- const buffer = Buffer.alloc(bytesToRead);
12058
- let totalRead = 0;
12059
- while (totalRead < bytesToRead) {
12060
- const bytesRead = fs5.readSync(fd, buffer, totalRead, bytesToRead - totalRead, safeStart + totalRead);
12061
- if (bytesRead <= 0) break;
12062
- totalRead += bytesRead;
12063
- }
12064
- const readBuffer = buffer.subarray(0, totalRead);
12065
- const lastNewlineIndex = readBuffer.lastIndexOf(10);
12066
- if (lastNewlineIndex < 0) {
12067
- return { content: "", nextOffset: safeStart };
12092
+ const chunks = [];
12093
+ let totalBytes = 0;
12094
+ let position = safeStart;
12095
+ let firstChunk = true;
12096
+ while (position < safeEnd) {
12097
+ const requestedBytes = Math.min(boundedBytes, safeEnd - position);
12098
+ const buffer = Buffer.allocUnsafe(requestedBytes);
12099
+ let totalRead = 0;
12100
+ while (totalRead < requestedBytes) {
12101
+ const bytesRead = fs5.readSync(
12102
+ fd,
12103
+ buffer,
12104
+ totalRead,
12105
+ requestedBytes - totalRead,
12106
+ position + totalRead
12107
+ );
12108
+ if (bytesRead <= 0) break;
12109
+ totalRead += bytesRead;
12110
+ }
12111
+ if (totalRead <= 0) break;
12112
+ const readBuffer = buffer.subarray(0, totalRead);
12113
+ const newlineIndex = firstChunk ? readBuffer.lastIndexOf(10) : readBuffer.indexOf(10);
12114
+ if (newlineIndex >= 0) {
12115
+ const completeChunk = readBuffer.subarray(0, newlineIndex + 1);
12116
+ chunks.push(completeChunk);
12117
+ totalBytes += completeChunk.length;
12118
+ const completeBuffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalBytes);
12119
+ return {
12120
+ content: completeBuffer.toString("utf-8"),
12121
+ nextOffset: safeStart + totalBytes
12122
+ };
12123
+ }
12124
+ chunks.push(readBuffer);
12125
+ totalBytes += totalRead;
12126
+ position += totalRead;
12127
+ firstChunk = false;
12128
+ if (totalRead < requestedBytes) break;
12068
12129
  }
12069
- const consumedBytes = lastNewlineIndex + 1;
12070
- return {
12071
- content: readBuffer.subarray(0, consumedBytes).toString("utf-8"),
12072
- nextOffset: safeStart + consumedBytes
12073
- };
12130
+ return { content: "", nextOffset: safeStart };
12074
12131
  } finally {
12075
12132
  fs5.closeSync(fd);
12076
12133
  }
@@ -12831,10 +12888,10 @@ function readDesktopSessionMessages(threadId, limit = 8) {
12831
12888
  if (!session) return [];
12832
12889
  return readDesktopSessionMessagesByFilePath(session.filePath, limit);
12833
12890
  }
12834
- function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = []) {
12891
+ function readDesktopSessionMirrorRecordDeltaByFilePath(filePath, startOffset, endOffset, trailingText = "", currentTurnId = null, currentSpecialCallIds = [], options = {}) {
12835
12892
  let range;
12836
12893
  try {
12837
- range = readCompleteUtf8LineRange(filePath, startOffset, endOffset);
12894
+ range = readCompleteUtf8LineRange(filePath, startOffset, endOffset, options.maxBytes);
12838
12895
  } catch {
12839
12896
  return {
12840
12897
  records: [],
@@ -14478,6 +14535,18 @@ function abortMirrorSuppression(store, sessionId, config2, suppressionId, nowMs
14478
14535
  }
14479
14536
  target.until = nowMs + config2.suppressionWindowMs;
14480
14537
  }
14538
+ function handoffMirrorSuppression(store, sessionId, suppressionId) {
14539
+ clearMirrorSuppression(store, sessionId, suppressionId);
14540
+ }
14541
+ function ignoreMirrorTurn(store, sessionId, config2, turnId, nowMs = Date.now()) {
14542
+ markIgnoredMirrorTurn(
14543
+ store,
14544
+ sessionId,
14545
+ turnId,
14546
+ config2.promptMatchGraceMs,
14547
+ nowMs
14548
+ );
14549
+ }
14481
14550
  function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs = Date.now(), onTurnAssociated) {
14482
14551
  const suppressions = getMirrorSuppressionStates(store, sessionId, nowMs);
14483
14552
  const ignoredTurnIds = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
@@ -14489,7 +14558,7 @@ function filterSuppressedMirrorRecords(store, sessionId, records, config2, nowMs
14489
14558
  while (true) {
14490
14559
  const ignoredTurnIds2 = cleanupIgnoredMirrorTurns(store, sessionId, nowMs);
14491
14560
  if (record.turnId && ignoredTurnIds2.has(record.turnId)) {
14492
- if (record.type === "task_complete") {
14561
+ if (record.type === "task_complete" || record.type === "task_aborted") {
14493
14562
  clearIgnoredMirrorTurn(store, sessionId, record.turnId, nowMs);
14494
14563
  }
14495
14564
  handled = true;
@@ -15951,16 +16020,35 @@ ${truncateHistoryContent(formatStoredMessageContent(message.content))}`;
15951
16020
  "suspected_detached"
15952
16021
  ]);
15953
16022
  const looksRunning = session?.runtime_status === "running" || session?.runtime_status === "queued" || runningHealthStatuses.has(session?.health_status || "");
15954
- if (task || looksRunning) {
15955
- const taskName = getSessionDisplayName(session, binding.workingDirectory);
15956
- const detail = "\u7528\u6237\u6267\u884C /stop\uFF0C\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
15957
- if (deps.forceStopSession) {
15958
- await deps.forceStopSession(binding.codepilotSessionId, detail);
15959
- } else if (task) {
16023
+ const taskName = getSessionDisplayName(session, binding.workingDirectory);
16024
+ const stopDetail = "\u7528\u6237\u6267\u884C /stop\uFF0C\u8BF7\u6C42\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
16025
+ if (deps.forceStopSession) {
16026
+ const result = await deps.forceStopSession(binding.codepilotSessionId, stopDetail);
16027
+ if (result.status === "stopped") {
16028
+ deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", stopDetail);
16029
+ response = `\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u4EFB\u52A1\u5DF2\u505C\u6B62\uFF0C\u53EF\u7EE7\u7EED\u53D1\u9001\u6D88\u606F\u6062\u590D\u8BE5\u7EBF\u7A0B\u3002`;
16030
+ } else if (result.status === "stop_requested") {
16031
+ deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", stopDetail);
16032
+ response = `\u5DF2\u5411\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u53D1\u9001\u505C\u6B62\u8BF7\u6C42\u3002\u53EF\u53D1\u9001 \`//\` \u786E\u8BA4\u6700\u7EC8\u72B6\u6001\u3002`;
16033
+ } else if (result.status === "not_running") {
16034
+ response = result.detail || "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
16035
+ } else if (result.status === "unavailable") {
16036
+ response = `\u65E0\u6CD5\u4ECE IM \u5B89\u5168\u505C\u6B62\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\uFF1A${result.detail}
16037
+
16038
+ \u672A\u7EC8\u6B62 Codex Desktop App \u6216\u5176\u4ED6\u65E0\u6CD5\u786E\u8BA4\u5F52\u5C5E\u7684\u8FDB\u7A0B\u3002`;
16039
+ } else {
16040
+ response = `\u505C\u6B62\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u5931\u8D25\uFF1A${result.detail}
16041
+
16042
+ \u53EF\u7A0D\u540E\u91CD\u8BD5 \`/stop\`\uFF0C\u6216\u53D1\u9001 \`//\` \u67E5\u770B\u4EFB\u52A1\u662F\u5426\u4ECD\u5728\u8FD0\u884C\u3002`;
16043
+ }
16044
+ } else if (task || looksRunning) {
16045
+ if (task) {
15960
16046
  task.abortController.abort();
16047
+ deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", stopDetail);
16048
+ response = `\u5DF2\u5411\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u53D1\u9001\u505C\u6B62\u8BF7\u6C42\u3002\u53EF\u53D1\u9001 \`//\` \u786E\u8BA4\u6700\u7EC8\u72B6\u6001\u3002`;
16049
+ } else {
16050
+ response = "\u5F53\u524D\u4EFB\u52A1\u770B\u8D77\u6765\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5F53\u524D\u8FD0\u884C\u65F6\u4E0D\u652F\u6301\u4ECE IM \u5B89\u5168\u505C\u6B62\u3002";
15961
16051
  }
15962
- deps.recordInteractiveHealthEnd?.(binding.codepilotSessionId, "aborted", detail);
15963
- response = `\u65E7\u4F1A\u8BDD\u300C${taskName}\u300D\u4EFB\u52A1\u5DF2\u505C\u6B62\uFF0C\u53EF\u7EE7\u7EED\u53D1\u9001\u6D88\u606F\u6062\u590D\u8BE5\u7EBF\u7A0B\u3002`;
15964
16052
  } else {
15965
16053
  response = "\u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u3002";
15966
16054
  }
@@ -16458,6 +16546,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
16458
16546
  let hasError = false;
16459
16547
  let errorMessage = "";
16460
16548
  let errorCode;
16549
+ let providerErrorCode;
16461
16550
  const seenToolResultIds = /* @__PURE__ */ new Set();
16462
16551
  const permissionRequests = [];
16463
16552
  let capturedSdkSessionId = null;
@@ -16590,6 +16679,9 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
16590
16679
  } catch {
16591
16680
  }
16592
16681
  }
16682
+ if (statusData.error_code === "desktop_transport_lost") {
16683
+ providerErrorCode = "desktop_transport_lost";
16684
+ }
16593
16685
  } catch {
16594
16686
  }
16595
16687
  break;
@@ -16616,6 +16708,8 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
16616
16708
  errorMessage = event.data || "Unknown error";
16617
16709
  if (isSessionBusyErrorMessage(errorMessage)) {
16618
16710
  errorCode = "session_busy";
16711
+ } else if (providerErrorCode) {
16712
+ errorCode = providerErrorCode;
16619
16713
  }
16620
16714
  break;
16621
16715
  case "result": {
@@ -16640,7 +16734,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
16640
16734
  tokenUsage,
16641
16735
  hasError,
16642
16736
  errorMessage,
16643
- errorCode,
16737
+ errorCode: errorCode || providerErrorCode,
16644
16738
  permissionRequests,
16645
16739
  sdkSessionId: capturedSdkSessionId
16646
16740
  };
@@ -16654,7 +16748,7 @@ async function consumeStream(stream, sessionId, onPermissionRequest, onPartialTe
16654
16748
  tokenUsage,
16655
16749
  hasError: true,
16656
16750
  errorMessage: fallbackErrorMessage,
16657
- errorCode: isSessionBusyErrorMessage(fallbackErrorMessage) ? "session_busy" : void 0,
16751
+ errorCode: isSessionBusyErrorMessage(fallbackErrorMessage) ? "session_busy" : providerErrorCode,
16658
16752
  permissionRequests,
16659
16753
  sdkSessionId: capturedSdkSessionId
16660
16754
  };
@@ -16977,6 +17071,12 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
16977
17071
  const streamState = createStreamState(taskStartedAt);
16978
17072
  let externalTerminalRequest = null;
16979
17073
  let desktopTerminalFinalExpected = false;
17074
+ let desktopTurnAccepted = false;
17075
+ let desktopTurnId = null;
17076
+ let desktopHandoffPreparation = null;
17077
+ let desktopHandoffStopReady = false;
17078
+ let desktopHandoffTaskActivated = false;
17079
+ let retainDesktopHandoffTask = false;
16980
17080
  let resolveExternalTerminal = null;
16981
17081
  const externalTerminalPromise = new Promise((resolve2) => {
16982
17082
  resolveExternalTerminal = resolve2;
@@ -17032,7 +17132,25 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
17032
17132
  desktopThreadId,
17033
17133
  requestMessageId: msg.messageId,
17034
17134
  streamKey,
17035
- startedAt: taskStartedAt
17135
+ startedAt: taskStartedAt,
17136
+ onDesktopTurnAssociated: (turnId) => {
17137
+ desktopTurnAccepted = true;
17138
+ desktopTurnId = turnId;
17139
+ if (desktopThreadId && deps.prepareDesktopHandoffTask && !desktopHandoffPreparation) {
17140
+ desktopHandoffPreparation = deps.prepareDesktopHandoffTask({
17141
+ sessionId: binding.codepilotSessionId,
17142
+ taskId,
17143
+ threadId: desktopThreadId,
17144
+ turnId
17145
+ }).catch((error) => {
17146
+ console.warn(
17147
+ `[interactive-message-runner] Failed to prepare Desktop handoff stop control for ${desktopThreadId}:`,
17148
+ error instanceof Error ? error.message : error
17149
+ );
17150
+ return false;
17151
+ });
17152
+ }
17153
+ }
17036
17154
  });
17037
17155
  deps.recordInteractiveHealthStart(binding.codepilotSessionId);
17038
17156
  let previewState = null;
@@ -17409,7 +17527,30 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
17409
17527
  shouldRecordHealthEnd = false;
17410
17528
  return;
17411
17529
  }
17530
+ const desktopTransportLost = Boolean(
17531
+ desktopThreadId && desktopTurnAccepted && result.hasError && result.errorCode === "desktop_transport_lost"
17532
+ );
17533
+ if (desktopTransportLost && desktopHandoffPreparation && deps.activateDesktopHandoffTask) {
17534
+ desktopHandoffStopReady = await desktopHandoffPreparation;
17535
+ if (deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
17536
+ desktopHandoffTaskActivated = deps.activateDesktopHandoffTask(
17537
+ binding.codepilotSessionId,
17538
+ taskId
17539
+ );
17540
+ }
17541
+ }
17542
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
17543
+ shouldRecordHealthEnd = false;
17544
+ return;
17545
+ }
17412
17546
  const terminalAfterProcess = await waitForDesktopTerminalFinalization();
17547
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) {
17548
+ shouldRecordHealthEnd = false;
17549
+ return;
17550
+ }
17551
+ if (taskAbort.signal.aborted && !externalTerminalRequest) {
17552
+ return;
17553
+ }
17413
17554
  const terminalResponse = terminalAfterProcess?.outcome === "completed" ? assembleDesktopFinalResponse({ text: terminalAfterProcess.finalText || "" }) : null;
17414
17555
  const sdkResponse = assembleSdkFinalResponse({
17415
17556
  text: result.responseText,
@@ -17417,6 +17558,33 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
17417
17558
  hasError: result.hasError,
17418
17559
  errorMessage: result.errorMessage
17419
17560
  });
17561
+ const shouldHandoffToMirror = Boolean(
17562
+ desktopThreadId && desktopTurnAccepted && !terminalAfterProcess && result.hasError && result.errorCode === "desktop_transport_lost" && deps.handoffMirrorSuppression
17563
+ );
17564
+ if (shouldHandoffToMirror) {
17565
+ const canStopFromIm = desktopHandoffTaskActivated && desktopHandoffStopReady;
17566
+ retainDesktopHandoffTask = Boolean(desktopHandoffTaskActivated && desktopTurnId);
17567
+ const handoffText = [
17568
+ "Codex Desktop \u5DF2\u63A5\u6536\u672C\u6B21\u8BF7\u6C42\uFF0C\u4F46 SDK \u8FDE\u63A5\u5728\u6267\u884C\u4E2D\u65AD\u5F00\u3002",
17569
+ "\u5DF2\u81EA\u52A8\u5207\u6362\u4E3A\u684C\u9762\u955C\u50CF\u63A5\u7BA1\uFF1B\u540E\u7EED\u56DE\u590D\u548C\u9644\u4EF6\u4F1A\u7EE7\u7EED\u56DE\u4F20\uFF0C\u8BF7\u4E0D\u8981\u91CD\u590D\u53D1\u9001\u540C\u4E00\u8BF7\u6C42\u3002",
17570
+ canStopFromIm ? "\u53EF\u53D1\u9001 `//` \u67E5\u770B\u5F53\u524D\u72B6\u6001\uFF1B\u82E5\u8981\u653E\u5F03\u672C\u6B21\u684C\u9762\u4EFB\u52A1\uFF0C\u8BF7\u53D1\u9001 `/stop`\u3002`/t 0` \u53EA\u5207\u6362\u4F1A\u8BDD\uFF0C\u4E0D\u4F1A\u505C\u6B62\u539F\u4EFB\u52A1\u3002" : "\u53EF\u53D1\u9001 `//` \u67E5\u770B\u5F53\u524D\u72B6\u6001\uFF1B\u4E5F\u53EF\u53D1\u9001 `/stop` \u5C1D\u8BD5\u5B89\u5168\u505C\u6B62\u3002\u82E5\u65E0\u6CD5\u9A8C\u8BC1 bridge \u81EA\u6709\u8FDB\u7A0B\uFF0C\u7CFB\u7EDF\u4E0D\u4F1A\u8BEF\u505C Desktop App\u3002"
17571
+ ].join("\n\n");
17572
+ if (taskState.mirrorSuppressionId && deps.handoffMirrorSuppression) {
17573
+ deps.handoffMirrorSuppression(
17574
+ binding.codepilotSessionId,
17575
+ taskState.mirrorSuppressionId
17576
+ );
17577
+ taskState.mirrorSuppressionId = null;
17578
+ }
17579
+ const handoffResponse = assembleSdkFinalResponse({
17580
+ text: handoffText,
17581
+ attachments: result.outboundAttachments
17582
+ });
17583
+ const cardFinalized2 = await finalizeStreamUiOnce("interrupted", handoffResponse.text);
17584
+ await deliverFinalPayload(handoffResponse, { skipText: cardFinalized2 });
17585
+ shouldRecordHealthEnd = false;
17586
+ return;
17587
+ }
17420
17588
  const terminalHasFinalPayload = Boolean(
17421
17589
  terminalResponse && hasFinalResponsePayload(terminalResponse)
17422
17590
  );
@@ -17472,6 +17640,9 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
17472
17640
  }
17473
17641
  deps.recordInteractiveHealthEnd(binding.codepilotSessionId, finalOutcome, finalOutcomeDetail);
17474
17642
  }
17643
+ if (!retainDesktopHandoffTask) {
17644
+ deps.releaseDesktopHandoffTask?.(binding.codepilotSessionId, taskId);
17645
+ }
17475
17646
  deps.releaseInteractiveTask(binding.codepilotSessionId, taskId);
17476
17647
  deps.releaseBridgeTurn?.(binding.codepilotSessionId, taskId);
17477
17648
  endMessageUiOnce();
@@ -17545,7 +17716,7 @@ function createInteractiveRuntime(getState2, deps) {
17545
17716
  async function forceStopSession(sessionId, detail) {
17546
17717
  const state = getState2();
17547
17718
  const task = state.activeTasks.get(sessionId);
17548
- let handled = false;
17719
+ let handled = getQueuedCount(sessionId) > 0;
17549
17720
  if (task?.forceStop) {
17550
17721
  handled = await task.forceStop(detail);
17551
17722
  } else if (task) {
@@ -17659,6 +17830,7 @@ function resetMirrorReadState(subscription) {
17659
17830
  subscription.trailingText = "";
17660
17831
  subscription.activeMirrorTurnId = null;
17661
17832
  subscription.activeSpecialCallIds.clear();
17833
+ subscription.recoveryState = null;
17662
17834
  subscription.bufferedRecords = [];
17663
17835
  }
17664
17836
  function createMirrorSubscription(input) {
@@ -17683,6 +17855,7 @@ function createMirrorSubscription(input) {
17683
17855
  trailingText: "",
17684
17856
  activeMirrorTurnId: null,
17685
17857
  activeSpecialCallIds: /* @__PURE__ */ new Set(),
17858
+ recoveryState: null,
17686
17859
  bufferedRecords: [],
17687
17860
  pendingTurn: null,
17688
17861
  pendingDeliveries: [],
@@ -17794,73 +17967,9 @@ function filterDuplicateAssistantEvents(cursor, records) {
17794
17967
  }
17795
17968
  return startIndex === 0 ? records : records.slice(startIndex);
17796
17969
  }
17797
- function findLastEventIndex(records, signature) {
17798
- for (let index = records.length - 1; index >= 0; index -= 1) {
17799
- if (records[index]?.signature === signature) {
17800
- return index;
17801
- }
17802
- }
17803
- return -1;
17804
- }
17805
- function collectEventsAfterTimestamp(records, timestamp) {
17806
- if (!timestamp) return [];
17807
- return records.filter((event) => Boolean(event.timestamp) && event.timestamp > timestamp);
17808
- }
17809
- function reconcileDesktopMirrorCursor(cursor, records) {
17810
- const nextCursor = makeCursor(records);
17811
- if (!cursor?.initialized) {
17812
- return {
17813
- nextCursor,
17814
- deliverableRecords: [],
17815
- reset: false
17816
- };
17817
- }
17818
- if (records.length === 0) {
17819
- return {
17820
- nextCursor,
17821
- deliverableRecords: [],
17822
- reset: cursor.lastEventCount > 0
17823
- };
17824
- }
17825
- if (cursor.lastEventSignature) {
17826
- const lastSeenIndex = findLastEventIndex(records, cursor.lastEventSignature);
17827
- if (lastSeenIndex === -1) {
17828
- const recoveredEvents = collectEventsAfterTimestamp(records, cursor.lastEventTimestamp);
17829
- return {
17830
- nextCursor,
17831
- deliverableRecords: recoveredEvents,
17832
- reset: true
17833
- };
17834
- }
17835
- return {
17836
- nextCursor,
17837
- deliverableRecords: records.slice(lastSeenIndex + 1),
17838
- reset: false
17839
- };
17840
- }
17841
- if (cursor.lastEventCount === 0) {
17842
- return {
17843
- nextCursor,
17844
- deliverableRecords: cursor.lastEventTimestamp ? collectEventsAfterTimestamp(records, cursor.lastEventTimestamp) : records,
17845
- reset: false
17846
- };
17847
- }
17848
- if (records.length < cursor.lastEventCount) {
17849
- const recoveredEvents = collectEventsAfterTimestamp(records, cursor.lastEventTimestamp);
17850
- return {
17851
- nextCursor,
17852
- deliverableRecords: recoveredEvents,
17853
- reset: true
17854
- };
17855
- }
17856
- return {
17857
- nextCursor,
17858
- deliverableRecords: records.slice(cursor.lastEventCount),
17859
- reset: false
17860
- };
17861
- }
17862
17970
 
17863
17971
  // src/lib/bridge/mirror-reconcile-core.ts
17972
+ var DEFAULT_MAX_MIRROR_READ_BYTES = 64 * 1024 * 1024;
17864
17973
  function statMirrorFile(filePath) {
17865
17974
  try {
17866
17975
  const stat = fs10.statSync(filePath);
@@ -17912,12 +18021,70 @@ function findLastCompleteLineOffset(filePath, fileSize) {
17912
18021
  fs10.closeSync(fd);
17913
18022
  }
17914
18023
  }
17915
- function readMirrorDeliverableRecords(subscription, snapshot) {
18024
+ function beginMirrorRecovery(subscription) {
18025
+ subscription.recoveryState = {
18026
+ baselineCursor: { ...subscription.cursor },
18027
+ scannedCursor: { initialized: true, lastEventCount: 0 },
18028
+ boundaryFound: false,
18029
+ dedupePending: true,
18030
+ scannedRecordCount: 0
18031
+ };
18032
+ subscription.trailingText = "";
18033
+ subscription.fileOffset = 0;
18034
+ subscription.activeMirrorTurnId = null;
18035
+ subscription.activeSpecialCallIds.clear();
18036
+ }
18037
+ function findCursorTimestampBoundary(cursor, records) {
18038
+ if (!cursor.lastEventTimestamp) return -1;
18039
+ return records.findIndex((record) => Boolean(record.timestamp) && record.timestamp > cursor.lastEventTimestamp);
18040
+ }
18041
+ function selectRecoveredRecords(subscription, records) {
18042
+ const recovery = subscription.recoveryState;
18043
+ if (!recovery || records.length === 0) return [];
18044
+ if (recovery.boundaryFound) return records;
18045
+ const baseline = recovery.baselineCursor;
18046
+ if (baseline.lastEventSignature) {
18047
+ for (let index = records.length - 1; index >= 0; index -= 1) {
18048
+ if (records[index]?.signature !== baseline.lastEventSignature) continue;
18049
+ recovery.boundaryFound = true;
18050
+ return records.slice(index + 1);
18051
+ }
18052
+ }
18053
+ const timestampBoundary = findCursorTimestampBoundary(baseline, records);
18054
+ if (timestampBoundary >= 0) {
18055
+ recovery.boundaryFound = true;
18056
+ return records.slice(timestampBoundary);
18057
+ }
18058
+ if (baseline.lastEventSignature || baseline.lastEventTimestamp) return [];
18059
+ if (baseline.lastEventCount > recovery.scannedRecordCount) {
18060
+ const boundary = baseline.lastEventCount - recovery.scannedRecordCount;
18061
+ if (boundary >= records.length) return [];
18062
+ recovery.boundaryFound = true;
18063
+ return records.slice(boundary);
18064
+ }
18065
+ recovery.boundaryFound = true;
18066
+ return records;
18067
+ }
18068
+ function filterFirstRecoveredAssistantDuplicates(subscription, records) {
18069
+ const recovery = subscription.recoveryState;
18070
+ if (!recovery?.dedupePending || records.length === 0) return records;
18071
+ const filtered = filterDuplicateAssistantEvents(recovery.baselineCursor, records);
18072
+ if (filtered.length > 0) {
18073
+ recovery.dedupePending = false;
18074
+ }
18075
+ return filtered;
18076
+ }
18077
+ function readMirrorDeliverableRecords(subscription, snapshot, options = {}) {
17916
18078
  let deliverableRecords = [];
17917
18079
  let unknownKinds = [];
17918
- 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;
18080
+ const previousOffset = subscription.fileOffset;
18081
+ const configuredMaxReadBytes = options.maxReadBytes ?? DEFAULT_MAX_MIRROR_READ_BYTES;
18082
+ const maxReadBytes = Number.isFinite(configuredMaxReadBytes) && configuredMaxReadBytes > 0 ? Math.max(1, Math.floor(configuredMaxReadBytes)) : DEFAULT_MAX_MIRROR_READ_BYTES;
18083
+ const sourceChanged = 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;
18084
+ const requiresFullRecover = Boolean(subscription.recoveryState) || !subscription.cursor.initialized || subscription.fileOffset === 0 || sourceChanged;
17919
18085
  if (requiresFullRecover && !subscription.cursor.initialized && !subscription.lastDeliveredAt) {
17920
18086
  subscription.cursor = { initialized: true, lastEventCount: 0 };
18087
+ subscription.recoveryState = null;
17921
18088
  subscription.trailingText = "";
17922
18089
  subscription.fileOffset = findLastCompleteLineOffset(subscription.filePath, snapshot.size);
17923
18090
  subscription.activeMirrorTurnId = null;
@@ -17926,26 +18093,37 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
17926
18093
  subscription.fileMtimeMs = snapshot.mtimeMs;
17927
18094
  subscription.fileIdentity = snapshot.identity;
17928
18095
  subscription.dirty = false;
17929
- return { records: [], unknownKinds: [] };
18096
+ return { records: [], unknownKinds: [], hasMoreData: false };
17930
18097
  }
17931
18098
  if (requiresFullRecover) {
17932
- const previousCursor = subscription.cursor;
17933
- const fullDelta = readDesktopSessionMirrorRecordDeltaByFilePath(
18099
+ if (!subscription.recoveryState || sourceChanged) {
18100
+ beginMirrorRecovery(subscription);
18101
+ }
18102
+ const recovery = subscription.recoveryState;
18103
+ const delta = readDesktopSessionMirrorRecordDeltaByFilePath(
17934
18104
  subscription.filePath,
17935
- 0,
18105
+ subscription.fileOffset,
17936
18106
  snapshot.size,
17937
- "",
17938
- null,
17939
- []
18107
+ subscription.trailingText,
18108
+ subscription.activeMirrorTurnId,
18109
+ subscription.activeSpecialCallIds,
18110
+ { maxBytes: maxReadBytes }
18111
+ );
18112
+ deliverableRecords = filterFirstRecoveredAssistantDuplicates(
18113
+ subscription,
18114
+ selectRecoveredRecords(subscription, delta.records)
17940
18115
  );
17941
- const delta = reconcileDesktopMirrorCursor(subscription.cursor, fullDelta.records);
17942
- subscription.cursor = delta.nextCursor;
17943
- deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.deliverableRecords);
17944
- subscription.trailingText = fullDelta.trailingText;
17945
- subscription.fileOffset = fullDelta.nextOffset;
17946
- subscription.activeMirrorTurnId = fullDelta.nextTurnId;
17947
- subscription.activeSpecialCallIds = new Set(fullDelta.nextSpecialCallIds);
17948
- unknownKinds = fullDelta.unknownKinds;
18116
+ recovery.scannedCursor = advanceDesktopMirrorCursor(recovery.scannedCursor, delta.records);
18117
+ recovery.scannedRecordCount += delta.records.length;
18118
+ subscription.trailingText = delta.trailingText;
18119
+ subscription.fileOffset = delta.nextOffset;
18120
+ subscription.activeMirrorTurnId = delta.nextTurnId;
18121
+ subscription.activeSpecialCallIds = new Set(delta.nextSpecialCallIds);
18122
+ unknownKinds = delta.unknownKinds;
18123
+ if (subscription.fileOffset >= snapshot.size && !subscription.trailingText) {
18124
+ subscription.cursor = recovery.scannedCursor;
18125
+ subscription.recoveryState = null;
18126
+ }
17949
18127
  } else if (snapshot.size > subscription.fileOffset || subscription.trailingText) {
17950
18128
  const previousCursor = subscription.cursor;
17951
18129
  const delta = readDesktopSessionMirrorRecordDeltaByFilePath(
@@ -17954,7 +18132,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
17954
18132
  snapshot.size,
17955
18133
  subscription.trailingText,
17956
18134
  subscription.activeMirrorTurnId,
17957
- subscription.activeSpecialCallIds
18135
+ subscription.activeSpecialCallIds,
18136
+ { maxBytes: maxReadBytes }
17958
18137
  );
17959
18138
  deliverableRecords = filterDuplicateAssistantEvents(previousCursor, delta.records);
17960
18139
  subscription.cursor = advanceDesktopMirrorCursor(subscription.cursor, delta.records);
@@ -17970,7 +18149,8 @@ function readMirrorDeliverableRecords(subscription, snapshot) {
17970
18149
  subscription.dirty = false;
17971
18150
  return {
17972
18151
  records: deliverableRecords,
17973
- unknownKinds
18152
+ unknownKinds,
18153
+ hasMoreData: subscription.fileOffset > previousOffset && subscription.fileOffset < snapshot.size
17974
18154
  };
17975
18155
  }
17976
18156
 
@@ -18298,6 +18478,9 @@ function createMirrorRuntime(getState2, options, deps) {
18298
18478
  return "processed";
18299
18479
  }
18300
18480
  const readResult = readMirrorDeliverableRecords(subscription, snapshot);
18481
+ if (readResult.hasMoreData) {
18482
+ scheduleMirrorWake();
18483
+ }
18301
18484
  const deliverableRecords = readResult.records;
18302
18485
  for (const kind of readResult.unknownKinds) {
18303
18486
  if (subscription.unknownMirrorKindsSeen.has(kind)) continue;
@@ -18423,8 +18606,41 @@ function createMirrorRuntime(getState2, options, deps) {
18423
18606
  }
18424
18607
  }
18425
18608
  }
18609
+ function interruptMirrorTurn(sessionId, threadId, turnId) {
18610
+ const normalizedTurnId = turnId.trim();
18611
+ if (!normalizedTurnId) return false;
18612
+ const streamKey = buildMirrorStreamKey(sessionId, normalizedTurnId, "");
18613
+ let interrupted = false;
18614
+ for (const subscription of getState2().mirrorSubscriptions.values()) {
18615
+ if (subscription.sessionId !== sessionId || subscription.threadId !== threadId) continue;
18616
+ let subscriptionInterrupted = false;
18617
+ const bufferedCount = subscription.bufferedRecords.length;
18618
+ const deliveryCount = subscription.pendingDeliveries.length;
18619
+ subscription.bufferedRecords = subscription.bufferedRecords.filter(
18620
+ (record) => record.turnId !== normalizedTurnId
18621
+ );
18622
+ subscription.pendingDeliveries = subscription.pendingDeliveries.filter(
18623
+ (turn) => turn.streamKey !== streamKey
18624
+ );
18625
+ if (subscription.pendingTurn?.turnId === normalizedTurnId) {
18626
+ deps.stopMirrorStreaming(subscription, "interrupted");
18627
+ subscription.pendingTurn = null;
18628
+ subscriptionInterrupted = true;
18629
+ }
18630
+ if (subscription.bufferedRecords.length !== bufferedCount || subscription.pendingDeliveries.length !== deliveryCount) {
18631
+ subscriptionInterrupted = true;
18632
+ }
18633
+ if (subscriptionInterrupted) {
18634
+ interrupted = true;
18635
+ orphanProbeAt.delete(subscription.bindingId);
18636
+ deps.syncMirrorSessionStateSafe(sessionId, "mirror turn interrupted from IM");
18637
+ }
18638
+ }
18639
+ return interrupted;
18640
+ }
18426
18641
  return {
18427
18642
  resetMirrorSessionForInteractiveRun: resetMirrorSessionForInteractiveRun2,
18643
+ interruptMirrorTurn,
18428
18644
  reconcileMirrorSubscriptions: reconcileMirrorSubscriptions2,
18429
18645
  clearMirrorSubscriptions: clearMirrorSubscriptions2
18430
18646
  };
@@ -19453,7 +19669,11 @@ function createTurnCoordinator(deps = {}) {
19453
19669
  const normalizedTurnId = turnId.trim();
19454
19670
  if (!turn || turn.kind !== "im_desktop_reuse" || !normalizedTurnId) return false;
19455
19671
  if (turn.codexTurnId && turn.codexTurnId !== normalizedTurnId) return false;
19672
+ const changed = turn.codexTurnId !== normalizedTurnId;
19456
19673
  turn.codexTurnId = normalizedTurnId;
19674
+ if (changed) {
19675
+ turn.onDesktopTurnAssociated?.(normalizedTurnId);
19676
+ }
19457
19677
  return true;
19458
19678
  }
19459
19679
  async function claimDesktopTerminal(terminal) {
@@ -19497,6 +19717,354 @@ function createTurnCoordinator(deps = {}) {
19497
19717
  };
19498
19718
  }
19499
19719
 
19720
+ // src/lib/bridge/managed-codex-process.ts
19721
+ import { execFile as execFile2 } from "node:child_process";
19722
+ import { promisify as promisify2 } from "node:util";
19723
+ var execFileAsync2 = promisify2(execFile2);
19724
+ var PROCESS_QUERY_TIMEOUT_MS = 5e3;
19725
+ var PROCESS_STOP_TIMEOUT_MS = 1e4;
19726
+ var PROCESS_EXIT_WAIT_MS = 5e3;
19727
+ function escapePowerShellSingleQuoted2(value) {
19728
+ return value.replace(/'/g, "''");
19729
+ }
19730
+ function escapeRegex(value) {
19731
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19732
+ }
19733
+ function normalizeSnapshot(raw) {
19734
+ if (!raw || typeof raw !== "object") return null;
19735
+ const record = raw;
19736
+ const pid = Number(record.ProcessId);
19737
+ const parentPid = Number(record.ParentProcessId);
19738
+ const createdAt = typeof record.CreationDate === "string" ? record.CreationDate.trim() : "";
19739
+ const name = typeof record.Name === "string" ? record.Name.trim() : "";
19740
+ const commandLine = typeof record.CommandLine === "string" ? record.CommandLine.trim() : "";
19741
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(parentPid) || parentPid <= 0) {
19742
+ return null;
19743
+ }
19744
+ if (!createdAt || !name || !commandLine) return null;
19745
+ return { pid, parentPid, createdAt, name, commandLine };
19746
+ }
19747
+ function parseSnapshots(stdout) {
19748
+ const trimmed = stdout.trim();
19749
+ if (!trimmed) return [];
19750
+ const parsed = JSON.parse(trimmed);
19751
+ const records = Array.isArray(parsed) ? parsed : [parsed];
19752
+ return records.map((record) => normalizeSnapshot(record)).filter((record) => Boolean(record));
19753
+ }
19754
+ function isManagedCodexExecProcess(processSnapshot, expected) {
19755
+ if (processSnapshot.name.toLowerCase() !== "codex.exe") return false;
19756
+ if (processSnapshot.parentPid !== expected.parentPid) return false;
19757
+ const commandLine = processSnapshot.commandLine;
19758
+ if (!/(?:^|\s)exec(?=\s|$)/i.test(commandLine)) return false;
19759
+ const threadId = escapeRegex(expected.threadId.trim());
19760
+ if (!threadId) return false;
19761
+ const resumePattern = new RegExp(
19762
+ `(?:^|\\s)resume\\s+(?:"${threadId}"|${threadId})(?=\\s|$)`,
19763
+ "i"
19764
+ );
19765
+ return resumePattern.test(commandLine);
19766
+ }
19767
+ async function captureManagedCodexExecProcess(threadId, ownerPid = process.pid) {
19768
+ const normalizedThreadId = threadId.trim();
19769
+ if (process.platform !== "win32" || !normalizedThreadId || !Number.isInteger(ownerPid) || ownerPid <= 0) {
19770
+ return null;
19771
+ }
19772
+ const escapedThreadId = escapePowerShellSingleQuoted2(normalizedThreadId);
19773
+ const script = [
19774
+ `$threadId = '${escapedThreadId}'`,
19775
+ "$escaped = [regex]::Escape($threadId)",
19776
+ `$ownerPid = ${ownerPid}`,
19777
+ "$procs = Get-CimInstance Win32_Process | Where-Object {",
19778
+ " $_.Name -ieq 'codex.exe' -and [int]$_.ParentProcessId -eq $ownerPid -and $_.CommandLine -match $escaped",
19779
+ "} | Select-Object ProcessId, ParentProcessId, CreationDate, Name, CommandLine",
19780
+ "if ($null -eq $procs) { '' } else { $procs | ConvertTo-Json -Compress }"
19781
+ ].join("; ");
19782
+ try {
19783
+ const { stdout } = await execFileAsync2(
19784
+ "powershell.exe",
19785
+ ["-NoProfile", "-NonInteractive", "-Command", script],
19786
+ {
19787
+ windowsHide: true,
19788
+ timeout: PROCESS_QUERY_TIMEOUT_MS,
19789
+ maxBuffer: 512 * 1024,
19790
+ encoding: "utf8"
19791
+ }
19792
+ );
19793
+ const snapshot = parseSnapshots(stdout).find((candidate) => isManagedCodexExecProcess(candidate, {
19794
+ threadId: normalizedThreadId,
19795
+ parentPid: ownerPid
19796
+ }));
19797
+ return snapshot ? {
19798
+ threadId: normalizedThreadId,
19799
+ pid: snapshot.pid,
19800
+ parentPid: snapshot.parentPid,
19801
+ createdAt: snapshot.createdAt
19802
+ } : null;
19803
+ } catch {
19804
+ return null;
19805
+ }
19806
+ }
19807
+ async function inspectManagedCodexProcess(pid) {
19808
+ if (process.platform !== "win32") {
19809
+ return { status: "unsupported", error: "Managed Desktop task stopping is currently supported only on Windows." };
19810
+ }
19811
+ if (!Number.isInteger(pid) || pid <= 0) {
19812
+ return { status: "error", error: "Invalid Codex process id." };
19813
+ }
19814
+ const script = [
19815
+ `$proc = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -First 1 ProcessId, ParentProcessId, CreationDate, Name, CommandLine`,
19816
+ "if ($null -eq $proc) { '' } else { $proc | ConvertTo-Json -Compress }"
19817
+ ].join("; ");
19818
+ try {
19819
+ const { stdout } = await execFileAsync2(
19820
+ "powershell.exe",
19821
+ ["-NoProfile", "-NonInteractive", "-Command", script],
19822
+ {
19823
+ windowsHide: true,
19824
+ timeout: PROCESS_QUERY_TIMEOUT_MS,
19825
+ maxBuffer: 512 * 1024,
19826
+ encoding: "utf8"
19827
+ }
19828
+ );
19829
+ const snapshot = parseSnapshots(stdout)[0];
19830
+ return snapshot ? { status: "found", process: snapshot } : { status: "not_found" };
19831
+ } catch (error) {
19832
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
19833
+ }
19834
+ }
19835
+ async function terminateWindowsProcessTree(pid) {
19836
+ await execFileAsync2(
19837
+ "taskkill.exe",
19838
+ ["/PID", String(pid), "/T", "/F"],
19839
+ {
19840
+ windowsHide: true,
19841
+ timeout: PROCESS_STOP_TIMEOUT_MS,
19842
+ maxBuffer: 512 * 1024
19843
+ }
19844
+ );
19845
+ }
19846
+ function sleep(delayMs) {
19847
+ return new Promise((resolve2) => setTimeout(resolve2, delayMs));
19848
+ }
19849
+ async function stopManagedCodexExecProcess(identity, deps = {}) {
19850
+ const inspectProcess = deps.inspectProcess ?? inspectManagedCodexProcess;
19851
+ const terminateProcessTree = deps.terminateProcessTree ?? terminateWindowsProcessTree;
19852
+ const sleepFn = deps.sleep ?? sleep;
19853
+ const nowMs = deps.nowMs ?? (() => Date.now());
19854
+ const initial = await inspectProcess(identity.pid);
19855
+ if (initial.status === "not_found") return { status: "not_running" };
19856
+ if (initial.status === "unsupported") return initial;
19857
+ if (initial.status === "error") return { status: "failed", error: initial.error };
19858
+ if (initial.process.createdAt !== identity.createdAt || !isManagedCodexExecProcess(initial.process, identity)) {
19859
+ return {
19860
+ status: "unsafe",
19861
+ error: "The recorded process identity no longer matches the bridge-owned Codex exec process."
19862
+ };
19863
+ }
19864
+ try {
19865
+ await terminateProcessTree(identity.pid);
19866
+ } catch (error) {
19867
+ const afterFailure = await inspectProcess(identity.pid);
19868
+ if (afterFailure.status === "not_found") return { status: "stopped" };
19869
+ return { status: "failed", error: error instanceof Error ? error.message : String(error) };
19870
+ }
19871
+ const deadline = nowMs() + PROCESS_EXIT_WAIT_MS;
19872
+ while (nowMs() < deadline) {
19873
+ const inspection = await inspectProcess(identity.pid);
19874
+ if (inspection.status === "not_found") return { status: "stopped" };
19875
+ if (inspection.status === "found") {
19876
+ if (inspection.process.createdAt !== identity.createdAt || !isManagedCodexExecProcess(inspection.process, identity)) {
19877
+ return { status: "stopped" };
19878
+ }
19879
+ } else if (inspection.status === "unsupported") {
19880
+ return inspection;
19881
+ } else {
19882
+ return { status: "failed", error: inspection.error };
19883
+ }
19884
+ await sleepFn(100);
19885
+ }
19886
+ return { status: "failed", error: "Codex process did not exit before the stop timeout." };
19887
+ }
19888
+
19889
+ // src/lib/bridge/desktop-handoff-control.ts
19890
+ function clearHandoffFields() {
19891
+ return {
19892
+ desktop_handoff_task_id: void 0,
19893
+ desktop_handoff_thread_id: void 0,
19894
+ desktop_handoff_turn_id: void 0,
19895
+ desktop_handoff_owner_pid: void 0,
19896
+ desktop_handoff_process_pid: void 0,
19897
+ desktop_handoff_process_parent_pid: void 0,
19898
+ desktop_handoff_process_created_at: void 0,
19899
+ desktop_handoff_registered_at: void 0,
19900
+ desktop_handoff_active: void 0
19901
+ };
19902
+ }
19903
+ function readTask(session) {
19904
+ const sessionId = session?.id || "";
19905
+ const taskId = session?.desktop_handoff_task_id?.trim() || "";
19906
+ const threadId = session?.desktop_handoff_thread_id?.trim() || "";
19907
+ const turnId = session?.desktop_handoff_turn_id?.trim() || "";
19908
+ const registeredAt = session?.desktop_handoff_registered_at?.trim() || "";
19909
+ const ownerPid = Number(session?.desktop_handoff_owner_pid);
19910
+ if (!sessionId || !taskId || !threadId || !turnId || !registeredAt) return null;
19911
+ if (!Number.isInteger(ownerPid) || ownerPid <= 0) return null;
19912
+ const pid = Number(session?.desktop_handoff_process_pid);
19913
+ const parentPid = Number(session?.desktop_handoff_process_parent_pid);
19914
+ const createdAt = session?.desktop_handoff_process_created_at?.trim() || "";
19915
+ const hasProcessIdentity = Number.isInteger(pid) && pid > 0 && Number.isInteger(parentPid) && parentPid > 0 && Boolean(createdAt);
19916
+ return {
19917
+ sessionId,
19918
+ taskId,
19919
+ threadId,
19920
+ turnId,
19921
+ ownerPid,
19922
+ registeredAt,
19923
+ active: session?.desktop_handoff_active === true,
19924
+ processIdentity: hasProcessIdentity ? {
19925
+ threadId,
19926
+ pid,
19927
+ parentPid,
19928
+ createdAt
19929
+ } : null
19930
+ };
19931
+ }
19932
+ function isTerminalRecord2(record) {
19933
+ return record.type === "task_complete" || record.type === "task_aborted";
19934
+ }
19935
+ function createDesktopHandoffControl(deps) {
19936
+ const captureProcess = deps.captureProcess ?? captureManagedCodexExecProcess;
19937
+ const stopProcess = deps.stopProcess ?? stopManagedCodexExecProcess;
19938
+ const nowIso4 = deps.nowIso ?? (() => (/* @__PURE__ */ new Date()).toISOString());
19939
+ const ownerPid = deps.ownerPid ?? process.pid;
19940
+ function releaseTask(sessionId, taskId) {
19941
+ const store = deps.getStore();
19942
+ const current = readTask(store.getSession(sessionId));
19943
+ if (!current) return;
19944
+ if (taskId && current.taskId !== taskId) return;
19945
+ store.updateSession(sessionId, clearHandoffFields(), { touch: false });
19946
+ }
19947
+ async function prepareTask(task) {
19948
+ const store = deps.getStore();
19949
+ const session = store.getSession(task.sessionId);
19950
+ if (!session) return false;
19951
+ const registeredAt = nowIso4();
19952
+ store.updateSession(task.sessionId, {
19953
+ ...clearHandoffFields(),
19954
+ desktop_handoff_task_id: task.taskId,
19955
+ desktop_handoff_thread_id: task.threadId,
19956
+ desktop_handoff_turn_id: task.turnId,
19957
+ desktop_handoff_owner_pid: ownerPid,
19958
+ desktop_handoff_registered_at: registeredAt,
19959
+ desktop_handoff_active: false
19960
+ }, { touch: false });
19961
+ let identity = null;
19962
+ try {
19963
+ identity = await captureProcess(task.threadId, ownerPid);
19964
+ } catch (error) {
19965
+ console.warn(
19966
+ `[desktop-handoff] Failed to capture Codex exec process for ${task.threadId}:`,
19967
+ error instanceof Error ? error.message : error
19968
+ );
19969
+ }
19970
+ if (!identity) return false;
19971
+ const current = readTask(store.getSession(task.sessionId));
19972
+ if (!current || current.taskId !== task.taskId || current.turnId !== task.turnId) return false;
19973
+ store.updateSession(task.sessionId, {
19974
+ desktop_handoff_process_pid: identity.pid,
19975
+ desktop_handoff_process_parent_pid: identity.parentPid,
19976
+ desktop_handoff_process_created_at: identity.createdAt
19977
+ }, { touch: false });
19978
+ return true;
19979
+ }
19980
+ function hasTask(sessionId) {
19981
+ return readTask(deps.getStore().getSession(sessionId))?.active === true;
19982
+ }
19983
+ function activateTask(sessionId, taskId) {
19984
+ const store = deps.getStore();
19985
+ const current = readTask(store.getSession(sessionId));
19986
+ if (!current || current.taskId !== taskId) return false;
19987
+ store.updateSession(sessionId, { desktop_handoff_active: true }, { touch: false });
19988
+ return true;
19989
+ }
19990
+ async function stopTask(sessionId) {
19991
+ const store = deps.getStore();
19992
+ let task = readTask(store.getSession(sessionId));
19993
+ if (!task || !task.active) return { status: "not_running" };
19994
+ if (!task.processIdentity) {
19995
+ let identity = null;
19996
+ try {
19997
+ identity = await captureProcess(task.threadId, task.ownerPid);
19998
+ } catch (error) {
19999
+ return {
20000
+ status: "failed",
20001
+ task,
20002
+ error: error instanceof Error ? error.message : String(error)
20003
+ };
20004
+ }
20005
+ if (!identity) {
20006
+ return {
20007
+ status: "unavailable",
20008
+ task,
20009
+ error: "\u672A\u627E\u5230\u53EF\u9A8C\u8BC1\u4E3A bridge \u81EA\u6709\u7684 Codex exec \u8FDB\u7A0B\u3002"
20010
+ };
20011
+ }
20012
+ store.updateSession(sessionId, {
20013
+ desktop_handoff_process_pid: identity.pid,
20014
+ desktop_handoff_process_parent_pid: identity.parentPid,
20015
+ desktop_handoff_process_created_at: identity.createdAt
20016
+ }, { touch: false });
20017
+ task = { ...task, processIdentity: identity };
20018
+ }
20019
+ const processIdentity = task.processIdentity;
20020
+ if (!processIdentity) {
20021
+ return { status: "unavailable", task, error: "\u672A\u80FD\u4FDD\u5B58\u53EF\u9A8C\u8BC1\u7684 Codex exec \u8FDB\u7A0B\u8EAB\u4EFD\u3002" };
20022
+ }
20023
+ let stopped;
20024
+ try {
20025
+ stopped = await stopProcess(processIdentity);
20026
+ } catch (error) {
20027
+ return {
20028
+ status: "failed",
20029
+ task,
20030
+ error: error instanceof Error ? error.message : String(error)
20031
+ };
20032
+ }
20033
+ if (stopped.status === "stopped") {
20034
+ releaseTask(sessionId, task.taskId);
20035
+ return { status: "stopped", task };
20036
+ }
20037
+ if (stopped.status === "not_running") {
20038
+ releaseTask(sessionId, task.taskId);
20039
+ return { status: "not_running", task };
20040
+ }
20041
+ if (stopped.status === "unsafe") {
20042
+ releaseTask(sessionId, task.taskId);
20043
+ return { status: "unavailable", task, error: stopped.error };
20044
+ }
20045
+ if (stopped.status === "unsupported") {
20046
+ return { status: "unavailable", task, error: stopped.error };
20047
+ }
20048
+ return { status: "failed", task, error: stopped.error };
20049
+ }
20050
+ function observeRecords(sessionId, threadId, records) {
20051
+ if (records.length === 0) return;
20052
+ const task = readTask(deps.getStore().getSession(sessionId));
20053
+ if (!task || task.threadId !== threadId) return;
20054
+ if (records.some((record) => record.turnId === task.turnId && isTerminalRecord2(record))) {
20055
+ releaseTask(sessionId, task.taskId);
20056
+ }
20057
+ }
20058
+ return {
20059
+ prepareTask,
20060
+ activateTask,
20061
+ releaseTask,
20062
+ hasTask,
20063
+ stopTask,
20064
+ observeRecords
20065
+ };
20066
+ }
20067
+
19500
20068
  // src/lib/bridge/bridge-manager.ts
19501
20069
  var GLOBAL_KEY = "__bridge_manager__";
19502
20070
  var DANGLING_MIRROR_THREAD_RETRY_LIMIT = 3;
@@ -19596,6 +20164,10 @@ var INTERACTIVE_RUNTIME = createInteractiveRuntime(getState, {
19596
20164
  getStore: () => getBridgeContext().store,
19597
20165
  nowIso: nowIso3
19598
20166
  });
20167
+ var DESKTOP_HANDOFF_CONTROL = createDesktopHandoffControl({
20168
+ getStore: () => getBridgeContext().store,
20169
+ nowIso: nowIso3
20170
+ });
19599
20171
  function formatDesktopTerminalDetail(terminal) {
19600
20172
  if (terminal.outcome === "aborted") {
19601
20173
  return "\u68C0\u6D4B\u5230\u684C\u9762\u7EBF\u7A0B\u5DF2\u505C\u6B62\u5F53\u524D\u4EFB\u52A1\u3002";
@@ -19679,6 +20251,17 @@ function abortMirrorSuppression2(sessionId, suppressionId) {
19679
20251
  suppressionId
19680
20252
  );
19681
20253
  }
20254
+ function handoffMirrorSuppression2(sessionId, suppressionId) {
20255
+ handoffMirrorSuppression(getMirrorSuppressionStore(), sessionId, suppressionId);
20256
+ }
20257
+ function ignoreMirrorTurn2(sessionId, turnId) {
20258
+ ignoreMirrorTurn(
20259
+ getMirrorSuppressionStore(),
20260
+ sessionId,
20261
+ MIRROR_SUPPRESSION_CONFIG,
20262
+ turnId
20263
+ );
20264
+ }
19682
20265
  function settleMirrorSuppression2(sessionId, suppressionId, durationMs = MIRROR_SUPPRESSION_WINDOW_MS) {
19683
20266
  settleMirrorSuppression(
19684
20267
  getMirrorSuppressionStore(),
@@ -19803,6 +20386,7 @@ var MIRROR_RUNTIME = createMirrorRuntime(getState, {
19803
20386
  syncMirrorSessionStateSafe,
19804
20387
  filterSuppressedMirrorRecords: filterSuppressedMirrorRecords2,
19805
20388
  observeSessionHealthRecords: (sessionId, threadId, records) => {
20389
+ DESKTOP_HANDOFF_CONTROL.observeRecords(sessionId, threadId, records);
19806
20390
  SESSION_HEALTH_RUNTIME.observeDesktopMirrorRecords(sessionId, threadId, records);
19807
20391
  },
19808
20392
  recordOrphanedMirrorTurn: (sessionId, detail) => {
@@ -19835,6 +20419,51 @@ async function reconcileMirrorSubscriptions() {
19835
20419
  function clearMirrorSubscriptions() {
19836
20420
  MIRROR_RUNTIME.clearMirrorSubscriptions();
19837
20421
  }
20422
+ async function forceStopBridgeSession(sessionId, detail) {
20423
+ const activeTask = INTERACTIVE_RUNTIME.getActiveTask(sessionId);
20424
+ const hadQueuedWork = INTERACTIVE_RUNTIME.getQueuedCount(sessionId) > 0;
20425
+ const handoffResult = DESKTOP_HANDOFF_CONTROL.hasTask(sessionId) ? await DESKTOP_HANDOFF_CONTROL.stopTask(sessionId) : null;
20426
+ let interactiveHandled = false;
20427
+ if (activeTask || hadQueuedWork) {
20428
+ interactiveHandled = await INTERACTIVE_RUNTIME.forceStopSession(sessionId, detail);
20429
+ if (activeTask && !handoffResult) {
20430
+ DESKTOP_HANDOFF_CONTROL.releaseTask(sessionId, activeTask.id);
20431
+ }
20432
+ }
20433
+ if (handoffResult) {
20434
+ if (handoffResult.status === "stopped") {
20435
+ ignoreMirrorTurn2(sessionId, handoffResult.task.turnId);
20436
+ MIRROR_RUNTIME.interruptMirrorTurn(
20437
+ sessionId,
20438
+ handoffResult.task.threadId,
20439
+ handoffResult.task.turnId
20440
+ );
20441
+ return { status: "stopped" };
20442
+ }
20443
+ if (handoffResult.status === "unavailable") {
20444
+ return {
20445
+ status: "unavailable",
20446
+ detail: `${interactiveHandled ? "\u5F53\u524D IM \u8BF7\u6C42\u5DF2\u505C\u6B62\uFF0C\u4F46\u6B64\u524D\u7684 Desktop \u4EFB\u52A1\u4ECD\u65E0\u6CD5\u786E\u8BA4\u3002 " : ""}${handoffResult.error}`
20447
+ };
20448
+ }
20449
+ if (handoffResult.status === "failed") {
20450
+ return {
20451
+ status: "failed",
20452
+ detail: `${interactiveHandled ? "\u5F53\u524D IM \u8BF7\u6C42\u5DF2\u505C\u6B62\uFF1B\u6B64\u524D\u7684 Desktop \u4EFB\u52A1\u505C\u6B62\u5931\u8D25\u3002 " : ""}${handoffResult.error}`
20453
+ };
20454
+ }
20455
+ if (!interactiveHandled) {
20456
+ return {
20457
+ status: "not_running",
20458
+ detail: "\u8BB0\u5F55\u4E2D\u7684 Desktop \u4EFB\u52A1\u8FDB\u7A0B\u5DF2\u7ECF\u7ED3\u675F\uFF1B\u5982\u6709\u5C1A\u672A\u6295\u9012\u7684\u7ED3\u679C\uFF0Cmirror \u4F1A\u7EE7\u7EED\u5B8C\u6210\u6536\u5C3E\u3002"
20459
+ };
20460
+ }
20461
+ }
20462
+ if (interactiveHandled) {
20463
+ return activeTask ? { status: "stop_requested" } : { status: "stopped" };
20464
+ }
20465
+ return { status: "not_running" };
20466
+ }
19838
20467
  var ADAPTER_RUNTIME = createAdapterRuntime(getState, {
19839
20468
  notifyAdapterSetChanged: (channelTypes) => {
19840
20469
  const { lifecycle } = getBridgeContext();
@@ -20095,7 +20724,11 @@ async function handleMessage(adapter, msg) {
20095
20724
  recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
20096
20725
  beginMirrorSuppression: beginMirrorSuppression2,
20097
20726
  abortMirrorSuppression: abortMirrorSuppression2,
20727
+ handoffMirrorSuppression: handoffMirrorSuppression2,
20098
20728
  settleMirrorSuppression: settleMirrorSuppression2,
20729
+ prepareDesktopHandoffTask: (task) => DESKTOP_HANDOFF_CONTROL.prepareTask(task),
20730
+ activateDesktopHandoffTask: (sessionId, taskId) => DESKTOP_HANDOFF_CONTROL.activateTask(sessionId, taskId),
20731
+ releaseDesktopHandoffTask: (sessionId, taskId) => DESKTOP_HANDOFF_CONTROL.releaseTask(sessionId, taskId),
20099
20732
  releaseInteractiveTask: (sessionId, taskId) => INTERACTIVE_RUNTIME.releaseInteractiveTask(sessionId, taskId),
20100
20733
  releaseBridgeTurn: (sessionId, taskId) => TURN_COORDINATOR.releaseSessionTurn(sessionId, taskId),
20101
20734
  deliverResponse,
@@ -20107,7 +20740,7 @@ async function handleMessage(adapter, msg) {
20107
20740
  async function handleCommand(adapter, msg, text2) {
20108
20741
  await handleBridgeCommand(adapter, msg, text2, {
20109
20742
  getActiveTask: (sessionId) => INTERACTIVE_RUNTIME.getActiveTask(sessionId),
20110
- forceStopSession: (sessionId, detail) => INTERACTIVE_RUNTIME.forceStopSession(sessionId, detail),
20743
+ forceStopSession: forceStopBridgeSession,
20111
20744
  recordInteractiveHealthEnd: (sessionId, outcome, detail) => SESSION_HEALTH_RUNTIME.recordInteractiveEnd(sessionId, outcome, detail),
20112
20745
  diagnoseSessionHealth: (sessionId) => SESSION_HEALTH_RUNTIME.diagnoseSessionHealth(sessionId),
20113
20746
  diagnoseAllActiveSessions: () => SESSION_HEALTH_RUNTIME.diagnoseAllActiveSessions()
@@ -5548,7 +5548,7 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds, vi
5548
5548
  }
5549
5549
  const lastEventAt = visibleThread?.updatedAtMs ? new Date(visibleThread.updatedAtMs).toISOString() : stat.mtime.toISOString();
5550
5550
  const firstSeenAt = parsed.payload.timestamp || parsed.timestamp || stat.birthtime.toISOString();
5551
- const title = visibleThread?.title || threadIndexEntries.get(threadId)?.title || buildFallbackTitle(threadId, filePath, cwd);
5551
+ const title = threadIndexEntries.get(threadId)?.title || visibleThread?.title || buildFallbackTitle(threadId, filePath, cwd);
5552
5552
  return {
5553
5553
  threadId,
5554
5554
  filePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
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",