@unlinearity/cli-wechat-bridge 1.2.1 → 1.2.3

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.
Files changed (54) hide show
  1. package/README.md +146 -73
  2. package/README_EN.md +544 -0
  3. package/bin/telegram-check-update.mjs +10 -0
  4. package/bin/telegram-claude.mjs +10 -0
  5. package/bin/telegram-codex.mjs +10 -0
  6. package/bin/telegram-daemon.mjs +10 -0
  7. package/bin/telegram-opencode.mjs +10 -0
  8. package/bin/telegram-pi.mjs +10 -0
  9. package/bin/telegram-setup.mjs +10 -0
  10. package/dist/bridge/adapter-control.js +19 -12
  11. package/dist/bridge/bridge-adapters.claude.js +219 -115
  12. package/dist/bridge/bridge-adapters.codex.js +532 -99
  13. package/dist/bridge/bridge-adapters.core.js +84 -7
  14. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  15. package/dist/bridge/bridge-adapters.pi.js +23 -18
  16. package/dist/bridge/bridge-adapters.shared.js +9 -9
  17. package/dist/bridge/bridge-session-resume.js +13 -6
  18. package/dist/bridge/bridge-state.js +3 -1
  19. package/dist/bridge/bridge-utils.js +94 -144
  20. package/dist/bridge/claude-hooks.js +60 -3
  21. package/dist/bridge/claude-native-controls.js +19 -12
  22. package/dist/bridge/language-control.js +33 -0
  23. package/dist/bridge/native-terminal-control.js +26 -10
  24. package/dist/bridge/opencode-v2-compat.js +11 -3
  25. package/dist/bridge/wechat-bridge.js +154 -72
  26. package/dist/channels/channel-driver-factory.js +56 -0
  27. package/dist/channels/local/local-channel-port.js +40 -0
  28. package/dist/channels/local/local-driver.js +217 -0
  29. package/dist/channels/telegram/setup-cli.js +12 -0
  30. package/dist/channels/telegram/setup.js +163 -0
  31. package/dist/channels/telegram/telegram-channel-port.js +47 -0
  32. package/dist/channels/telegram/telegram-config.js +66 -0
  33. package/dist/channels/telegram/telegram-driver.js +106 -0
  34. package/dist/channels/telegram/telegram-message.js +41 -0
  35. package/dist/channels/telegram/telegram-transport.js +378 -0
  36. package/dist/channels/wechat/wechat-driver.js +11 -2
  37. package/dist/channels/wecom/wecom-driver.js +8 -1
  38. package/dist/companion/local-companion-start.js +180 -61
  39. package/dist/companion/local-companion.js +27 -7
  40. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  41. package/dist/core/bridge-control.js +3 -0
  42. package/dist/core/bridge-defer.js +3 -2
  43. package/dist/core/bridge-event-forwarder.js +3 -0
  44. package/dist/core/channel-types.js +13 -1
  45. package/dist/daemon/wechat-daemon.js +239 -126
  46. package/dist/i18n/index.js +25 -11
  47. package/dist/i18n/messages-en.js +245 -1
  48. package/dist/i18n/messages-zh.js +245 -1
  49. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  50. package/dist/wechat/channel-config.js +3 -2
  51. package/package.json +14 -4
  52. package/skills/codex-bridge/SKILL.md +35 -0
  53. package/skills/codex-bridge/agents/openai.yaml +4 -0
  54. package/skills/codex-bridge/scripts/install.mjs +28 -0
@@ -4,6 +4,7 @@
4
4
  // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
5
  // must be released under AGPL-3.0-or-later with full source code; see
6
6
  // LICENSE.txt. Network services built on it must offer source to users.
7
+ import { t } from "../i18n/index.js";
7
8
  import { spawn as spawnChildProcess } from "node:child_process";
8
9
  import { randomBytes } from "node:crypto";
9
10
  import fs from "node:fs";
