cli-wechat-bridge 1.1.4 → 1.1.5
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/README.md +14 -7
- package/bin/_run-entry.mjs +0 -12
- package/dist/bridge/bridge-adapters.claude.js +427 -10
- package/dist/bridge/bridge-adapters.codex.js +235 -7
- package/dist/bridge/bridge-adapters.opencode.js +392 -53
- package/dist/bridge/bridge-adapters.pi.js +32 -6
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-utils.js +26 -5
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +37 -29
- package/dist/companion/codex-remote-client.js +291 -32
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +26 -0
- package/dist/companion/local-companion-start.js +0 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/daemon/wechat-daemon.js +35 -17
- package/package.json +1 -5
- package/bin/wechat-claude-start.mjs +0 -10
- package/bin/wechat-codex-start.mjs +0 -10
- package/bin/wechat-opencode-start.mjs +0 -10
- package/bin/wechat-pi-start.mjs +0 -10
|
@@ -6,11 +6,53 @@ import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./
|
|
|
6
6
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
7
7
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
8
8
|
import * as shared from "./bridge-adapters.shared.js";
|
|
9
|
+
import { requestCodexVisibleThreadSwitch } from "../companion/codex-visible-client-link.js";
|
|
9
10
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
10
11
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
11
12
|
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
12
13
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
13
14
|
const CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS = 120_000;
|
|
15
|
+
function getCodexThreadStatusType(value) {
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return isRecord(value) && typeof value.type === "string" ? value.type : null;
|
|
20
|
+
}
|
|
21
|
+
function getCodexThreadTitle(thread) {
|
|
22
|
+
const name = typeof thread.name === "string" ? normalizeOutput(thread.name).trim() : "";
|
|
23
|
+
const preview = typeof thread.preview === "string" ? normalizeOutput(thread.preview).trim() : "";
|
|
24
|
+
const threadId = typeof thread.id === "string" ? thread.id : "";
|
|
25
|
+
return truncatePreview(name || preview || `Codex thread ${threadId.slice(0, 8)}`, 120);
|
|
26
|
+
}
|
|
27
|
+
export function buildCodexResumeCandidatesFromThreadList(response, cwd, limit) {
|
|
28
|
+
if (!isRecord(response) || !Array.isArray(response.data)) {
|
|
29
|
+
throw new Error("Codex returned an invalid thread/list response.");
|
|
30
|
+
}
|
|
31
|
+
const currentCwd = normalizeComparablePath(cwd);
|
|
32
|
+
return response.data
|
|
33
|
+
.filter((thread) => isRecord(thread))
|
|
34
|
+
.filter((thread) => typeof thread.id === "string")
|
|
35
|
+
.filter((thread) => {
|
|
36
|
+
const threadCwd = typeof thread.cwd === "string" ? thread.cwd : "";
|
|
37
|
+
return threadCwd && normalizeComparablePath(threadCwd) === currentCwd;
|
|
38
|
+
})
|
|
39
|
+
.filter((thread) => typeof thread.parentThreadId !== "string")
|
|
40
|
+
.map((thread) => {
|
|
41
|
+
const updatedAt = typeof thread.updatedAt === "number" && Number.isFinite(thread.updatedAt)
|
|
42
|
+
? thread.updatedAt * 1_000
|
|
43
|
+
: 0;
|
|
44
|
+
const threadId = thread.id;
|
|
45
|
+
return {
|
|
46
|
+
sessionId: threadId,
|
|
47
|
+
threadId,
|
|
48
|
+
title: getCodexThreadTitle(thread),
|
|
49
|
+
lastUpdatedAt: new Date(updatedAt).toISOString(),
|
|
50
|
+
source: "codex",
|
|
51
|
+
};
|
|
52
|
+
})
|
|
53
|
+
.sort((left, right) => Date.parse(right.lastUpdatedAt) - Date.parse(left.lastUpdatedAt))
|
|
54
|
+
.slice(0, Math.max(1, limit));
|
|
55
|
+
}
|
|
14
56
|
export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
15
57
|
runtimeKind = "codex_runtime_host";
|
|
16
58
|
appServer = null;
|
|
@@ -37,6 +79,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
37
79
|
pendingTurnThreadId = null;
|
|
38
80
|
interruptPendingTurnStart = false;
|
|
39
81
|
pendingThreadFollowId = null;
|
|
82
|
+
pendingVisibleResume = null;
|
|
40
83
|
pendingApprovalRequests = [];
|
|
41
84
|
pendingUserInputRequest = null;
|
|
42
85
|
pendingUserInputTimer = null;
|
|
@@ -165,13 +208,16 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
165
208
|
this.writeToPty("\r");
|
|
166
209
|
}
|
|
167
210
|
async listResumeSessions(limit = 10) {
|
|
211
|
+
if (this.usesRpcTurnTransport()) {
|
|
212
|
+
return await this.listAppServerResumeSessions(limit);
|
|
213
|
+
}
|
|
168
214
|
return listCodexResumeSessions(this.options.cwd, limit);
|
|
169
215
|
}
|
|
170
216
|
async resumeSession(threadId) {
|
|
171
|
-
if (this.
|
|
172
|
-
throw new Error('WeChat /resume
|
|
217
|
+
if (!this.isHeadlessRuntimeMode()) {
|
|
218
|
+
throw new Error('WeChat /resume requires the managed "wechat-codex" visible client.');
|
|
173
219
|
}
|
|
174
|
-
await this.
|
|
220
|
+
await this.resumeVisibleSharedThread(threadId);
|
|
175
221
|
}
|
|
176
222
|
async interrupt() {
|
|
177
223
|
if (this.usesRpcTurnTransport()) {
|
|
@@ -292,6 +338,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
292
338
|
profile: this.options.profile,
|
|
293
339
|
sharedSessionId: this.state.sharedSessionId,
|
|
294
340
|
sharedThreadId: this.state.sharedThreadId,
|
|
341
|
+
codexVisibleThreadId: this.state.sharedThreadId,
|
|
295
342
|
resumeConversationId: this.state.resumeConversationId,
|
|
296
343
|
transcriptPath: this.state.transcriptPath,
|
|
297
344
|
startedAt: this.state.startedAt ?? nowIso(),
|
|
@@ -610,7 +657,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
610
657
|
if (this.sessionFilePath && candidate.modifiedAtMs <= currentSessionModifiedAtMs) {
|
|
611
658
|
return;
|
|
612
659
|
}
|
|
613
|
-
if (!this.activeTurn ||
|
|
660
|
+
if (!this.activeTurn ||
|
|
661
|
+
this.activeTurn.threadId === candidate.threadId ||
|
|
662
|
+
this.activeTurn.origin === "wechat") {
|
|
614
663
|
this.trackLocalSharedThread(candidate.threadId, {
|
|
615
664
|
reason: "local_session_fallback",
|
|
616
665
|
signal: "session_fallback",
|
|
@@ -1316,6 +1365,147 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1316
1365
|
this.updateSharedThread(resumedThreadId);
|
|
1317
1366
|
}
|
|
1318
1367
|
}
|
|
1368
|
+
async listAppServerResumeSessions(limit) {
|
|
1369
|
+
const boundedLimit = Math.max(1, Math.min(100, limit));
|
|
1370
|
+
const baseParams = {
|
|
1371
|
+
limit: boundedLimit,
|
|
1372
|
+
sortKey: "updated_at",
|
|
1373
|
+
sortDirection: "desc",
|
|
1374
|
+
sourceKinds: ["cli", "vscode"],
|
|
1375
|
+
archived: false,
|
|
1376
|
+
cwd: this.options.cwd,
|
|
1377
|
+
};
|
|
1378
|
+
try {
|
|
1379
|
+
const fastResponse = await this.sendRpcRequest("thread/list", {
|
|
1380
|
+
...baseParams,
|
|
1381
|
+
useStateDbOnly: true,
|
|
1382
|
+
});
|
|
1383
|
+
const fastCandidates = buildCodexResumeCandidatesFromThreadList(fastResponse, this.options.cwd, boundedLimit);
|
|
1384
|
+
if (fastCandidates.length > 0) {
|
|
1385
|
+
return fastCandidates;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
catch {
|
|
1389
|
+
// Older supported app-server builds may not understand useStateDbOnly.
|
|
1390
|
+
// The scan-and-repair request below is also the authoritative fallback.
|
|
1391
|
+
}
|
|
1392
|
+
const response = await this.sendRpcRequest("thread/list", baseParams);
|
|
1393
|
+
return buildCodexResumeCandidatesFromThreadList(response, this.options.cwd, boundedLimit);
|
|
1394
|
+
}
|
|
1395
|
+
validateCodexResumeThread(response, targetThreadId) {
|
|
1396
|
+
if (!isRecord(response) || !isRecord(response.thread)) {
|
|
1397
|
+
throw new Error("Codex returned an invalid thread response.");
|
|
1398
|
+
}
|
|
1399
|
+
const thread = response.thread;
|
|
1400
|
+
if (thread.id !== targetThreadId) {
|
|
1401
|
+
throw new Error(`Codex opened ${String(thread.id)}, expected ${targetThreadId}.`);
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof thread.cwd !== "string" ||
|
|
1404
|
+
normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) {
|
|
1405
|
+
throw new Error(`Codex thread ${targetThreadId} does not belong to ${this.options.cwd}.`);
|
|
1406
|
+
}
|
|
1407
|
+
if (typeof thread.parentThreadId === "string") {
|
|
1408
|
+
throw new Error(`Codex thread ${targetThreadId} is a subagent thread and cannot be resumed.`);
|
|
1409
|
+
}
|
|
1410
|
+
if (thread.ephemeral === true) {
|
|
1411
|
+
throw new Error(`Codex thread ${targetThreadId} is ephemeral and cannot be resumed.`);
|
|
1412
|
+
}
|
|
1413
|
+
if (getCodexThreadStatusType(thread.status) === "active") {
|
|
1414
|
+
throw new Error(`Codex thread ${targetThreadId} is still active. Stop its running task before resuming it from WeChat.`);
|
|
1415
|
+
}
|
|
1416
|
+
return thread;
|
|
1417
|
+
}
|
|
1418
|
+
async resumeVisibleSharedThread(threadId) {
|
|
1419
|
+
const targetThreadId = threadId.trim();
|
|
1420
|
+
if (!targetThreadId) {
|
|
1421
|
+
throw new Error("A thread id is required to resume a Codex thread.");
|
|
1422
|
+
}
|
|
1423
|
+
if (this.pendingVisibleResume) {
|
|
1424
|
+
throw new Error("Codex is already switching visible threads.");
|
|
1425
|
+
}
|
|
1426
|
+
if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
|
|
1427
|
+
throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
|
|
1428
|
+
}
|
|
1429
|
+
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1430
|
+
throw new Error("Codex is waiting for user input. Reply with /answer or use /stop.");
|
|
1431
|
+
}
|
|
1432
|
+
if (this.pendingTurnStart ||
|
|
1433
|
+
this.activeTurn ||
|
|
1434
|
+
this.state.status === "busy" ||
|
|
1435
|
+
this.state.status === "awaiting_approval" ||
|
|
1436
|
+
this.state.status === "awaiting_input") {
|
|
1437
|
+
throw new Error("codex is still working. Wait for the current reply or use /stop.");
|
|
1438
|
+
}
|
|
1439
|
+
if (this.state.status !== "idle") {
|
|
1440
|
+
throw new Error(`Codex cannot switch threads while its status is ${this.state.status}.`);
|
|
1441
|
+
}
|
|
1442
|
+
const readResponse = await this.sendRpcRequest("thread/read", {
|
|
1443
|
+
threadId: targetThreadId,
|
|
1444
|
+
includeTurns: false,
|
|
1445
|
+
});
|
|
1446
|
+
this.validateCodexResumeThread(readResponse, targetThreadId);
|
|
1447
|
+
const previousThreadId = this.sharedThreadId;
|
|
1448
|
+
let targetSubscribed = false;
|
|
1449
|
+
this.pendingVisibleResume = { targetThreadId };
|
|
1450
|
+
this.setStatus("starting", `Switching Codex to thread ${targetThreadId.slice(0, 12)}...`);
|
|
1451
|
+
try {
|
|
1452
|
+
this.rememberBridgeOwnedThreadSignal(targetThreadId);
|
|
1453
|
+
const resumeResponse = await this.sendRpcRequest("thread/resume", {
|
|
1454
|
+
threadId: targetThreadId,
|
|
1455
|
+
approvalPolicy: "on-request",
|
|
1456
|
+
approvalsReviewer: "user",
|
|
1457
|
+
sandbox: "workspace-write",
|
|
1458
|
+
excludeTurns: true,
|
|
1459
|
+
});
|
|
1460
|
+
targetSubscribed = true;
|
|
1461
|
+
this.validateCodexResumeThread(resumeResponse, targetThreadId);
|
|
1462
|
+
this.subscribedThreadIds.add(targetThreadId);
|
|
1463
|
+
const visibleThreadId = await this.switchVisibleCodexThread(targetThreadId);
|
|
1464
|
+
if (visibleThreadId !== targetThreadId) {
|
|
1465
|
+
throw new Error(`The visible Codex client opened ${visibleThreadId}, expected ${targetThreadId}.`);
|
|
1466
|
+
}
|
|
1467
|
+
this.sessionFilePath = null;
|
|
1468
|
+
this.sessionReadOffset = 0;
|
|
1469
|
+
this.sessionPartialLine = "";
|
|
1470
|
+
this.sessionFinalText = null;
|
|
1471
|
+
this.pendingThreadFollowId = null;
|
|
1472
|
+
this.updateSharedThread(targetThreadId, {
|
|
1473
|
+
source: "wechat",
|
|
1474
|
+
reason: "wechat_resume",
|
|
1475
|
+
notify: true,
|
|
1476
|
+
});
|
|
1477
|
+
this.setStatus("idle");
|
|
1478
|
+
if (previousThreadId && previousThreadId !== targetThreadId) {
|
|
1479
|
+
await this.unsubscribeCodexThreadBestEffort(previousThreadId);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
if (targetSubscribed && targetThreadId !== previousThreadId) {
|
|
1484
|
+
await this.unsubscribeCodexThreadBestEffort(targetThreadId);
|
|
1485
|
+
}
|
|
1486
|
+
this.setStatus("idle");
|
|
1487
|
+
throw error;
|
|
1488
|
+
}
|
|
1489
|
+
finally {
|
|
1490
|
+
this.pendingVisibleResume = null;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
async switchVisibleCodexThread(threadId) {
|
|
1494
|
+
return await requestCodexVisibleThreadSwitch({
|
|
1495
|
+
cwd: this.options.cwd,
|
|
1496
|
+
instanceId: this.localClientInstanceId,
|
|
1497
|
+
threadId,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
async unsubscribeCodexThreadBestEffort(threadId) {
|
|
1501
|
+
try {
|
|
1502
|
+
await this.sendRpcRequest("thread/unsubscribe", { threadId });
|
|
1503
|
+
this.subscribedThreadIds.delete(threadId);
|
|
1504
|
+
}
|
|
1505
|
+
catch {
|
|
1506
|
+
// A failed unsubscribe must not undo an otherwise successful visible switch.
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1319
1509
|
async resumeSharedThread(threadId, options = {}) {
|
|
1320
1510
|
const trimmedThreadId = threadId.trim();
|
|
1321
1511
|
if (!trimmedThreadId) {
|
|
@@ -1614,6 +1804,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1614
1804
|
pending.timer.unref?.();
|
|
1615
1805
|
}
|
|
1616
1806
|
trackLocalSharedThread(threadId, options) {
|
|
1807
|
+
if (this.pendingVisibleResume) {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
this.interruptWechatTurnForLocalThreadSwitch(threadId);
|
|
1617
1811
|
if (!this.isNativePanelMode()) {
|
|
1618
1812
|
this.updateSharedThread(threadId, {
|
|
1619
1813
|
source: "local",
|
|
@@ -1655,6 +1849,34 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1655
1849
|
}
|
|
1656
1850
|
this.schedulePendingThreadAnnouncement();
|
|
1657
1851
|
}
|
|
1852
|
+
interruptWechatTurnForLocalThreadSwitch(nextThreadId) {
|
|
1853
|
+
const activeTurn = this.activeTurn;
|
|
1854
|
+
if (!activeTurn ||
|
|
1855
|
+
activeTurn.origin !== "wechat" ||
|
|
1856
|
+
activeTurn.threadId === nextThreadId) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const interruptedTurnId = activeTurn.turnId;
|
|
1860
|
+
void this.sendRpcRequest("turn/interrupt", {
|
|
1861
|
+
threadId: activeTurn.threadId,
|
|
1862
|
+
turnId: interruptedTurnId,
|
|
1863
|
+
}).catch(() => undefined);
|
|
1864
|
+
this.clearInterruptTimer();
|
|
1865
|
+
this.clearFinalReplyCompletionTimerForTurn(interruptedTurnId);
|
|
1866
|
+
this.clearPendingApprovalState();
|
|
1867
|
+
this.clearPendingUserInputState();
|
|
1868
|
+
this.pendingThreadFollowId = null;
|
|
1869
|
+
this.cleanupTurnArtifacts(interruptedTurnId);
|
|
1870
|
+
this.rememberCompletedTurn(interruptedTurnId);
|
|
1871
|
+
this.bridgeOwnedTurnIds.add(interruptedTurnId);
|
|
1872
|
+
this.setActiveTurn(null);
|
|
1873
|
+
this.setStatus("idle");
|
|
1874
|
+
this.emit({
|
|
1875
|
+
type: "task_failed",
|
|
1876
|
+
message: "The active WeChat task was interrupted because the local Codex terminal switched threads.",
|
|
1877
|
+
timestamp: nowIso(),
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1658
1880
|
rememberBridgeOwnedThreadSignal(threadId) {
|
|
1659
1881
|
const cutoff = Date.now() - CODEX_THREAD_SIGNAL_TTL_MS;
|
|
1660
1882
|
for (const [candidateThreadId, recordedAtMs] of this.recentBridgeThreadSignalAtById.entries()) {
|
|
@@ -2171,7 +2393,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2171
2393
|
if (!threadId) {
|
|
2172
2394
|
return;
|
|
2173
2395
|
}
|
|
2174
|
-
if (!this.activeTurn ||
|
|
2396
|
+
if (!this.activeTurn ||
|
|
2397
|
+
this.activeTurn.threadId === threadId ||
|
|
2398
|
+
this.activeTurn.origin === "wechat") {
|
|
2175
2399
|
this.trackLocalSharedThread(threadId, {
|
|
2176
2400
|
reason: "local_follow",
|
|
2177
2401
|
signal: "status_changed",
|
|
@@ -2195,7 +2419,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2195
2419
|
return;
|
|
2196
2420
|
}
|
|
2197
2421
|
}
|
|
2198
|
-
if (!this.activeTurn ||
|
|
2422
|
+
if (!this.activeTurn ||
|
|
2423
|
+
this.activeTurn.threadId === threadId ||
|
|
2424
|
+
this.activeTurn.origin === "wechat") {
|
|
2199
2425
|
this.trackLocalSharedThread(threadId, {
|
|
2200
2426
|
reason: "local_follow",
|
|
2201
2427
|
signal: "thread_started",
|
|
@@ -2211,7 +2437,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2211
2437
|
}
|
|
2212
2438
|
if (trackedTurn.origin === "local" &&
|
|
2213
2439
|
trackedTurn.threadId !== this.sharedThreadId) {
|
|
2214
|
-
if (!this.activeTurn ||
|
|
2440
|
+
if (!this.activeTurn ||
|
|
2441
|
+
this.activeTurn.threadId === trackedTurn.threadId ||
|
|
2442
|
+
this.activeTurn.origin === "wechat") {
|
|
2215
2443
|
this.trackLocalSharedThread(trackedTurn.threadId, {
|
|
2216
2444
|
reason: "local_turn",
|
|
2217
2445
|
signal: "turn_started",
|