cli-wechat-bridge 1.1.1 → 1.1.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 (35) hide show
  1. package/README.md +60 -43
  2. package/bin/_run-entry.mjs +24 -0
  3. package/bin/wechat-bridge-pi.mjs +5 -0
  4. package/bin/wechat-pi-start.mjs +5 -0
  5. package/bin/wechat-pi.mjs +5 -0
  6. package/dist/bridge/bridge-adapter-common.js +2 -0
  7. package/dist/bridge/bridge-adapters.claude.js +18 -0
  8. package/dist/bridge/bridge-adapters.codex.js +83 -2
  9. package/dist/bridge/bridge-adapters.core.js +36 -2
  10. package/dist/bridge/bridge-adapters.js +5 -0
  11. package/dist/bridge/bridge-adapters.opencode.js +278 -32
  12. package/dist/bridge/bridge-adapters.pi.js +697 -0
  13. package/dist/bridge/bridge-adapters.shared.js +44 -46
  14. package/dist/bridge/bridge-final-reply.js +8 -4
  15. package/dist/bridge/bridge-process-reaper.js +58 -3
  16. package/dist/bridge/bridge-state.js +15 -6
  17. package/dist/bridge/bridge-utils.js +129 -30
  18. package/dist/bridge/claude-hook.js +7 -0
  19. package/dist/bridge/claude-hooks.js +10 -2
  20. package/dist/bridge/wechat-bridge.js +20 -8
  21. package/dist/commands/check-update.js +15 -4
  22. package/dist/companion/local-companion-link.js +6 -1
  23. package/dist/companion/local-companion-start.js +29 -8
  24. package/dist/companion/local-companion.js +3 -3
  25. package/dist/companion/pi-tui-bridge-extension.js +256 -0
  26. package/dist/daemon/wechat-daemon.js +126 -25
  27. package/dist/i18n/messages-en.js +12 -2
  28. package/dist/i18n/messages-zh.js +12 -2
  29. package/dist/utils/doctor.js +85 -2
  30. package/dist/utils/version-checker.js +77 -44
  31. package/dist/wechat/channel-config.js +28 -0
  32. package/dist/wechat/setup.js +2 -0
  33. package/dist/wechat/wechat-transport.js +49 -13
  34. package/package.json +120 -100
  35. package/scripts/ensure-node-pty-permissions.mjs +73 -0
@@ -1,5 +1,5 @@
1
1
  import { spawn as spawnChildProcess } from "node:child_process";
2
- import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
2
+ import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, OPENCODE_HTTP_READY_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_INTERVAL_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
3
3
  import { killProcessTreeSync } from "./bridge-process-reaper.js";
4
4
  import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
5
5
  const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
@@ -8,6 +8,8 @@ const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
8
8
  const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
9
9
  const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
10
10
  const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
11
+ const OPENCODE_MINIMUM_SUPPORTED_VERSION = "1.18.0";
12
+ const OPENCODE_MAXIMUM_SUPPORTED_MAJOR = 2;
11
13
  /* ------------------------------------------------------------------ */
12
14
  /* Adapter */
13
15
  /* ------------------------------------------------------------------ */
