opencode-tbot 0.1.24 → 0.1.26

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/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-CCeFjxSf.js";
1
+ import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-DNeV2Ckw.js";
2
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -227,11 +227,13 @@ var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
227
227
  1e3
228
228
  ];
229
229
  var PROMPT_MESSAGE_POLL_INTERVAL_MS = 2e3;
230
- var PROMPT_POLL_REQUEST_TIMEOUT_MS = 15e3;
231
- var PROMPT_SEND_TIMEOUT_MS = 3e4;
232
- var PROMPT_MESSAGE_POLL_TIMEOUT_MS = 6e4;
233
230
  var PROMPT_MESSAGE_POLL_LIMIT = 20;
234
231
  var PROMPT_LOG_SERVICE = "opencode-tbot";
232
+ var DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY = {
233
+ pollRequestTimeoutMs: 15e3,
234
+ recoveryInactivityTimeoutMs: 12e4,
235
+ waitTimeoutMs: 18e5
236
+ };
235
237
  var STRUCTURED_REPLY_SCHEMA = {
236
238
  type: "json_schema",
237
239
  retryCount: 2,
@@ -253,20 +255,39 @@ var StructuredReplySchema = z.object({ body_md: z.string() });
253
255
  var OpenCodeClient = class {
254
256
  client;
255
257
  fetchFn;
256
- promptRequestTimeouts = {
257
- pollRequestMs: PROMPT_POLL_REQUEST_TIMEOUT_MS,
258
- sendMs: PROMPT_SEND_TIMEOUT_MS,
259
- totalPollMs: PROMPT_MESSAGE_POLL_TIMEOUT_MS
260
- };
258
+ sdkFlavor;
259
+ promptTimeoutPolicy;
261
260
  modelCache = {
262
261
  expiresAt: 0,
263
262
  promise: null,
264
263
  value: null
265
264
  };
266
- constructor(options, client, fetchFn = fetch) {
265
+ constructor(options, client, fetchFn = fetch, promptTimeoutPolicy = {}) {
267
266
  if (!options && !client) throw new Error("OpenCodeClient requires either base URL options or an injected SDK client.");
268
267
  this.client = client ?? createOpencodeClient(buildOpenCodeSdkConfig(options));
269
268
  this.fetchFn = fetchFn;
269
+ this.sdkFlavor = detectSdkFlavor(this.client);
270
+ this.promptTimeoutPolicy = resolvePromptTimeoutPolicy(promptTimeoutPolicy);
271
+ }
272
+ configurePromptTimeoutPolicy(promptTimeoutPolicy) {
273
+ this.promptTimeoutPolicy = resolvePromptTimeoutPolicy({
274
+ ...this.promptTimeoutPolicy,
275
+ ...promptTimeoutPolicy
276
+ });
277
+ }
278
+ async callScopedSdkMethod(scope, method, input) {
279
+ const target = this.client[scope];
280
+ const handler = target?.[method];
281
+ if (typeof handler !== "function") throw new Error(`OpenCode SDK client does not expose a compatible ${scope}.${method} method.`);
282
+ if (this.sdkFlavor === "legacy") return unwrapSdkData(await handler.call(target, {
283
+ ...SDK_OPTIONS,
284
+ ...input.signal ? { signal: input.signal } : {},
285
+ ...input.legacyOptions ?? {}
286
+ }));
287
+ return unwrapSdkData(await handler.call(target, input.v2Parameters, {
288
+ ...SDK_OPTIONS,
289
+ ...input.signal ? { signal: input.signal } : {}
290
+ }));
270
291
  }
271
292
  async getHealth() {
272
293
  const rawClient = getRawSdkClient(this.client);
@@ -277,44 +298,70 @@ var OpenCodeClient = class {
277
298
  return this.requestRaw("get", { url: "/global/health" });
278
299
  }
279
300
  async abortSession(sessionId) {
280
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
281
- url: "/session/{sessionID}/abort",
282
- path: { sessionID: sessionId }
301
+ if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", buildRawSessionRequest(this.sdkFlavor, sessionId, "/abort"));
302
+ return this.callScopedSdkMethod("session", "abort", {
303
+ legacyOptions: { path: { id: sessionId } },
304
+ v2Parameters: { sessionID: sessionId }
305
+ });
306
+ }
307
+ async deleteSession(sessionId) {
308
+ if (hasRawSdkMethod(this.client, "delete")) return this.requestRaw("delete", buildRawSessionRequest(this.sdkFlavor, sessionId));
309
+ return this.callScopedSdkMethod("session", "delete", {
310
+ legacyOptions: { path: { id: sessionId } },
311
+ v2Parameters: { sessionID: sessionId }
312
+ });
313
+ }
314
+ async forkSession(sessionId, messageId) {
315
+ if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", buildRawSessionRequest(this.sdkFlavor, sessionId, "/fork", messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}));
316
+ return this.callScopedSdkMethod("session", "fork", {
317
+ legacyOptions: {
318
+ path: { id: sessionId },
319
+ ...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
320
+ },
321
+ v2Parameters: {
322
+ sessionID: sessionId,
323
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
324
+ }
283
325
  });
284
- return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
285
326
  }
286
327
  async getPath() {
287
328
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
288
- return unwrapSdkData(await this.client.path.get(void 0, SDK_OPTIONS));
329
+ return this.callScopedSdkMethod("path", "get", {});
289
330
  }
290
331
  async listLspStatuses(directory) {
291
332
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
292
333
  url: "/lsp",
293
334
  ...directory ? { query: { directory } } : {}
294
335
  });
295
- return unwrapSdkData(await this.client.lsp.status(directory ? { directory } : void 0, SDK_OPTIONS));
336
+ return this.callScopedSdkMethod("lsp", "status", {
337
+ legacyOptions: directory ? { query: { directory } } : void 0,
338
+ v2Parameters: directory ? { directory } : void 0
339
+ });
296
340
  }
297
341
  async listMcpStatuses(directory) {
298
342
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
299
343
  url: "/mcp",
300
344
  ...directory ? { query: { directory } } : {}
301
345
  });
302
- return unwrapSdkData(await this.client.mcp.status(directory ? { directory } : void 0, SDK_OPTIONS));
346
+ return this.callScopedSdkMethod("mcp", "status", {
347
+ legacyOptions: directory ? { query: { directory } } : void 0,
348
+ v2Parameters: directory ? { directory } : void 0
349
+ });
303
350
  }