@@ -97,8 +98,14 @@ export class OpenCodeServerAdapter {
97
98
  lastMirroredLocalPrompt = null;
98
99
  pendingLocalSessionCreateFollowUntilMs = 0;
99
100
  pendingPermission = null;
101
+ pendingPermissions = new Map();
102
+ approvalOwnershipGeneration = 0;
103
+ settledPermissionIds = new Set();
104
+ resolvingPermissions = new Set();
105
+ childSessionParents = new Map();
100
106
  pendingQuestion = null;
101
107
  previousAgentBySession = new Map();
108
+ childSessionIds = new Set();
102
109
  async listModels() {
103
110
  this.assertRemoteControlReady();
104
111
  const session = await this.ensureControlSession();
@@ -116,7 +123,7 @@ export class OpenCodeServerAdapter {
116
123
  const models = await this.listControlModels();
117
124
  const target = models.find((model) => model.id === modelId);
118
125
  if (!target) {
119
- throw new Error("This OpenCode model is no longer available. Send /model again.");
126
+ throw new Error(t("adapter.opencode.modelUnavailable"));
120
127
  }
121
128
  const currentVariant = session.model?.variant;
122
129
  const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
@@ -136,7 +143,7 @@ export class OpenCodeServerAdapter {
136
143
  updated.model?.providerID !== target.providerID ||
137
144
  updated.model.id !== target.modelID ||
138
145
  (updated.model.variant ?? "default") !== variant) {
139
- throw new Error("OpenCode did not confirm the selected model.");
146
+ throw new Error(t("adapter.opencode.modelUnconfirmed"));
140
147
  }
141
148
  return { id: target.id, displayName: target.displayName, isCurrent: true };
142
149
  }
@@ -146,11 +153,11 @@ export class OpenCodeServerAdapter {
146
153
  const agents = await this.listPrimaryAgents();
147
154
  const planAgent = agents.find((agent) => agent.name === "plan");
148
155
  if (!planAgent) {
149
- throw new Error("OpenCode primary agent 'plan' is not available.");
156
+ throw new Error(t("adapter.opencode.planUnavailable"));
150
157
  }
151
158
  const currentAgent = session.agent ?? agents[0]?.name;
152
159
  if (!currentAgent) {
153
- throw new Error("OpenCode did not return any primary agents.");
160
+ throw new Error(t("adapter.opencode.agentsMissing"));
154
161
  }
155
162
  if (enabled && currentAgent === "plan") {
156
163
  return true;
@@ -167,7 +174,7 @@ export class OpenCodeServerAdapter {
167
174
  : agents.find((agent) => agent.name === "build")?.name ??
168
175
  agents.find((agent) => agent.name !== "plan")?.name;
169
176
  if (!target) {
170
- throw new Error("OpenCode did not return an agent that can replace plan mode.");
177
+ throw new Error(t("adapter.opencode.planReplacementMissing"));
171
178
  }
172
179
  const result = await this.client.v2.session.switchAgent({
173
180
  sessionID: session.id,
@@ -176,7 +183,7 @@ export class OpenCodeServerAdapter {
176
183
  this.unwrapOrThrow(result);
177
184
  const updated = await this.getSessionForCurrentDirectory(session.id);
178
185
  if (!updated || updated.agent !== target) {
179
- throw new Error("OpenCode did not confirm the selected agent.");
186
+ throw new Error(t("adapter.opencode.agentUnconfirmed"));
180
187
  }
181
188
  if (enabled) {
182
189
  this.previousAgentBySession.set(session.id, currentAgent);
@@ -188,16 +195,16 @@ export class OpenCodeServerAdapter {
188
195
  }
189
196
  assertRemoteControlReady() {
190
197
  if (!this.client) {
191
- throw new Error("OpenCode is not running.");
198
+ throw new Error(t("adapter.opencode.notRunning"));
192
199
  }
193
200
  if (this.state.status === "busy") {
194
- throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
201
+ throw new Error(t("adapter.opencode.busy"));
195
202
  }
196
203
  if (this.pendingPermission) {
197
- throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
204
+ throw new Error(t("adapter.opencode.approvalPending"));
198
205
  }
199
206
  if (this.pendingQuestion) {
200
- throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
207
+ throw new Error(t("adapter.opencode.inputPending"));
201
208
  }
202
209
  }
203
210
  async ensureControlSession() {
@@ -214,7 +221,7 @@ export class OpenCodeServerAdapter {
214
221
  }
215
222
  async listPrimaryAgents() {
216
223
  if (!this.client)
217
- throw new Error("OpenCode is not running.");
224
+ throw new Error(t("adapter.opencode.notRunning"));
218
225
  const agents = this.unwrapOrThrow(await this.client.app.agents({
219
226
  directory: this.options.cwd,
220
227
  workspace: this.activeWorkspaceId ?? undefined,
@@ -223,7 +230,7 @@ export class OpenCodeServerAdapter {
223
230
  }
224
231
  async listControlModels() {
225
232
  if (!this.client)
226
- throw new Error("OpenCode is not running.");
233
+ throw new Error(t("adapter.opencode.notRunning"));
227
234
  const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
228
235
  return providers.all.filter((provider) => providers.connected.includes(provider.id)).flatMap((provider) => Object.entries(provider.models).filter(([id, model]) => model.status !== "deprecated" && !(provider.id === "opencode" && id.includes("-nano"))).map(([id, model]) => ({
229
236
  id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
@@ -236,6 +243,8 @@ export class OpenCodeServerAdapter {
236
243
  this.state = {
237
244
  kind: options.kind,
238
245
  status: "stopped",
246
+ supportsApprovalRequestIds: true,
247
+ pendingApprovals: [],
239
248
  cwd: options.cwd,
240
249
  command: options.command,
241
250
  profile: options.profile,
@@ -290,16 +299,16 @@ export class OpenCodeServerAdapter {
290
299
  }
291
300
  async sendInput(text) {
292
301
  if (!this.client) {
293
- throw new Error("OpenCode adapter is not running.");
302
+ throw new Error(t("adapter.opencode.adapterNotRunning"));
294
303
  }
295
304
  if (this.state.status === "busy") {
296
- throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
305
+ throw new Error(t("adapter.opencode.busy"));
297
306
  }
298
307
  if (this.pendingPermission) {
299
- throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
308
+ throw new Error(t("adapter.opencode.approvalPending"));
300
309
  }
301
310
  if (this.pendingQuestion) {
302
- throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
311
+ throw new Error(t("adapter.opencode.inputPending"));
303
312
  }
304
313
  const normalized = normalizeOutput(text).trim();
305
314
  if (!normalized) {
@@ -349,7 +358,7 @@ export class OpenCodeServerAdapter {
349
358
  }
350
359
  async listResumeSessions(limit = 10) {
351
360
  if (!this.client) {
352
- throw new Error("OpenCode is not running yet.");
361
+ throw new Error(t("adapter.opencode.notReady"));
353
362
  }
354
363
  try {
355
364
  const sessions = this.unwrapOrThrow(await this.client.session.list({
@@ -385,16 +394,16 @@ export class OpenCodeServerAdapter {
385
394
  }
386
395
  async resumeSession(sessionId) {
387
396
  if (!this.client) {
388
- throw new Error("OpenCode is not running yet.");
397
+ throw new Error(t("adapter.opencode.notReady"));
389
398
  }
390
399
  if (this.pendingPermission) {
391
- throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
400
+ throw new Error(t("adapter.opencode.approvalPending"));
392
401
  }
393
402
  if (this.pendingQuestion) {
394
- throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
403
+ throw new Error(t("adapter.opencode.inputPending"));
395
404
  }
396
405
  if (this.state.status === "busy") {
397
- throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
406
+ throw new Error(t("adapter.opencode.busy"));
398
407
  }
399
408
  if (this.state.status !== "idle") {
400
409
  throw new Error(`OpenCode cannot switch sessions while its status is ${this.state.status}.`);
@@ -422,16 +431,16 @@ export class OpenCodeServerAdapter {
422
431
  }
423
432
  async createSession() {
424
433
  if (!this.client) {
425
- throw new Error("OpenCode adapter is not running.");
434
+ throw new Error(t("adapter.opencode.adapterNotRunning"));
426
435
  }
427
436
  if (this.state.status === "busy") {
428
- throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
437
+ throw new Error(t("adapter.opencode.busy"));
429
438
  }
430
439
  if (this.pendingPermission) {
431
- throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
440
+ throw new Error(t("adapter.opencode.approvalPending"));
432
441
  }
433
442
  if (this.pendingQuestion) {
434
- throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
443
+ throw new Error(t("adapter.opencode.inputPending"));
435
444
  }
436
445
  this.outputBatcher.clear();
437
446
  this.clearStreamedPartState();
@@ -498,22 +507,30 @@ export class OpenCodeServerAdapter {
498
507
  await this.dispose();
499
508
  await this.start();
500
509
  }
501
- async resolveApproval(action) {
502
- if (!this.pendingPermission || !this.client) {
510
+ async resolveApproval(action, requestId) {
511
+ const pending = requestId === undefined
512
+ ? this.pendingPermission
513
+ : this.pendingPermissions.get(requestId);
514
+ if (!pending || !this.client || this.resolvingPermissions.has(pending.permissionId)) {
503
515
  return false;
504
516
  }
505
- const { permissionId } = this.pendingPermission;
506
- const response = action === "confirm" ? "once" : "reject";
517
+ this.resolvingPermissions.add(pending.permissionId);
507
518
  try {
508
519
  const result = await this.client.permission.reply({
509
- requestID: permissionId,
520
+ requestID: pending.permissionId,
521
+ sessionID: pending.sessionId,
510
522
  directory: this.options.cwd,
511
523
  workspace: this.activeWorkspaceId ?? undefined,
512
- reply: response,
524
+ reply: action === "confirm" ? "once" : "reject",
513
525
  });
514
526
  if (result.error !== undefined) {
515
527
  throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
516
528
  }
529
+ this.rememberSettledPermission(pending.permissionId);
530
+ this.pendingPermissions.delete(pending.permissionId);
531
+ this.clearWechatWorkingNotice();
532
+ this.syncPendingPermissions();
533
+ return true;
517
534
  }
518
535
  catch (err) {
519
536
  this.emit({
@@ -523,14 +540,17 @@ export class OpenCodeServerAdapter {
523
540
  });
524
541
  return false;
525
542
  }
526
- this.clearWechatWorkingNotice();
527
- this.clearPendingPermissionState();
528
- this.setStatus("busy");
529
- return true;
543
+ finally {
544
+ this.resolvingPermissions.delete(pending.permissionId);
545
+ }
530
546
  }
531
547
  async resolveAllApprovals(action) {
532
- const ok = await this.resolveApproval(action);
533
- return ok ? 1 : 0;
548
+ let count = 0;
549
+ for (const id of Array.from(this.pendingPermissions.keys())) {
550
+ if (await this.resolveApproval(action, id))
551
+ count++;
552
+ }
553
+ return count;
534
554
  }
535
555
  async submitUserInput(answers) {
536
556
  if (!this.pendingQuestion || !this.client) {
@@ -558,11 +578,14 @@ export class OpenCodeServerAdapter {
558
578
  return false;
559
579
  }
560
580
  this.clearPendingQuestionState();
561
- this.setStatus("busy", "OpenCode user input submitted.");
581
+ this.setStatus(this.pendingPermissions.size ? "awaiting_approval" : "busy", "OpenCode user input submitted.");
562
582
  return true;
563
583
  }
564
584
  async dispose() {
565
585
  this.shuttingDown = true;
586
+ this.childSessionIds.clear();
587
+ this.childSessionParents.clear();
588
+ this.settledPermissionIds.clear();
566
589
  this.clearWechatWorkingNotice(true);
567
590
  this.pendingLocalPrompt = "";
568
591
  this.localPromptNoticeSent = false;
@@ -1355,6 +1378,10 @@ export class OpenCodeServerAdapter {
1355
1378
  }
1356
1379
  // Note: only permission.asked exists in the OpenCode wire protocol;
1357
1380
  // permission.updated was never emitted by the server.
1381
+ case "permission.replied": {
1382
+ this.handlePermissionSettled(payload);
1383
+ return;
1384
+ }
1358
1385
  case "permission.asked": {
1359
1386
  this.handlePermissionRequest(payload);
1360
1387
  return;
@@ -1416,7 +1443,6 @@ export class OpenCodeServerAdapter {
1416
1443
  case "session.diff":
1417
1444
  case "session.diff.delta":
1418
1445
  case "message.removed":
1419
- case "permission.replied":
1420
1446
  case "tui.toast.show":
1421
1447
  return;
1422
1448
  default:
@@ -1523,7 +1549,17 @@ export class OpenCodeServerAdapter {
1523
1549
  return;
1524
1550
  }
1525
1551
  const sessionId = this.extractSessionId(properties);
1526
- if (sessionId && !this.syncTrackedSessionFromEvent(sessionId)) {
1552
+ if (!sessionId)
1553
+ return;
1554
+ if (!this.isApprovalSessionOwned(sessionId)) {
1555
+ const ownerSessionId = this.activeSessionId;
1556
+ const generation = this.approvalOwnershipGeneration;
1557
+ void this.discoverApprovalSessionOwnership(sessionId).then((owned) => {
1558
+ if (owned && ownerSessionId === this.activeSessionId && generation === this.approvalOwnershipGeneration)
1559
+ this.handlePermissionRequest(properties);
1560
+ }).catch((error) => {
1561
+ this.logDebug(`[opencode-adapter:approval-owner] ${describeUnknownError(error)}`);
1562
+ });
1527
1563
  return;
1528
1564
  }
1529
1565
  const pendingPermission = this.buildPendingPermission(properties);
@@ -1536,21 +1572,86 @@ export class OpenCodeServerAdapter {
1536
1572
  this.rejectPermission(pendingPermission, denyMessage);
1537
1573
  return;
1538
1574
  }
1539
- const approval = this.toPendingApproval(pendingPermission);
1540
- this.pendingPermission = pendingPermission;
1541
- this.state.pendingApproval = approval;
1542
- this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
1543
- this.setStatus("awaiting_approval", "OpenCode approval is required.");
1544
- this.emit({
1545
- type: "approval_required",
1546
- request: approval,
1547
- timestamp: nowIso(),
1548
- });
1575
+ if (this.pendingPermissions.has(pendingPermission.permissionId) || this.settledPermissionIds.has(pendingPermission.permissionId))
1576
+ return;
1577
+ this.pendingPermissions.set(pendingPermission.permissionId, pendingPermission);
1578
+ this.syncPendingPermissions();
1579
+ }
1580
+ isApprovalSessionOwned(sessionId) {
1581
+ const seen = new Set();
1582
+ let current = sessionId;
1583
+ while (current && !seen.has(current)) {
1584
+ if (current === this.activeSessionId)
1585
+ return true;
1586
+ seen.add(current);
1587
+ current = this.childSessionParents.get(current);
1588
+ }
1589
+ return false;
1590
+ }
1591
+ async discoverApprovalSessionOwnership(sessionId, seen = new Set()) {
1592
+ if (this.isApprovalSessionOwned(sessionId))
1593
+ return true;
1594
+ if (seen.has(sessionId) || seen.size >= 32 || !this.client?.session?.get)
1595
+ return false;
1596
+ seen.add(sessionId);
1597
+ const result = await this.client.session.get({ sessionID: sessionId, directory: this.options.cwd });
1598
+ const session = result.data;
1599
+ if (!session || !this.isCurrentDirectorySession(session) || !session.parentID)
1600
+ return false;
1601
+ this.childSessionIds.add(sessionId);
1602
+ if (!await this.discoverApprovalSessionOwnership(session.parentID, seen))
1603
+ return false;
1604
+ this.childSessionParents.set(sessionId, session.parentID);
1605
+ return this.isApprovalSessionOwned(sessionId);
1606
+ }
1607
+ rememberSettledPermission(id) {
1608
+ this.settledPermissionIds.add(id);
1609
+ if (this.settledPermissionIds.size > 512) {
1610
+ this.settledPermissionIds.delete(this.settledPermissionIds.values().next().value);
1611
+ }
1612
+ }
1613
+ handlePermissionSettled(properties) {
1614
+ if (!isRecord(properties))
1615
+ return;
1616
+ const id = typeof properties.requestID === "string" ? properties.requestID : properties.id;
1617
+ if (typeof id !== "string")
1618
+ return;
1619
+ const pending = this.pendingPermissions.get(id);
1620
+ if (!pending) {
1621
+ this.rememberSettledPermission(id);
1622
+ return;
1623
+ }
1624
+ if (typeof properties.sessionID === "string" && properties.sessionID !== pending.sessionId)
1625
+ return;
1626
+ this.rememberSettledPermission(id);
1627
+ this.pendingPermissions.delete(id);
1628
+ this.syncPendingPermissions();
1629
+ }
1630
+ syncPendingPermissions() {
1631
+ const previousId = this.state.pendingApproval?.requestId;
1632
+ this.pendingPermission = this.pendingPermissions.values().next().value ?? null;
1633
+ const requests = Array.from(this.pendingPermissions.values(), (pending) => this.toPendingApproval(pending));
1634
+ this.state.pendingApprovals = requests;
1635
+ this.state.pendingApproval = requests[0] ?? null;
1636
+ this.state.pendingApprovalOrigin = this.pendingPermission?.origin;
1637
+ this.setStatus(requests.length ? "awaiting_approval" : this.state.pendingUserInput ? "awaiting_input" : "busy");
1638
+ this.emit({ type: "approval_state_changed", requests, timestamp: nowIso() });
1639
+ if (requests[0] && requests[0].requestId !== previousId) {
1640
+ this.emit({ type: "approval_required", request: requests[0], timestamp: nowIso() });
1641
+ }
1549
1642
  }
1550
1643
  clearPendingPermissionState() {
1644
+ this.approvalOwnershipGeneration++;
1645
+ const hadPending = this.pendingPermissions.size > 0;
1646
+ for (const id of this.pendingPermissions.keys())
1647
+ this.rememberSettledPermission(id);
1648
+ this.pendingPermissions.clear();
1551
1649
  this.pendingPermission = null;
1552
1650
  this.state.pendingApproval = null;
1651
+ this.state.pendingApprovals = [];
1553
1652
  this.state.pendingApprovalOrigin = undefined;
1653
+ if (hadPending)
1654
+ this.emit({ type: "approval_state_changed", requests: [], timestamp: nowIso() });
1554
1655
  }
1555
1656
  handleQuestionAsked(properties) {
1556
1657
  if (!isRecord(properties) || !this.client) {
@@ -1599,7 +1700,7 @@ export class OpenCodeServerAdapter {
1599
1700
  return;
1600
1701
  }
1601
1702
  const request = {
1602
- summary: `OpenCode requested answers to ${questions.length} question${questions.length === 1 ? "" : "s"}.`,
1703
+ summary: t("input.request.opencode", { count: questions.length, questions: t(questions.length === 1 ? "input.request.singular" : "input.request.plural") }),
1603
1704
  questions,
1604
1705
  };
1605
1706
  this.pendingQuestion = {
@@ -1610,7 +1711,7 @@ export class OpenCodeServerAdapter {
1610
1711
  this.state.pendingUserInput = request;
1611
1712
  this.state.pendingUserInputOrigin = this.state.activeTurnOrigin;
1612
1713
  this.clearWechatWorkingNotice();
1613
- this.setStatus("awaiting_input", "OpenCode is waiting for user input.");
1714
+ this.setStatus(this.pendingPermissions.size ? "awaiting_approval" : "awaiting_input", "OpenCode is waiting for user input.");
1614
1715
  this.emit({
1615
1716
  type: "user_input_required",
1616
1717
  request,
@@ -1693,6 +1794,7 @@ export class OpenCodeServerAdapter {
1693
1794
  try {
1694
1795
  const result = await client.permission.reply({
1695
1796
  requestID: pendingPermission.permissionId,
1797
+ sessionID: pendingPermission.sessionId,
1696
1798
  directory: this.options.cwd,
1697
1799
  workspace: this.activeWorkspaceId ?? undefined,
1698
1800
  reply: "reject",
@@ -1736,12 +1838,14 @@ export class OpenCodeServerAdapter {
1736
1838
  const metadata = isRecord(properties.metadata) ? properties.metadata : {};
1737
1839
  const command = this.extractPermissionCommand(properties, metadata);
1738
1840
  return {
1841
+ origin: this.state.activeTurnOrigin,
1739
1842
  sessionId,
1740
1843
  permissionId,
1741
1844
  code: buildOneTimeCode(),
1742
1845
  createdAt: nowIso(),
1743
1846
  request: {
1744
1847
  source: "cli",
1848
+ requestId: permissionId,
1745
1849
  summary: title ?? `OpenCode needs approval${toolName ? ` for tool: ${toolName}` : ""}.`,
1746
1850
  commandPreview: truncatePreview(command ?? title ?? "Permission request", 180),
1747
1851
  toolName,
@@ -1777,6 +1881,17 @@ export class OpenCodeServerAdapter {
1777
1881
  return;
1778
1882
  }
1779
1883
  const session = this.extractSessionReference(properties);
1884
+ if (session?.parentID) {
1885
+ // Subagent creation is not a visible TUI selection. Remember the child
1886
+ // so its later status/output events cannot implicitly follow it either.
1887
+ this.childSessionIds.add(session.id);
1888
+ const directory = this.extractSessionDirectory(properties);
1889
+ if ((!directory || this.normalizeDirectory(directory) === this.normalizeDirectory(this.options.cwd))
1890
+ && (!session.workspaceID || !this.activeWorkspaceId || session.workspaceID === this.activeWorkspaceId)) {
1891
+ this.childSessionParents.set(session.id, session.parentID);
1892
+ }
1893
+ return;
1894
+ }
1780
1895
  if (this.syncTrackedSessionFromEvent(session)) {
1781
1896
  return;
1782
1897
  }
@@ -1802,6 +1917,31 @@ export class OpenCodeServerAdapter {
1802
1917
  if (!sessionId) {
1803
1918
  return;
1804
1919
  }
1920
+ const removedSessions = new Set([sessionId]);
1921
+ let expanded = true;
1922
+ while (expanded) {
1923
+ expanded = false;
1924
+ for (const [child, parent] of this.childSessionParents) {
1925
+ if (removedSessions.has(parent) && !removedSessions.has(child)) {
1926
+ removedSessions.add(child);
1927
+ expanded = true;
1928
+ }
1929
+ }
1930
+ }
1931
+ for (const id of removedSessions) {
1932
+ this.childSessionIds.delete(id);
1933
+ this.childSessionParents.delete(id);
1934
+ }
1935
+ let removedApproval = false;
1936
+ for (const [id, pending] of this.pendingPermissions) {
1937
+ if (removedSessions.has(pending.sessionId)) {
1938
+ this.rememberSettledPermission(id);
1939
+ this.pendingPermissions.delete(id);
1940
+ removedApproval = true;
1941
+ }
1942
+ }
1943
+ if (removedApproval)
1944
+ this.syncPendingPermissions();
1805
1945
  this.previousAgentBySession.delete(sessionId);
1806
1946
  if (sessionId !== this.activeSessionId) {
1807
1947
  return;
@@ -2592,6 +2732,9 @@ export class OpenCodeServerAdapter {
2592
2732
  this.assignActiveSession(session);
2593
2733
  return true;
2594
2734
  }
2735
+ if (this.childSessionIds.has(sessionId)) {
2736
+ return false;
2737
+ }
2595
2738
  if (options.allowLocalTurnFollow !== false && this.shouldFollowLocalTurnSession(sessionId)) {
2596
2739
  this.switchSharedSession(session ?? sessionId, {
2597
2740
  source: "local",
@@ -2604,6 +2747,9 @@ export class OpenCodeServerAdapter {
2604
2747
  return false;
2605
2748
  }
2606
2749
  extractSessionReference(properties) {
2750
+ const session = isRecord(properties.session) ? properties.session : undefined;
2751
+ const info = isRecord(properties.info) ? properties.info : undefined;
2752
+ const parentID = [properties.parentID, session?.parentID, info?.parentID].find((value) => typeof value === "string" && value.length > 0);
2607
2753
  if (typeof properties.sessionID === "string" || typeof properties.sessionId === "string") {
2608
2754
  const id = typeof properties.sessionID === "string"
2609
2755
  ? properties.sessionID
@@ -2611,20 +2757,21 @@ export class OpenCodeServerAdapter {
2611
2757
  return {
2612
2758
  id,
2613
2759
  workspaceID: this.extractWorkspaceId(properties) ?? undefined,
2760
+ parentID,
2614
2761
  };
2615
2762
  }
2616
- const session = isRecord(properties.session) ? properties.session : undefined;
2617
2763
  if (typeof session?.id === "string") {
2618
2764
  return {
2619
2765
  id: session.id,
2620
2766
  workspaceID: this.extractWorkspaceId(session) ?? this.extractWorkspaceId(properties) ?? undefined,
2767
+ parentID,
2621
2768
  };
2622
2769
  }
2623
- const info = isRecord(properties.info) ? properties.info : undefined;
2624
2770
  if (typeof info?.id === "string") {
2625
2771
  return {
2626
2772
  id: info.id,
2627
2773
  workspaceID: this.extractWorkspaceId(info) ?? this.extractWorkspaceId(properties) ?? undefined,
2774
+ parentID,
2628
2775
  };
2629
2776
  }
2630
2777
  return null;
@@ -2670,7 +2817,7 @@ export class OpenCodeServerAdapter {
2670
2817
  return false;
2671
2818
  }
2672
2819
  const session = this.extractSessionReference(payload);
2673
- if (!session?.id || session.id === this.activeSessionId) {
2820
+ if (!session?.id || session.parentID || session.id === this.activeSessionId) {
2674
2821
  return false;
2675
2822
  }
2676
2823
  if (typeof event.directory === "string" ||