@@ -31,6 +33,11 @@ export class OpenCodeServerAdapter {
31
33
  workingNoticeTimer = null;
32
34
  workingNoticeSent = false;
33
35
  lastBusyAtMs = 0;
36
+ // Monotonic token bumped on every beginTrackedTurn. Lets a deferred turn
37
+ // completion (.then in handleSessionIdle) detect that a newer turn started
38
+ // while it was awaiting, so the stale completion becomes a no-op instead of
39
+ // clobbering the in-flight turn (stale final_reply / status flip / batcher clear).
40
+ currentTurnToken = 0;
34
41
  pendingLocalPrompt = "";
35
42
  localPromptNoticeSent = false;
36
43
  loggedUnknownEventTypes = new Set();
@@ -48,6 +55,7 @@ export class OpenCodeServerAdapter {
48
55
  lastMirroredLocalPrompt = null;
49
56
  pendingLocalSessionCreateFollowUntilMs = 0;
50
57
  pendingPermission = null;
58
+ pendingQuestion = null;
51
59
  constructor(options) {
52
60
  this.options = options;
53
61
  this.state = {
@@ -112,6 +120,9 @@ export class OpenCodeServerAdapter {
112
120
  if (this.pendingPermission) {
113
121
  throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
114
122
  }
123
+ if (this.pendingQuestion) {
124
+ throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
125
+ }
115
126
  const normalized = normalizeOutput(text).trim();
116
127
  if (!normalized) {
117
128
  return;
@@ -165,6 +176,9 @@ export class OpenCodeServerAdapter {
165
176
  if (this.pendingPermission) {
166
177
  throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
167
178
  }
179
+ if (this.pendingQuestion) {
180
+ throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
181
+ }
168
182
  this.outputBatcher.clear();
169
183
  this.clearStreamedPartState();
170
184
  const session = this.unwrapOrThrow(await this.client.session.create({
@@ -185,10 +199,16 @@ export class OpenCodeServerAdapter {
185
199
  if (!this.client || !this.activeSessionId) {
186
200
  return false;
187
201
  }
188
- if (this.state.status !== "busy" && this.state.status !== "awaiting_approval") {
202
+ if (this.state.status !== "busy" &&
203
+ this.state.status !== "awaiting_approval" &&
204
+ this.state.status !== "awaiting_input") {
189
205
  return false;
190
206
  }
191
207
  this.clearWechatWorkingNotice(true);
208
+ if (this.pendingQuestion) {
209
+ await this.rejectPendingQuestion(this.pendingQuestion);
210
+ this.clearPendingQuestionState();
211
+ }
192
212
  try {
193
213
  await this.client.session.abort({
194
214
  sessionID: this.activeSessionId,
@@ -209,6 +229,7 @@ export class OpenCodeServerAdapter {
209
229
  this.clearObservedMessageTracking();
210
230
  this.recentSdkEventObservations.clear();
211
231
  this.clearPendingPermissionState();
232
+ this.clearPendingQuestionState();
212
233
  this.activeSessionId = null;
213
234
  this.state.sharedSessionId = undefined;
214
235
  this.state.sharedThreadId = undefined;
@@ -227,15 +248,14 @@ export class OpenCodeServerAdapter {
227
248
  if (!this.pendingPermission || !this.client) {
228
249
  return false;
229
250
  }
230
- const { sessionId, permissionId } = this.pendingPermission;
251
+ const { permissionId } = this.pendingPermission;
231
252
  const response = action === "confirm" ? "once" : "reject";
232
253
  try {
233
- const result = await this.client.permission.respond({
234
- sessionID: sessionId,
235
- permissionID: permissionId,
254
+ const result = await this.client.permission.reply({
255
+ requestID: permissionId,
236
256
  directory: this.options.cwd,
237
257
  workspace: this.activeWorkspaceId ?? undefined,
238
- response,
258
+ reply: response,
239
259
  });
240
260
  if (result.error !== undefined) {
241
261
  throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
@@ -258,8 +278,34 @@ export class OpenCodeServerAdapter {
258
278
  const ok = await this.resolveApproval(action);
259
279
  return ok ? 1 : 0;
260
280
  }
261
- async submitUserInput(_answers) {
262
- return false;
281
+ async submitUserInput(answers) {
282
+ if (!this.pendingQuestion || !this.client) {
283
+ return false;
284
+ }
285
+ const pending = this.pendingQuestion;
286
+ const orderedAnswers = pending.request.questions.map((question) => answers[question.id] ?? []);
287
+ try {
288
+ const result = await this.client.question.reply({
289
+ requestID: pending.requestId,
290
+ directory: this.options.cwd,
291
+ workspace: this.activeWorkspaceId ?? undefined,
292
+ answers: orderedAnswers,
293
+ });
294
+ if (result.error !== undefined) {
295
+ throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
296
+ }
297
+ }
298
+ catch (err) {
299
+ this.emit({
300
+ type: "stderr",
301
+ text: `Failed to submit OpenCode user input: ${describeUnknownError(err)}`,
302
+ timestamp: nowIso(),
303
+ });
304
+ return false;
305
+ }
306
+ this.clearPendingQuestionState();
307
+ this.setStatus("busy", "OpenCode user input submitted.");
308
+ return true;
263
309
  }
264
310
  async dispose() {
265
311
  this.shuttingDown = true;
@@ -272,6 +318,7 @@ export class OpenCodeServerAdapter {
272
318
  this.outputBatcher.clear();
273
319
  this.clearStreamedPartState();
274
320
  this.clearPendingPermissionState();
321
+ this.clearPendingQuestionState();
275
322
  // Stop SSE listener
276
323
  if (this.sseAbortController) {
277
324
  this.sseAbortController.abort();
@@ -453,10 +500,54 @@ export class OpenCodeServerAdapter {
453
500
  }
454
501
  async checkHealth() {
455
502
  const baseUrl = `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`;
456
- const response = await fetch(`${baseUrl}/session/status`);
457
- if (!response.ok) {
458
- throw new Error(`OpenCode health check failed (HTTP ${response.status}).`);
503
+ // `opencode serve` binds its TCP port before the HTTP layer is ready,
504
+ // so a fetch fired the instant `waitForTcpPort` returns can be accepted
505
+ // into the listen backlog but never answered, hanging until undici's
506
+ // ~300s `headersTimeout` and surfacing as `fetch failed` after ~5min.
507
+ // Retry with a short per-attempt timeout so the startup race
508
+ // self-heals once the server finishes booting.
509
+ const deadline = Date.now() + OPENCODE_HTTP_READY_TIMEOUT_MS;
510
+ let lastError;
511
+ while (Date.now() < deadline) {
512
+ try {
513
+ const response = await fetch(`${baseUrl}/global/health`, {
514
+ signal: AbortSignal.timeout(OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS),
515
+ });
516
+ if (response.ok) {
517
+ const health = await response.json();
518
+ if (!isRecord(health) || health.healthy !== true || typeof health.version !== "string") {
519
+ lastError = new Error("OpenCode health check returned an invalid response.");
520
+ }
521
+ else if (!this.isSupportedOpenCodeVersion(health.version)) {
522
+ throw new Error(`Unsupported OpenCode version ${health.version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
523
+ }
524
+ else {
525
+ return;
526
+ }
527
+ }
528
+ else {
529
+ lastError = new Error(`OpenCode health check failed (HTTP ${response.status}).`);
530
+ }
531
+ }
532
+ catch (err) {
533
+ if (err instanceof Error &&
534
+ err.message.startsWith("Unsupported OpenCode version ")) {
535
+ throw err;
536
+ }
537
+ lastError = err;
538
+ }
539
+ await delay(OPENCODE_HTTP_READY_PROBE_INTERVAL_MS);
459
540
  }
541
+ throw lastError ?? new Error("OpenCode health check timed out.");
542
+ }
543
+ isSupportedOpenCodeVersion(version) {
544
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
545
+ if (!match) {
546
+ return false;
547
+ }
548
+ const major = Number(match[1]);
549
+ const minor = Number(match[2]);
550
+ return major === 1 && minor >= 18;
460
551
  }
461
552
  async initializeSessions() {
462
553
  if (!this.client) {
@@ -563,11 +654,11 @@ export class OpenCodeServerAdapter {
563
654
  return;
564
655
  }
565
656
  this.sseAbortController = new AbortController();
566
- this.sseLoopPromise = Promise.all([
657
+ const sseLoops = [
567
658
  this.runSseLoop("event"),
568
659
  this.runSseLoop("global-event"),
569
- this.runSseLoop("global-sync"),
570
- ]).then(() => undefined);
660
+ ];
661
+ this.sseLoopPromise = Promise.all(sseLoops).then(() => undefined);
571
662
  }
572
663
  async runSseLoop(streamName) {
573
664
  while (!this.shuttingDown) {
@@ -602,9 +693,6 @@ export class OpenCodeServerAdapter {
602
693
  }
603
694
  subscribeToSseStream(streamName) {
604
695
  const signal = this.sseAbortController?.signal;
605
- if (streamName === "global-sync") {
606
- return this.client.global.syncEvent.subscribe({ signal });
607
- }
608
696
  if (streamName === "global-event") {
609
697
  return this.client.global.event({ signal });
610
698
  }
@@ -618,10 +706,16 @@ export class OpenCodeServerAdapter {
618
706
  return null;
619
707
  }
620
708
  if (typeof rawEvent.type === "string") {
709
+ if (rawEvent.type === "sync") {
710
+ return this.normalizeGlobalSyncPayload(rawEvent, rawEvent);
711
+ }
621
712
  return rawEvent;
622
713
  }
623
714
  const payload = rawEvent.payload;
624
715
  if (isRecord(payload) && typeof payload.type === "string") {
716
+ if (payload.type === "sync") {
717
+ return this.normalizeGlobalSyncPayload(payload, rawEvent);
718
+ }
625
719
  const normalized = { ...payload };
626
720
  if (typeof rawEvent.directory === "string") {
627
721
  normalized.directory = rawEvent.directory;
@@ -630,6 +724,17 @@ export class OpenCodeServerAdapter {
630
724
  }
631
725
  return null;
632
726
  }
727
+ normalizeGlobalSyncPayload(payload, envelope) {
728
+ const syncEvent = isRecord(payload.syncEvent) ? payload.syncEvent : undefined;
729
+ if (!syncEvent || typeof syncEvent.type !== "string") {
730
+ return null;
731
+ }
732
+ const normalized = { ...syncEvent };
733
+ if (typeof envelope.directory === "string") {
734
+ normalized.directory = envelope.directory;
735
+ }
736
+ return normalized;
737
+ }
633
738
  shouldHandleSseEvent(event, streamName) {
634
739
  if (streamName === "event") {
635
740
  return true;
@@ -642,7 +747,10 @@ export class OpenCodeServerAdapter {
642
747
  type !== "command.executed" &&
643
748
  type !== "session.created" &&
644
749
  type !== "session.updated" &&
645
- type !== "session.deleted") {
750
+ type !== "session.deleted" &&
751
+ type !== "question.asked" &&
752
+ type !== "question.replied" &&
753
+ type !== "question.rejected") {
646
754
  return false;
647
755
  }
648
756
  if (this.isImplicitLocalCompanionUiEvent(type)) {
@@ -651,13 +759,6 @@ export class OpenCodeServerAdapter {
651
759
  return (this.matchesCurrentDirectoryEvent(event) ||
652
760
  this.shouldAcceptUnscopedLocalSessionCreatedEvent(event));
653
761
  }
654
- if (type === "session.created" ||
655
- type === "session.updated" ||
656
- type === "session.deleted") {
657
- return (this.matchesCurrentDirectoryEvent(event) ||
658
- (type === "session.created" &&
659
- this.shouldAcceptUnscopedLocalSessionCreatedEvent(event)));
660
- }
661
762
  return false;
662
763
  }
663
764
  isImplicitLocalCompanionUiEvent(type) {
@@ -696,6 +797,15 @@ export class OpenCodeServerAdapter {
696
797
  this.handlePermissionRequest(payload);
697
798
  return;
698
799
  }
800
+ case "question.asked": {
801
+ this.handleQuestionAsked(payload);
802
+ return;
803
+ }
804
+ case "question.replied":
805
+ case "question.rejected": {
806
+ this.handleQuestionSettled(payload);
807
+ return;
808
+ }
699
809
  case "session.created": {
700
810
  this.handleSessionCreated(payload);
701
811
  return;
@@ -757,26 +867,42 @@ export class OpenCodeServerAdapter {
757
867
  if (!this.syncTrackedSessionFromEvent(sessionId, { allowLocalTurnFollow: false })) {
758
868
  return;
759
869
  }
760
- if (this.state.status !== "busy" && this.state.status !== "awaiting_approval") {
870
+ if (this.state.status !== "busy" &&
871
+ this.state.status !== "awaiting_approval" &&
872
+ this.state.status !== "awaiting_input") {
761
873
  return;
762
874
  }
763
875
  // Wait a short settle time before emitting task_complete,
764
876
  // in case more events follow the idle signal.
765
877
  setTimeout(() => {
766
- if (this.state.status !== "busy" && this.state.status !== "awaiting_approval") {
878
+ if (this.state.status !== "busy" &&
879
+ this.state.status !== "awaiting_approval" &&
880
+ this.state.status !== "awaiting_input") {
767
881
  return;
768
882
  }
769
883
  this.clearWechatWorkingNotice(true);
770
884
  this.pendingLocalPrompt = "";
771
885
  this.clearPendingPermissionState();
886
+ this.clearPendingQuestionState();
772
887
  this.state.activeTurnOrigin = undefined;
773
888
  this.hasAcceptedInput = false;
774
889
  const completedPreview = this.currentPreview;
775
890
  const turnStartedAtMs = this.lastBusyAtMs;
891
+ const completingTurnToken = this.currentTurnToken;
776
892
  void this.outputBatcher.flushNow()
777
893
  .catch(() => undefined)
778
894
  .then(async () => {
895
+ // A newer turn (local or wechat) may have started while we awaited
896
+ // the flush. If so, this completion is stale: dropping it avoids
897
+ // flipping the active turn back to idle, emitting the old final_reply,
898
+ // and clearing the new turn's buffered output.
899
+ if (this.currentTurnToken !== completingTurnToken) {
900
+ return;
901
+ }
779
902
  const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
903
+ if (this.currentTurnToken !== completingTurnToken) {
904
+ return;
905
+ }
780
906
  this.setStatus("idle");
781
907
  if (finalReplyText) {
782
908
  this.emit({
@@ -858,6 +984,114 @@ export class OpenCodeServerAdapter {
858
984
  this.state.pendingApproval = null;
859
985
  this.state.pendingApprovalOrigin = undefined;
860
986
  }
987
+ handleQuestionAsked(properties) {
988
+ if (!isRecord(properties) || !this.client) {
989
+ return;
990
+ }
991
+ const sessionId = this.extractSessionId(properties);
992
+ if (!sessionId || !this.syncTrackedSessionFromEvent(sessionId)) {
993
+ return;
994
+ }
995
+ const requestId = typeof properties.id === "string" ? properties.id : null;
996
+ if (!requestId || !Array.isArray(properties.questions)) {
997
+ return;
998
+ }
999
+ const questions = properties.questions.flatMap((candidate, index) => {
1000
+ if (!isRecord(candidate)) {
1001
+ return [];
1002
+ }
1003
+ const header = typeof candidate.header === "string" ? candidate.header.trim() : "";
1004
+ const question = typeof candidate.question === "string" ? candidate.question.trim() : "";
1005
+ if (!header || !question) {
1006
+ return [];
1007
+ }
1008
+ const options = Array.isArray(candidate.options)
1009
+ ? candidate.options.flatMap((option) => {
1010
+ if (!isRecord(option) || typeof option.label !== "string") {
1011
+ return [];
1012
+ }
1013
+ return [{
1014
+ label: option.label,
1015
+ description: typeof option.description === "string" ? option.description : "",
1016
+ }];
1017
+ })
1018
+ : [];
1019
+ return [{
1020
+ id: `question_${index + 1}`,
1021
+ header,
1022
+ question,
1023
+ isOther: candidate.custom === true,
1024
+ isSecret: false,
1025
+ multiple: candidate.multiple === true,
1026
+ customAnswerMode: "value",
1027
+ options,
1028
+ }];
1029
+ });
1030
+ if (questions.length !== properties.questions.length || questions.length === 0) {
1031
+ return;
1032
+ }
1033
+ const request = {
1034
+ summary: `OpenCode requested answers to ${questions.length} question${questions.length === 1 ? "" : "s"}.`,
1035
+ questions,
1036
+ };
1037
+ this.pendingQuestion = {
1038
+ sessionId,
1039
+ requestId,
1040
+ request,
1041
+ };
1042
+ this.state.pendingUserInput = request;
1043
+ this.state.pendingUserInputOrigin = this.state.activeTurnOrigin;
1044
+ this.clearWechatWorkingNotice();
1045
+ this.setStatus("awaiting_input", "OpenCode is waiting for user input.");
1046
+ this.emit({
1047
+ type: "user_input_required",
1048
+ request,
1049
+ timestamp: nowIso(),
1050
+ });
1051
+ }
1052
+ handleQuestionSettled(properties) {
1053
+ if (!isRecord(properties) || !this.pendingQuestion) {
1054
+ return;
1055
+ }
1056
+ const sessionId = this.extractSessionId(properties);
1057
+ const requestId = typeof properties.requestID === "string" ? properties.requestID : null;
1058
+ if (sessionId !== this.pendingQuestion.sessionId ||
1059
+ requestId !== this.pendingQuestion.requestId) {
1060
+ return;
1061
+ }
1062
+ this.clearPendingQuestionState();
1063
+ if (this.state.status === "awaiting_input") {
1064
+ this.setStatus("busy", "OpenCode user input resolved.");
1065
+ }
1066
+ }
1067
+ clearPendingQuestionState() {
1068
+ this.pendingQuestion = null;
1069
+ this.state.pendingUserInput = null;
1070
+ this.state.pendingUserInputOrigin = undefined;
1071
+ }
1072
+ async rejectPendingQuestion(pending) {
1073
+ const client = this.client;
1074
+ if (!client) {
1075
+ return;
1076
+ }
1077
+ try {
1078
+ const result = await client.question.reject({
1079
+ requestID: pending.requestId,
1080
+ directory: this.options.cwd,
1081
+ workspace: this.activeWorkspaceId ?? undefined,
1082
+ });
1083
+ if (result.error !== undefined) {
1084
+ throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
1085
+ }
1086
+ }
1087
+ catch (err) {
1088
+ this.emit({
1089
+ type: "stderr",
1090
+ text: `Failed to reject OpenCode user input: ${describeUnknownError(err)}`,
1091
+ timestamp: nowIso(),
1092
+ });
1093
+ }
1094
+ }
861
1095
  getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
862
1096
  const metadata = isRecord(properties.metadata) ? properties.metadata : {};
863
1097
  const command = this.extractPermissionCommand(properties, metadata);
@@ -889,12 +1123,11 @@ export class OpenCodeServerAdapter {
889
1123
  }
890
1124
  void (async () => {
891
1125
  try {
892
- const result = await client.permission.respond({
893
- sessionID: pendingPermission.sessionId,
894
- permissionID: pendingPermission.permissionId,
1126
+ const result = await client.permission.reply({
1127
+ requestID: pendingPermission.permissionId,
895
1128
  directory: this.options.cwd,
896
1129
  workspace: this.activeWorkspaceId ?? undefined,
897
- response: "reject",
1130
+ reply: "reject",
898
1131
  });
899
1132
  if (result.error !== undefined) {
900
1133
  throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
@@ -1618,6 +1851,7 @@ export class OpenCodeServerAdapter {
1618
1851
  return session;
1619
1852
  }
1620
1853
  beginTrackedTurn(text, origin, options = {}) {
1854
+ this.currentTurnToken += 1;
1621
1855
  this.hasAcceptedInput = true;
1622
1856
  this.currentPreview = truncatePreview(text);
1623
1857
  this.state.lastInputAt = nowIso();
@@ -1644,8 +1878,10 @@ export class OpenCodeServerAdapter {
1644
1878
  hasTrackedTurnState() {
1645
1879
  return (this.state.status === "busy" ||
1646
1880
  this.state.status === "awaiting_approval" ||
1881
+ this.state.status === "awaiting_input" ||
1647
1882
  this.hasAcceptedInput ||
1648
1883
  this.pendingPermission !== null ||
1884
+ this.pendingQuestion !== null ||
1649
1885
  this.state.activeTurnOrigin !== undefined ||
1650
1886
  this.currentPreview !== "(idle)");
1651
1887
  }
@@ -1654,6 +1890,7 @@ export class OpenCodeServerAdapter {
1654
1890
  this.pendingLocalPrompt = "";
1655
1891
  this.localPromptNoticeSent = false;
1656
1892
  this.clearPendingPermissionState();
1893
+ this.clearPendingQuestionState();
1657
1894
  this.state.activeTurnOrigin = undefined;
1658
1895
  this.hasAcceptedInput = false;
1659
1896
  this.currentPreview = "(idle)";
@@ -2224,6 +2461,15 @@ export class OpenCodeServerAdapter {
2224
2461
  const sessionId = this.extractSessionId(payload);
2225
2462
  return sessionId ? `${type}:${sessionId}` : null;
2226
2463
  }
2464
+ case "question.asked": {
2465
+ const requestId = typeof payload.id === "string" ? payload.id : undefined;
2466
+ return requestId ? `${type}:${requestId}` : null;
2467
+ }
2468
+ case "question.replied":
2469
+ case "question.rejected": {
2470
+ const requestId = typeof payload.requestID === "string" ? payload.requestID : undefined;
2471
+ return requestId ? `${type}:${requestId}` : null;
2472
+ }
2227
2473
  default:
2228
2474
  return null;
2229
2475
  }