304
351
  async getSessionStatuses() {
305
352
  return this.loadSessionStatuses();
306
353
  }
307
354
  async listProjects() {
308
355
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
309
- return unwrapSdkData(await this.client.project.list(void 0, SDK_OPTIONS));
356
+ return this.callScopedSdkMethod("project", "list", {});
310
357
  }
311
358
  async listSessions() {
312
359
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/session" });
313
- return unwrapSdkData(await this.client.session.list(void 0, SDK_OPTIONS));
360
+ return this.callScopedSdkMethod("session", "list", {});
314
361
  }
315
362
  async getCurrentProject() {
316
363
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project/current" });
317
- return unwrapSdkData(await this.client.project.current(void 0, SDK_OPTIONS));
364
+ return this.callScopedSdkMethod("project", "current", {});
318
365
  }
319
366
  async createSessionForDirectory(directory, title) {
320
367
  if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
@@ -322,34 +369,57 @@ var OpenCodeClient = class {
322
369
  query: { directory },
323
370
  ...title ? { body: { title } } : {}
324
371
  });
325
- return unwrapSdkData(await this.client.session.create(title ? {
326
- directory,
327
- title
328
- } : { directory }, SDK_OPTIONS));
372
+ return this.callScopedSdkMethod("session", "create", {
373
+ legacyOptions: {
374
+ query: { directory },
375
+ ...title ? { body: { title } } : {}
376
+ },
377
+ v2Parameters: title ? {
378
+ directory,
379
+ title
380
+ } : { directory }
381
+ });
329
382
  }
330
383
  async renameSession(sessionId, title) {
331
- if (hasRawSdkMethod(this.client, "patch")) return this.requestRaw("patch", {
332
- url: "/session/{sessionID}",
333
- path: { sessionID: sessionId },
334
- body: { title }
384
+ if (hasRawSdkMethod(this.client, "patch")) return this.requestRaw("patch", buildRawSessionRequest(this.sdkFlavor, sessionId, "", { body: { title } }));
385
+ return this.callScopedSdkMethod("session", "update", {
386
+ legacyOptions: {
387
+ path: { id: sessionId },
388
+ body: { title }
389
+ },
390
+ v2Parameters: {
391
+ sessionID: sessionId,
392
+ title
393
+ }
335
394
  });
336
- return unwrapSdkData(await this.client.session.update({
337
- sessionID: sessionId,
338
- title
339
- }, SDK_OPTIONS));
340
395
  }
341
396
  async listAgents() {
342
397
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/agent" });
343
- return unwrapSdkData(await this.client.app.agents(void 0, SDK_OPTIONS));
398
+ return this.callScopedSdkMethod("app", "agents", {});
344
399
  }
345
400
  async listPendingPermissions(directory) {
346
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
401
+ if (this.sdkFlavor === "v2" && hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
347
402
  url: "/permission",
348
403
  ...directory ? { query: { directory } } : {}
349
404
  });
350
- return unwrapSdkData(await this.client.permission.list(directory ? { directory } : void 0, SDK_OPTIONS));
405
+ return this.callScopedSdkMethod("permission", "list", {
406
+ legacyOptions: directory ? { query: { directory } } : void 0,
407
+ v2Parameters: directory ? { directory } : void 0
408
+ });
351
409
  }
352
- async replyToPermission(requestId, reply, message) {
410
+ async replyToPermission(requestId, reply, message, sessionId) {
411
+ if (this.sdkFlavor === "legacy" && sessionId) {
412
+ if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", buildRawLegacyPermissionReplyRequest(sessionId, requestId, reply));
413
+ const legacyClient = this.client;
414
+ if (typeof legacyClient.postSessionIdPermissionsPermissionId === "function") return unwrapSdkData(await legacyClient.postSessionIdPermissionsPermissionId({
415
+ ...SDK_OPTIONS,
416
+ body: { response: reply },
417
+ path: {
418
+ id: sessionId,
419
+ permissionID: requestId
420
+ }
421
+ }));
422
+ }
353
423
  if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
354
424
  url: "/permission/{requestID}/reply",
355
425
  path: { requestID: requestId },
@@ -358,11 +428,20 @@ var OpenCodeClient = class {
358
428
  ...message?.trim() ? { message: message.trim() } : {}
359
429
  }
360
430
  });
361
- return unwrapSdkData(await this.client.permission.reply({
362
- requestID: requestId,
363
- reply,
364
- ...message?.trim() ? { message: message.trim() } : {}
365
- }, SDK_OPTIONS));
431
+ return this.callScopedSdkMethod("permission", "reply", {
432
+ legacyOptions: sessionId ? {
433
+ body: { response: reply },
434
+ path: {
435
+ id: sessionId,
436
+ permissionID: requestId
437
+ }
438
+ } : void 0,
439
+ v2Parameters: {
440
+ requestID: requestId,
441
+ reply,
442
+ ...message?.trim() ? { message: message.trim() } : {}
443
+ }
444
+ });
366
445
  }
367
446
  async listModels() {
368
447
  const now = Date.now();
@@ -398,7 +477,7 @@ var OpenCodeClient = class {
398
477
  }
399
478
  async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
400
479
  const structured = input.structured ?? false;
401
- if (data && !shouldPollPromptMessage(data, structured)) return data;
480
+ if (data && shouldReturnPromptResponseImmediately(data, structured)) return data;
402
481
  const messageId = data ? extractMessageId(data.info) : null;
403
482
  const candidateOptions = {
404
483
  initialMessageId: messageId,
@@ -408,14 +487,18 @@ var OpenCodeClient = class {
408
487
  structured
409
488
  };
410
489
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
411
- const deadlineAt = Date.now() + this.promptRequestTimeouts.totalPollMs;
490
+ let lastProgressAt = Date.now();
491
+ const deadlineAt = startedAt + this.promptTimeoutPolicy.waitTimeoutMs;
412
492
  let idleStatusSeen = false;
413
493
  let attempt = 0;
414
494
  while (true) {
495
+ const remainingWaitMs = deadlineAt - Date.now();
496
+ const remainingInactivityMs = this.promptTimeoutPolicy.recoveryInactivityTimeoutMs - (Date.now() - lastProgressAt);
497
+ if (remainingWaitMs <= 0 || remainingInactivityMs <= 0) break;
415
498
  const delayMs = getPromptMessagePollDelayMs(attempt);
416
499
  attempt += 1;
417
500
  if (delayMs > 0) {
418
- const remainingMs = deadlineAt - Date.now();
501
+ const remainingMs = Math.min(remainingWaitMs, remainingInactivityMs);
419
502
  if (remainingMs <= 0) break;
420
503
  await delay(Math.min(delayMs, remainingMs));
421
504
  }
@@ -423,37 +506,59 @@ var OpenCodeClient = class {
423
506
  const next = await this.fetchPromptMessage(input.sessionId, messageId);
424
507
  if (next) {
425
508
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
426
- if (nextCandidate) bestCandidate = nextCandidate;
427
- if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
509
+ if (nextCandidate) {
510
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
511
+ lastProgressAt = Date.now();
512
+ idleStatusSeen = false;
513
+ }
514
+ bestCandidate = nextCandidate;
515
+ }
516
+ if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
428
517
  }
429
518
  }
430
519
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
431
520
  if (latest) {
432
521
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
433
- if (nextCandidate) bestCandidate = nextCandidate;
434
- if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
522
+ if (nextCandidate) {
523
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
524
+ lastProgressAt = Date.now();
525
+ idleStatusSeen = false;
526
+ }
527
+ bestCandidate = nextCandidate;
528
+ }
529
+ if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
435
530
  }
436
- if ((await this.fetchPromptSessionStatus(input.sessionId))?.type === "idle") {
531
+ const status = await this.fetchPromptSessionStatus(input.sessionId);
532
+ if (status?.type === "busy" || status?.type === "retry") {
533
+ lastProgressAt = Date.now();
534
+ idleStatusSeen = false;
535
+ } else if (status?.type === "idle") {
437
536
  if (idleStatusSeen) break;
438
537
  idleStatusSeen = true;
439
538
  }
539
+ if (bestCandidate && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
440
540
  if (Date.now() >= deadlineAt) break;
441
541
  }
442
542
  const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
443
543
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
444
544
  if (!resolved || shouldPollPromptMessage(resolved, structured)) {
545
+ const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
546
+ const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
445
547
  const error = createOpenCodePromptTimeoutError({
446
548
  sessionId: input.sessionId,
447
549
  stage: "final-scan",
448
- timeoutMs: this.promptRequestTimeouts.totalPollMs,
550
+ timeoutMs,
449
551
  messageId: messageId ?? void 0
450
552
  });
451
- this.logPromptRequestFailure(error, {
553
+ this.logPromptRequest("warn", {
554
+ lastProgressAt,
555
+ messageId: messageId ?? void 0,
556
+ sdkFlavor: this.sdkFlavor,
452
557
  sessionId: input.sessionId,
453
558
  stage: "final-scan",
454
- timeoutMs: this.promptRequestTimeouts.totalPollMs,
455
- messageId
456
- });
559
+ timeoutMs,
560
+ timeoutReason
561
+ }, "OpenCode prompt recovery timed out");
457
562
  throw error;
458
563
  }
459
564
  return resolved;
@@ -463,31 +568,28 @@ var OpenCodeClient = class {
463
568
  return await this.runPromptRequestWithTimeout({
464
569
  sessionId,
465
570
  stage: "poll-message",
466
- timeoutMs: this.promptRequestTimeouts.pollRequestMs,
571
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
467
572
  messageId
468
573
  }, async (signal) => {
469
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", {
470
- url: "/session/{sessionID}/message/{messageID}",
471
- path: {
574
+ if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", buildRawSessionMessageRequest(this.sdkFlavor, sessionId, messageId, { signal })));
575
+ if (typeof this.client.session.message !== "function") return null;
576
+ return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
577
+ legacyOptions: { path: {
578
+ id: sessionId,
579
+ messageID: messageId
580
+ } },
581
+ signal,
582
+ v2Parameters: {
472
583
  sessionID: sessionId,
473
584
  messageID: messageId
474
- },
475
- signal
585
+ }
476
586
  }));
477
- if (typeof this.client.session.message !== "function") return null;
478
- return normalizePromptResponse(unwrapSdkData(await this.client.session.message({
479
- sessionID: sessionId,
480
- messageID: messageId
481
- }, {
482
- ...SDK_OPTIONS,
483
- signal
484
- })));
485
587
  });
486
588
  } catch (error) {
487
589
  this.logPromptRequestFailure(error, {
488
590
  sessionId,
489
591
  stage: "poll-message",
490
- timeoutMs: this.promptRequestTimeouts.pollRequestMs,
592
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
491
593
  messageId
492
594
  });
493
595
  return null;
@@ -503,28 +605,30 @@ var OpenCodeClient = class {
503
605
  return await this.runPromptRequestWithTimeout({
504
606
  sessionId,
505
607
  stage,
506
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
608
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
507
609
  }, async (signal) => {
508
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", {
509
- url: "/session/{sessionID}/message",
510
- path: { sessionID: sessionId },
610
+ if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", buildRawSessionRequest(this.sdkFlavor, sessionId, "/message", {
511
611
  query: { limit: PROMPT_MESSAGE_POLL_LIMIT },
512
612
  signal
513
- }));
514
- if (typeof this.client.session.messages !== "function") return null;
515
- return normalizePromptResponses(unwrapSdkData(await this.client.session.messages({
516
- sessionID: sessionId,
517
- limit: PROMPT_MESSAGE_POLL_LIMIT
518
- }, {
519
- ...SDK_OPTIONS,
520
- signal
521
613
  })));
614
+ if (typeof this.client.session.messages !== "function") return null;
615
+ return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
616
+ legacyOptions: {
617
+ path: { id: sessionId },
618
+ query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
619
+ },
620
+ signal,
621
+ v2Parameters: {
622
+ sessionID: sessionId,
623
+ limit: PROMPT_MESSAGE_POLL_LIMIT
624
+ }
625
+ }));
522
626
  });
523
627
  } catch (error) {
524
628
  this.logPromptRequestFailure(error, {
525
629
  sessionId,
526
630
  stage,
527
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
631
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
528
632
  });
529
633
  return null;
530
634
  }
@@ -534,13 +638,13 @@ var OpenCodeClient = class {
534
638
  return (await this.runPromptRequestWithTimeout({
535
639
  sessionId,
536
640
  stage: "poll-status",
537
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
641
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
538
642
  }, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
539
643
  } catch (error) {
540
644
  this.logPromptRequestFailure(error, {
541
645
  sessionId,
542
646
  stage: "poll-status",
543
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
647
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
544
648
  });
545
649
  return null;
546
650
  }
@@ -563,11 +667,11 @@ var OpenCodeClient = class {
563
667
  }
564
668
  async loadConfig() {
565
669
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config" });
566
- return unwrapSdkData(await this.client.config.get(void 0, SDK_OPTIONS));
670
+ return this.callScopedSdkMethod("config", "get", {});
567
671
  }
568
672
  async loadProviderCatalog() {
569
673
  if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config/providers" });
570
- return unwrapSdkData(await this.client.config.providers(void 0, SDK_OPTIONS));
674
+ return this.callScopedSdkMethod("config", "providers", {});
571
675
  }
572
676
  async sendPromptRequest(input, parts) {
573
677
  const requestBody = {
@@ -582,43 +686,52 @@ var OpenCodeClient = class {
582
686
  ...requestBody
583
687
  };
584
688
  try {
689
+ if (hasRawSdkMethod(this.client, "post")) return await this.runPromptRequestWithTimeout({
690
+ sessionId: input.sessionId,
691
+ stage: "send-prompt",
692
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
693
+ }, async (signal) => normalizePromptResponse(await this.requestRaw("post", buildRawSessionRequest(this.sdkFlavor, input.sessionId, "/message", {
694
+ body: requestBody,
695
+ signal
696
+ }))));
697
+ if (typeof this.client.session?.prompt === "function") return await this.runPromptRequestWithTimeout({
698
+ sessionId: input.sessionId,
699
+ stage: "send-prompt",
700
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
701
+ }, async (signal) => normalizePromptResponse(await this.callScopedSdkMethod("session", "prompt", {
702
+ legacyOptions: {
703
+ body: requestBody,
704
+ path: { id: input.sessionId }
705
+ },
706
+ signal,
707
+ v2Parameters: requestParameters
708
+ })));
585
709
  if (typeof this.client.session?.promptAsync === "function") {
586
710
  await this.runPromptRequestWithTimeout({
587
711
  sessionId: input.sessionId,
588
712
  stage: "send-prompt",
589
- timeoutMs: this.promptRequestTimeouts.sendMs
713
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
590
714
  }, async (signal) => {
591
- await this.client.session.promptAsync(requestParameters, {
592
- ...SDK_OPTIONS,
593
- signal
715
+ await this.callScopedSdkMethod("session", "promptAsync", {
716
+ legacyOptions: {
717
+ body: requestBody,
718
+ path: { id: input.sessionId }
719
+ },
720
+ signal,
721
+ v2Parameters: requestParameters
594
722
  });
595
723
  });
596
724
  return null;
597
725
  }
598
- return await this.runPromptRequestWithTimeout({
599
- sessionId: input.sessionId,
600
- stage: "send-prompt",
601
- timeoutMs: this.promptRequestTimeouts.sendMs
602
- }, async (signal) => {
603
- if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
604
- url: "/session/{sessionID}/message",
605
- path: { sessionID: input.sessionId },
606
- body: requestBody,
607
- signal
608
- }));
609
- return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt(requestParameters, {
610
- ...SDK_OPTIONS,
611
- signal
612
- })));
613
- });
614
726
  } catch (error) {
615
727
  this.logPromptRequestFailure(error, {
616
728
  sessionId: input.sessionId,
617
729
  stage: "send-prompt",
618
- timeoutMs: this.promptRequestTimeouts.sendMs
730
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
619
731
  });
620
732
  throw error;
621
733
  }
734
+ throw new Error("OpenCode SDK client does not expose a compatible prompt endpoint.");
622
735
  }
623
736
  async requestRaw(method, options) {
624
737
  const handler = getRawSdkClient(this.client)?.[method];
@@ -633,10 +746,7 @@ var OpenCodeClient = class {
633
746
  url: "/session/status",
634
747
  ...signal ? { signal } : {}
635
748
  });
636
- return unwrapSdkData(await this.client.session.status(void 0, {
637
- ...SDK_OPTIONS,
638
- ...signal ? { signal } : {}
639
- }));
749
+ return this.callScopedSdkMethod("session", "status", { signal });
640
750
  }
641
751
  async runPromptRequestWithTimeout(input, operation) {
642
752
  const startedAt = Date.now();
@@ -663,8 +773,10 @@ var OpenCodeClient = class {
663
773
  logPromptRequestFailure(error, input) {
664
774
  if (error instanceof OpenCodePromptTimeoutError) {
665
775
  this.logPromptRequest("warn", {
776
+ endpointKind: resolvePromptEndpointKind(error.data.stage),
666
777
  elapsedMs: error.data.elapsedMs,
667
778
  messageId: error.data.messageId,
779
+ sdkFlavor: this.sdkFlavor,
668
780
  sessionId: error.data.sessionId,
669
781
  stage: error.data.stage,
670
782
  timeoutMs: error.data.timeoutMs
@@ -672,8 +784,10 @@ var OpenCodeClient = class {
672
784
  return;
673
785
  }
674
786
  this.logPromptRequest("warn", {
787
+ endpointKind: resolvePromptEndpointKind(input.stage),
675
788
  error,
676
789
  messageId: input.messageId ?? void 0,
790
+ sdkFlavor: this.sdkFlavor,
677
791
  sessionId: input.sessionId,
678
792
  stage: input.stage,
679
793
  timeoutMs: input.timeoutMs
@@ -690,8 +804,8 @@ var OpenCodeClient = class {
690
804
  }).catch(() => void 0);
691
805
  }
692
806
  };
693
- function createOpenCodeClientFromSdkClient(client, fetchFn = fetch) {
694
- return new OpenCodeClient(void 0, client, fetchFn);
807
+ function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
808
+ return new OpenCodeClient(void 0, client, fetchFn, promptTimeoutPolicy);
695
809
  }
696
810
  function buildSelectableModels(config, providers, providerAvailability = /* @__PURE__ */ new Map()) {
697
811
  const configuredProviders = config.provider ?? {};
@@ -798,7 +912,14 @@ function shouldPollPromptMessage(data, structured) {
798
912
  const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
799
913
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
800
914
  const hasAssistantError = !!assistantInfo?.error;
801
- return !hasText && !bodyMd && !hasAssistantError;
915
+ const isCompleted = isAssistantMessageCompleted(assistantInfo);
916
+ return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
917
+ }
918
+ function shouldReturnPromptResponseImmediately(data, structured) {
919
+ return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
920
+ }
921
+ function isPromptResponseUsable(data, structured) {
922
+ return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
802
923
  }
803
924
  function normalizePromptResponse(response) {
804
925
  return {
@@ -860,6 +981,9 @@ function extractMessageId(message) {
860
981
  function delay(ms) {
861
982
  return new Promise((resolve) => setTimeout(resolve, ms));
862
983
  }
984
+ function didPromptResponseAdvance(previous, next, structured) {
985
+ return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
986
+ }
863
987
  function createOpenCodePromptTimeoutError(input) {
864
988
  return new OpenCodePromptTimeoutError({
865
989
  ...input,
@@ -867,6 +991,16 @@ function createOpenCodePromptTimeoutError(input) {
867
991
  message: input.message ?? "The OpenCode host did not finish this request in time."
868
992
  });
869
993
  }
994
+ function resolvePromptEndpointKind(stage) {
995
+ switch (stage) {
996
+ case "capture-known-messages":
997
+ case "poll-messages":
998
+ case "final-scan": return "messages";
999
+ case "poll-message": return "message";
1000
+ case "poll-status": return "status";
1001
+ default: return "prompt";
1002
+ }
1003
+ }
870
1004
  function getPromptMessagePollDelayMs(attempt) {
871
1005
  return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
872
1006
  }
@@ -922,6 +1056,47 @@ function unwrapSdkData(response) {
922
1056
  if (response && typeof response === "object" && "data" in response) return response.data;
923
1057
  return response;
924
1058
  }
1059
+ function resolvePromptTimeoutPolicy(input) {
1060
+ return {
1061
+ pollRequestTimeoutMs: input.pollRequestTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.pollRequestTimeoutMs,
1062
+ recoveryInactivityTimeoutMs: input.recoveryInactivityTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.recoveryInactivityTimeoutMs,
1063
+ waitTimeoutMs: input.waitTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.waitTimeoutMs
1064
+ };
1065
+ }
1066
+ function detectSdkFlavor(client) {
1067
+ if (typeof client.global?.health === "function" || "permission" in client) return "v2";
1068
+ return clientExposesTwoArgumentSdkMethod(client) ? "v2" : "legacy";
1069
+ }
1070
+ function buildRawSessionRequest(sdkFlavor, sessionId, suffix = "", extra = {}) {
1071
+ return {
1072
+ url: sdkFlavor === "legacy" ? `/session/{id}${suffix}` : `/session/{sessionID}${suffix}`,
1073
+ path: sdkFlavor === "legacy" ? { id: sessionId } : { sessionID: sessionId },
1074
+ ...extra
1075
+ };
1076
+ }
1077
+ function buildRawSessionMessageRequest(sdkFlavor, sessionId, messageId, extra = {}) {
1078
+ return {
1079
+ url: sdkFlavor === "legacy" ? "/session/{id}/message/{messageID}" : "/session/{sessionID}/message/{messageID}",
1080
+ path: sdkFlavor === "legacy" ? {
1081
+ id: sessionId,
1082
+ messageID: messageId
1083
+ } : {
1084
+ sessionID: sessionId,
1085
+ messageID: messageId
1086
+ },
1087
+ ...extra
1088
+ };
1089
+ }
1090
+ function buildRawLegacyPermissionReplyRequest(sessionId, requestId, reply) {
1091
+ return {
1092
+ body: { response: reply },
1093
+ path: {
1094
+ id: sessionId,
1095
+ permissionID: requestId
1096
+ },
1097
+ url: "/session/{id}/permissions/{permissionID}"
1098
+ };
1099
+ }
925
1100
  function getRawSdkClient(client) {
926
1101
  return client.client ?? client._client ?? null;
927
1102
  }
@@ -936,6 +1111,32 @@ function normalizeAssistantError(value) {
936
1111
  ...isPlainRecord(value.data) ? { data: value.data } : {}
937
1112
  };
938
1113
  }
1114
+ function isAssistantMessageCompleted(message) {
1115
+ return !!message?.error || typeof message?.time?.completed === "number" || typeof message?.finish === "string" && message.finish.trim().length > 0;
1116
+ }
1117
+ function isCompletedEmptyPromptResponse(data, structured) {
1118
+ const assistantInfo = toAssistantMessage(data.info);
1119
+ const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
1120
+ const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1121
+ return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1122
+ }
1123
+ function clientExposesTwoArgumentSdkMethod(client) {
1124
+ return [
1125
+ "app",
1126
+ "config",
1127
+ "global",
1128
+ "lsp",
1129
+ "mcp",
1130
+ "path",
1131
+ "permission",
1132
+ "project",
1133
+ "session"
1134
+ ].some((scope) => {
1135
+ const target = client[scope];
1136
+ if (!target || typeof target !== "object") return false;
1137
+ return Object.values(target).some((value) => typeof value === "function" && value.length >= 2);
1138
+ });
1139
+ }
939
1140
  function extractStructuredPayload(message) {
940
1141
  if (!isPlainRecord(message)) return null;
941
1142
  if ("structured" in message && message.structured !== void 0) return message.structured;
@@ -960,7 +1161,7 @@ function getPromptResponseCandidateRank(message, options) {
960
1161
  createdAt,
961
1162
  isInitial: !!id && id === options.initialMessageId,
962
1163
  isNewSinceRequestStart: isPromptResponseNewSinceRequestStart(id, createdAt, options.knownMessageIds, options.requestStartedAt),
963
- isUsable: !shouldPollPromptMessage(message, options.structured),
1164
+ isUsable: isPromptResponseUsable(message, options.structured),
964
1165
  sharesParent: !!assistant?.parentID && assistant.parentID === options.initialParentId
965
1166
  };
966
1167
  }
@@ -970,6 +1171,20 @@ function resolvePromptCandidateStartTime(startedAt, initialMessage) {
970
1171
  if (initialCreatedAt === null) return startedAt;
971
1172
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
972
1173
  }
1174
+ function getPromptResponseProgressSignature(response, structured) {
1175
+ if (!response) return "null";
1176
+ const assistant = toAssistantMessage(response.info);
1177
+ const responseParts = Array.isArray(response.parts) ? response.parts : [];
1178
+ return JSON.stringify({
1179
+ assistantError: assistant?.error?.name ?? null,
1180
+ bodyMd: structured ? extractStructuredMarkdown(extractStructuredPayload(assistant)) : null,
1181
+ completedAt: assistant?.time?.completed ?? null,
1182
+ finish: assistant?.finish ?? null,
1183
+ id: assistant?.id ?? null,
1184
+ partCount: responseParts.length,
1185
+ text: extractTextFromParts(responseParts)
1186
+ });
1187
+ }
973
1188
  function isPromptResponseNewSinceRequestStart(messageId, createdAt, knownMessageIds, requestStartedAt) {
974
1189
  if (!messageId || knownMessageIds.has(messageId)) return false;
975
1190
  if (requestStartedAt === null) return true;
@@ -1146,29 +1361,73 @@ function extractErrorMessage(error) {
1146
1361
  //#endregion
1147
1362
  //#region src/services/session-activity/foreground-session-tracker.ts
1148
1363
  var ForegroundSessionTracker = class {
1364
+ chatStacks = /* @__PURE__ */ new Map();
1149
1365
  counts = /* @__PURE__ */ new Map();
1150
- begin(sessionId) {
1366
+ sessionChats = /* @__PURE__ */ new Map();
1367
+ begin(chatId, sessionId) {
1151
1368
  const currentCount = this.counts.get(sessionId) ?? 0;
1152
1369
  this.counts.set(sessionId, currentCount + 1);
1370
+ this.incrementChat(chatId, sessionId);
1153
1371
  return () => {
1154
- this.decrement(sessionId);
1372
+ this.decrement(chatId, sessionId);
1155
1373
  };
1156
1374
  }
1157
1375
  clear(sessionId) {
1158
1376
  const wasForeground = this.counts.has(sessionId);
1377
+ const chatCounts = this.sessionChats.get(sessionId);
1159
1378
  this.counts.delete(sessionId);
1379
+ this.sessionChats.delete(sessionId);
1380
+ for (const chatId of chatCounts?.keys() ?? []) {
1381
+ const stack = this.chatStacks.get(chatId);
1382
+ if (!stack) continue;
1383
+ const nextStack = stack.filter((trackedSessionId) => trackedSessionId !== sessionId);
1384
+ if (nextStack.length === 0) {
1385
+ this.chatStacks.delete(chatId);
1386
+ continue;
1387
+ }
1388
+ this.chatStacks.set(chatId, nextStack);
1389
+ }
1160
1390
  return wasForeground;
1161
1391
  }
1392
+ getActiveSessionId(chatId) {
1393
+ return this.chatStacks.get(chatId)?.at(-1) ?? null;
1394
+ }
1162
1395
  isForeground(sessionId) {
1163
1396
  return this.counts.has(sessionId);
1164
1397
  }
1165
- decrement(sessionId) {
1398
+ listChatIds(sessionId) {
1399
+ return [...this.sessionChats.get(sessionId)?.keys() ?? []];
1400
+ }
1401
+ decrement(chatId, sessionId) {
1166
1402
  const currentCount = this.counts.get(sessionId);
1167
- if (!currentCount || currentCount <= 1) {
1168
- this.counts.delete(sessionId);
1169
- return;
1403
+ if (!currentCount || currentCount <= 1) this.counts.delete(sessionId);
1404
+ else this.counts.set(sessionId, currentCount - 1);
1405
+ this.decrementChat(chatId, sessionId);
1406
+ }
1407
+ incrementChat(chatId, sessionId) {
1408
+ const stack = this.chatStacks.get(chatId) ?? [];
1409
+ stack.push(sessionId);
1410
+ this.chatStacks.set(chatId, stack);
1411
+ const chatCounts = this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Map();
1412
+ const currentCount = chatCounts.get(chatId) ?? 0;
1413
+ chatCounts.set(chatId, currentCount + 1);
1414
+ this.sessionChats.set(sessionId, chatCounts);
1415
+ }
1416
+ decrementChat(chatId, sessionId) {
1417
+ const stack = this.chatStacks.get(chatId);
1418
+ if (stack) {
1419
+ const index = stack.lastIndexOf(sessionId);
1420
+ if (index >= 0) stack.splice(index, 1);
1421
+ if (stack.length === 0) this.chatStacks.delete(chatId);
1422
+ else this.chatStacks.set(chatId, stack);
1170
1423
  }
1171
- this.counts.set(sessionId, currentCount - 1);
1424
+ const chatCounts = this.sessionChats.get(sessionId);
1425
+ if (!chatCounts) return;
1426
+ const currentCount = chatCounts.get(chatId) ?? 0;
1427
+ if (currentCount <= 1) chatCounts.delete(chatId);
1428
+ else chatCounts.set(chatId, currentCount - 1);
1429
+ if (chatCounts.size === 0) this.sessionChats.delete(sessionId);
1430
+ else this.sessionChats.set(sessionId, chatCounts);
1172
1431
  }
1173
1432
  };
1174
1433
  var NOOP_FOREGROUND_SESSION_TRACKER = {
@@ -1178,25 +1437,33 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
1178
1437
  clear() {
1179
1438
  return false;
1180
1439
  },
1440
+ getActiveSessionId() {
1441
+ return null;
1442
+ },
1181
1443
  isForeground() {
1182
1444
  return false;
1445
+ },
1446
+ listChatIds() {
1447
+ return [];
1183
1448
  }
1184
1449
  };
1185
1450
  //#endregion
1186
1451
  //#region src/use-cases/abort-prompt.usecase.ts
1187
1452
  var AbortPromptUseCase = class {
1188
- constructor(sessionRepo, opencodeClient) {
1453
+ constructor(sessionRepo, opencodeClient, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1189
1454
  this.sessionRepo = sessionRepo;
1190
1455
  this.opencodeClient = opencodeClient;
1456
+ this.foregroundSessionTracker = foregroundSessionTracker;
1191
1457
  }
1192
1458
  async execute(input) {
1193
- const binding = await this.sessionRepo.getByChatId(input.chatId);
1194
- if (!binding?.sessionId) return {
1459
+ const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
1460
+ const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
1461
+ const sessionId = activeSessionId ?? binding?.sessionId ?? null;
1462
+ if (!sessionId) return {
1195
1463
  sessionId: null,
1196
1464
  status: "no_session",
1197
1465
  sessionStatus: null
1198
1466
  };
1199
- const sessionId = binding.sessionId;
1200
1467
  const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
1201
1468
  if (!sessionStatus || sessionStatus.type === "idle") return {
1202
1469
  sessionId,
@@ -1693,6 +1960,7 @@ var SendPromptUseCase = class {
1693
1960
  }
1694
1961
  if (!binding || !binding.sessionId || !binding.projectId) throw new Error("Failed to initialize chat session.");
1695
1962
  let activeBinding = binding;
1963
+ const shouldIsolateImageTurn = hasImageFiles(files);
1696
1964
  const model = activeBinding.modelProviderId && activeBinding.modelId ? {
1697
1965
  providerID: activeBinding.modelProviderId,
1698
1966
  modelID: activeBinding.modelId
@@ -1702,11 +1970,13 @@ var SendPromptUseCase = class {
1702
1970
  activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
1703
1971
  this.logger.warn?.({ chatId: input.chatId }, "selected agent is no longer available, falling back to OpenCode default");
1704
1972
  }
1705
- const endForegroundSession = this.foregroundSessionTracker.begin(activeBinding.sessionId);
1973
+ const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1974
+ const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1975
+ const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
1706
1976
  let result;
1707
1977
  try {
1708
1978
  result = await this.opencodeClient.promptSession({
1709
- sessionId: activeBinding.sessionId,
1979
+ sessionId: executionSessionId,
1710
1980
  prompt: promptText,
1711
1981
  ...files.length > 0 ? { files } : {},
1712
1982
  ...selectedAgent ? { agent: selectedAgent.name } : {},
@@ -1716,6 +1986,7 @@ var SendPromptUseCase = class {
1716
1986
  });
1717
1987
  } finally {
1718
1988
  endForegroundSession();
1989
+ if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1719
1990
  }
1720
1991
  await this.sessionRepo.touch(input.chatId);
1721
1992
  return {
@@ -1729,6 +2000,32 @@ var SendPromptUseCase = class {
1729
2000
  this.logger.warn?.({ chatId }, `${reason}, falling back to the current OpenCode project`);
1730
2001
  return nextBinding;
1731
2002
  }
2003
+ async createTemporaryImageSession(chatId, sessionId) {
2004
+ const temporarySession = await this.opencodeClient.forkSession(sessionId);
2005
+ if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
2006
+ this.logger.info?.({
2007
+ chatId,
2008
+ parentSessionId: sessionId,
2009
+ sessionId: temporarySession.id
2010
+ }, "created temporary image session");
2011
+ return temporarySession.id;
2012
+ }
2013
+ async cleanupTemporaryImageSession(chatId, parentSessionId, sessionId) {
2014
+ try {
2015
+ if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
2016
+ chatId,
2017
+ parentSessionId,
2018
+ sessionId
2019
+ }, "failed to delete temporary image session");
2020
+ } catch (error) {
2021
+ this.logger.warn?.({
2022
+ error,
2023
+ chatId,
2024
+ parentSessionId,
2025
+ sessionId
2026
+ }, "failed to delete temporary image session");
2027
+ }
2028
+ }
1732
2029
  };
1733
2030
  function buildPromptText(text, files) {
1734
2031
  const trimmedText = text?.trim() ?? "";
@@ -1745,6 +2042,9 @@ function buildPromptText(text, files) {
1745
2042
  function isImageFile(file) {
1746
2043
  return file.mime.trim().toLowerCase().startsWith("image/");
1747
2044
  }
2045
+ function hasImageFiles(files) {
2046
+ return files.some(isImageFile);
2047
+ }
1748
2048
  //#endregion
1749
2049
  //#region src/use-cases/switch-agent.usecase.ts
1750
2050
  var SwitchAgentUseCase = class {
@@ -1922,7 +2222,11 @@ function resolveExtension(mimeType) {
1922
2222
  //#region src/app/container.ts
1923
2223
  function createAppContainer(config, client) {
1924
2224
  const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
1925
- return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
2225
+ return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
2226
+ waitTimeoutMs: config.promptWaitTimeoutMs,
2227
+ pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
2228
+ recoveryInactivityTimeoutMs: config.promptRecoveryInactivityTimeoutMs
2229
+ }), logger);
1926
2230
  }
1927
2231
  function createContainer(config, opencodeClient, logger) {
1928
2232
  const stateStore = new JsonStateStore({
@@ -1938,7 +2242,7 @@ function createContainer(config, opencodeClient, logger) {
1938
2242
  apiRoot: config.telegramApiRoot
1939
2243
  });
1940
2244
  const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
1941
- const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
2245
+ const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
1942
2246
  const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
1943
2247
  const getHealthUseCase = new GetHealthUseCase(opencodeClient);
1944
2248
  const getPathUseCase = new GetPathUseCase(opencodeClient);
@@ -2072,19 +2376,20 @@ async function handleTelegramBotPluginEvent(runtime, event) {
2072
2376
  }
2073
2377
  async function handlePermissionAsked(runtime, request) {
2074
2378
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2379
+ const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2075
2380
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
2076
2381
  const approvedChatIds = new Set(approvals.map((approval) => approval.chatId));
2077
- for (const binding of bindings) {
2078
- if (approvedChatIds.has(binding.chatId)) continue;
2382
+ for (const chatId of chatIds) {
2383
+ if (approvedChatIds.has(chatId)) continue;
2079
2384
  try {
2080
- const message = await runtime.bot.api.sendMessage(binding.chatId, buildPermissionApprovalMessage(request), {
2385
+ const message = await runtime.bot.api.sendMessage(chatId, buildPermissionApprovalMessage(request), {
2081
2386
  parse_mode: "MarkdownV2",
2082
2387
  reply_markup: buildPermissionApprovalKeyboard(request.id)
2083
2388
  });
2084
2389
  await runtime.container.permissionApprovalRepo.set({
2085
2390
  requestId: request.id,
2086
2391
  sessionId: request.sessionID,
2087
- chatId: binding.chatId,
2392
+ chatId,
2088
2393
  messageId: message.message_id,
2089
2394
  status: "pending",
2090
2395
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2092,7 +2397,7 @@ async function handlePermissionAsked(runtime, request) {
2092
2397
  } catch (error) {
2093
2398
  runtime.container.logger.error({
2094
2399
  error,
2095
- chatId: binding.chatId,
2400
+ chatId,
2096
2401
  requestId: request.id
2097
2402
  }, "failed to deliver permission request to Telegram");
2098
2403
  }
@@ -2118,7 +2423,7 @@ async function handleSessionError(runtime, sessionId, error) {
2118
2423
  runtime.container.logger.error({ error }, "session error received without a session id");
2119
2424
  return;
2120
2425
  }
2121
- if (runtime.container.foregroundSessionTracker.isForeground(sessionId)) {
2426
+ if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2122
2427
  runtime.container.logger.warn({
2123
2428
  error,
2124
2429
  sessionId
@@ -4374,8 +4679,8 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
4374
4679
  const parsed = parsePermissionApprovalCallbackData(data);
4375
4680
  if (!parsed) return;
4376
4681
  try {
4377
- await dependencies.opencodeClient.replyToPermission(parsed.requestId, parsed.reply);
4378
4682
  const approval = (await dependencies.permissionApprovalRepo.listByRequestId(parsed.requestId)).find((item) => item.chatId === ctx.chat?.id);
4683
+ await dependencies.opencodeClient.replyToPermission(parsed.requestId, parsed.reply, void 0, approval?.sessionId);
4379
4684
  if (approval) await dependencies.permissionApprovalRepo.set({
4380
4685
  ...approval,
4381
4686
  status: parsed.reply,
@@ -4727,7 +5032,9 @@ function createHooks(runtime) {
4727
5032
  await handleTelegramBotPluginEvent(runtime, event);
4728
5033
  },
4729
5034
  async "permission.ask"(input, output) {
4730
- if ((await runtime.container.sessionRepo.listBySessionId(input.sessionID)).length > 0) output.status = "ask";
5035
+ const bindings = await runtime.container.sessionRepo.listBySessionId(input.sessionID);
5036
+ const foregroundChatIds = runtime.container.foregroundSessionTracker.listChatIds(input.sessionID);
5037
+ if (bindings.length > 0 || foregroundChatIds.length > 0) output.status = "ask";
4731
5038
  }
4732
5039
  };
4733
5040
  }