opencode-tbot 0.1.26 → 0.1.28

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
@@ -218,6 +218,17 @@ var OpenCodePromptTimeoutError = class extends Error {
218
218
  this.data = data;
219
219
  }
220
220
  };
221
+ var OpenCodeMessageAbortedError = class extends Error {
222
+ data;
223
+ constructor(message = "Request was aborted.") {
224
+ super(message);
225
+ this.name = "MessageAbortedError";
226
+ this.data = { message };
227
+ }
228
+ };
229
+ function createMessageAbortedError(message = "Request was aborted.") {
230
+ return new OpenCodeMessageAbortedError(message);
231
+ }
221
232
  var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
222
233
  var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
223
234
  0,
@@ -255,7 +266,6 @@ var StructuredReplySchema = z.object({ body_md: z.string() });
255
266
  var OpenCodeClient = class {
256
267
  client;
257
268
  fetchFn;
258
- sdkFlavor;
259
269
  promptTimeoutPolicy;
260
270
  modelCache = {
261
271
  expiresAt: 0,
@@ -266,7 +276,6 @@ var OpenCodeClient = class {
266
276
  if (!options && !client) throw new Error("OpenCodeClient requires either base URL options or an injected SDK client.");
267
277
  this.client = client ?? createOpencodeClient(buildOpenCodeSdkConfig(options));
268
278
  this.fetchFn = fetchFn;
269
- this.sdkFlavor = detectSdkFlavor(this.client);
270
279
  this.promptTimeoutPolicy = resolvePromptTimeoutPolicy(promptTimeoutPolicy);
271
280
  }
272
281
  configurePromptTimeoutPolicy(promptTimeoutPolicy) {
@@ -279,169 +288,77 @@ var OpenCodeClient = class {
279
288
  const target = this.client[scope];
280
289
  const handler = target?.[method];
281
290
  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, {
291
+ const options = {
288
292
  ...SDK_OPTIONS,
289
293
  ...input.signal ? { signal: input.signal } : {}
290
- }));
294
+ };
295
+ return unwrapSdkData(input.parameters === void 0 ? await handler.call(target, options) : await handler.call(target, input.parameters, options));
291
296
  }
292
297
  async getHealth() {
293
- const rawClient = getRawSdkClient(this.client);
294
- if (rawClient?.get) return await this.requestRaw("get", { url: "/global/health" });
295
- const healthEndpoint = this.client.global?.health;
296
- if (typeof healthEndpoint === "function") return unwrapSdkData(await healthEndpoint.call(this.client.global, SDK_OPTIONS));
297
- if (!rawClient?.get) throw new Error("OpenCode SDK client does not expose a compatible health endpoint.");
298
- return this.requestRaw("get", { url: "/global/health" });
298
+ try {
299
+ return await this.callScopedSdkMethod("global", "health", {});
300
+ } catch (error) {
301
+ if (!isMissingScopedSdkMethodError(error, "global", "health")) throw error;
302
+ return this.callRawSdkGet("/global/health");
303
+ }
299
304
  }
300
305
  async abortSession(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
+ return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
306
307
  }
307
308
  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
- });
309
+ return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
313
310
  }
314
311
  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
- }
325
- });
312
+ return this.callScopedSdkMethod("session", "fork", { parameters: {
313
+ sessionID: sessionId,
314
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
315
+ } });
326
316
  }
327
317
  async getPath() {
328
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
329
318
  return this.callScopedSdkMethod("path", "get", {});
330
319
  }
331
320
  async listLspStatuses(directory) {
332
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
333
- url: "/lsp",
334
- ...directory ? { query: { directory } } : {}
335
- });
336
- return this.callScopedSdkMethod("lsp", "status", {
337
- legacyOptions: directory ? { query: { directory } } : void 0,
338
- v2Parameters: directory ? { directory } : void 0
339
- });
321
+ return this.callScopedSdkMethod("lsp", "status", { parameters: directory ? { directory } : void 0 });
340
322
  }
341
323
  async listMcpStatuses(directory) {
342
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
343
- url: "/mcp",
344
- ...directory ? { query: { directory } } : {}
345
- });
346
- return this.callScopedSdkMethod("mcp", "status", {
347
- legacyOptions: directory ? { query: { directory } } : void 0,
348
- v2Parameters: directory ? { directory } : void 0
349
- });
324
+ return this.callScopedSdkMethod("mcp", "status", { parameters: directory ? { directory } : void 0 });
350
325
  }
351
326
  async getSessionStatuses() {
352
327
  return this.loadSessionStatuses();
353
328
  }
354
329
  async listProjects() {
355
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
356
330
  return this.callScopedSdkMethod("project", "list", {});
357
331
  }
358
332
  async listSessions() {
359
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/session" });
360
333
  return this.callScopedSdkMethod("session", "list", {});
361
334
  }
362
335
  async getCurrentProject() {
363
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project/current" });
364
336
  return this.callScopedSdkMethod("project", "current", {});
365
337
  }
366
338
  async createSessionForDirectory(directory, title) {
367
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
368
- url: "/session",
369
- query: { directory },
370
- ...title ? { body: { title } } : {}
371
- });
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
- });
339
+ return this.callScopedSdkMethod("session", "create", { parameters: title ? {
340
+ directory,
341
+ title
342
+ } : { directory } });
382
343
  }
383
344
  async renameSession(sessionId, 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
- }
394
- });
345
+ return this.callScopedSdkMethod("session", "update", { parameters: {
346
+ sessionID: sessionId,
347
+ title
348
+ } });
395
349
  }
396
350
  async listAgents() {
397
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/agent" });
398
351
  return this.callScopedSdkMethod("app", "agents", {});
399
352
  }
400
353
  async listPendingPermissions(directory) {
401
- if (this.sdkFlavor === "v2" && hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
402
- url: "/permission",
403
- ...directory ? { query: { directory } } : {}
404
- });
405
- return this.callScopedSdkMethod("permission", "list", {
406
- legacyOptions: directory ? { query: { directory } } : void 0,
407
- v2Parameters: directory ? { directory } : void 0
408
- });
354
+ return this.callScopedSdkMethod("permission", "list", { parameters: directory ? { directory } : void 0 });
409
355
  }
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
- }
423
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
424
- url: "/permission/{requestID}/reply",
425
- path: { requestID: requestId },
426
- body: {
427
- reply,
428
- ...message?.trim() ? { message: message.trim() } : {}
429
- }
430
- });
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
- });
356
+ async replyToPermission(requestId, reply, message, _sessionId) {
357
+ return this.callScopedSdkMethod("permission", "reply", { parameters: {
358
+ requestID: requestId,
359
+ reply,
360
+ ...message?.trim() ? { message: message.trim() } : {}
361
+ } });
445
362
  }
446
363
  async listModels() {
447
364
  const now = Date.now();
@@ -466,9 +383,10 @@ var OpenCodeClient = class {
466
383
  url: file.url
467
384
  }))];
468
385
  if (parts.length === 0) throw new Error("Prompt requires text or file attachments.");
469
- const knownMessageIds = await this.captureKnownMessageIds(input.sessionId);
470
- const initialData = await this.sendPromptRequest(input, parts);
471
- return buildPromptSessionResult(await this.resolvePromptResponse(input, initialData, knownMessageIds, startedAt), {
386
+ throwIfAborted(input.signal);
387
+ const knownMessageIds = await this.captureKnownMessageIds(input.sessionId, input.signal);
388
+ await this.sendPromptRequest(input, parts);
389
+ return buildPromptSessionResult(await this.resolvePromptResponse(input, null, knownMessageIds, startedAt), {
472
390
  emptyResponseText: EMPTY_RESPONSE_TEXT,
473
391
  finishedAt: Date.now(),
474
392
  startedAt,
@@ -488,10 +406,12 @@ var OpenCodeClient = class {
488
406
  };
489
407
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
490
408
  let lastProgressAt = Date.now();
409
+ let lastStatus = null;
491
410
  const deadlineAt = startedAt + this.promptTimeoutPolicy.waitTimeoutMs;
492
411
  let idleStatusSeen = false;
493
412
  let attempt = 0;
494
413
  while (true) {
414
+ throwIfAborted(input.signal);
495
415
  const remainingWaitMs = deadlineAt - Date.now();
496
416
  const remainingInactivityMs = this.promptTimeoutPolicy.recoveryInactivityTimeoutMs - (Date.now() - lastProgressAt);
497
417
  if (remainingWaitMs <= 0 || remainingInactivityMs <= 0) break;
@@ -500,10 +420,10 @@ var OpenCodeClient = class {
500
420
  if (delayMs > 0) {
501
421
  const remainingMs = Math.min(remainingWaitMs, remainingInactivityMs);
502
422
  if (remainingMs <= 0) break;
503
- await delay(Math.min(delayMs, remainingMs));
423
+ await delay(Math.min(delayMs, remainingMs), input.signal);
504
424
  }
505
425
  if (messageId) {
506
- const next = await this.fetchPromptMessage(input.sessionId, messageId);
426
+ const next = await this.fetchPromptMessage(input.sessionId, messageId, input.signal);
507
427
  if (next) {
508
428
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
509
429
  if (nextCandidate) {
@@ -513,10 +433,10 @@ var OpenCodeClient = class {
513
433
  }
514
434
  bestCandidate = nextCandidate;
515
435
  }
516
- if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
436
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
517
437
  }
518
438
  }
519
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
439
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
520
440
  if (latest) {
521
441
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
522
442
  if (nextCandidate) {
@@ -526,9 +446,10 @@ var OpenCodeClient = class {
526
446
  }
527
447
  bestCandidate = nextCandidate;
528
448
  }
529
- if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
449
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
530
450
  }
531
- const status = await this.fetchPromptSessionStatus(input.sessionId);
451
+ const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
452
+ lastStatus = status;
532
453
  if (status?.type === "busy" || status?.type === "retry") {
533
454
  lastProgressAt = Date.now();
534
455
  idleStatusSeen = false;
@@ -536,12 +457,14 @@ var OpenCodeClient = class {
536
457
  if (idleStatusSeen) break;
537
458
  idleStatusSeen = true;
538
459
  }
539
- if (bestCandidate && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
460
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
540
461
  if (Date.now() >= deadlineAt) break;
541
462
  }
542
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
463
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
543
464
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
544
- if (!resolved || shouldPollPromptMessage(resolved, structured)) {
465
+ const requestScopedResolved = resolved && isPromptResponseForCurrentRequest(resolved, candidateOptions) ? resolved : null;
466
+ if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured))) throw createMessageAbortedError();
467
+ if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured)) {
545
468
  const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
546
469
  const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
547
470
  const error = createOpenCodePromptTimeoutError({
@@ -553,7 +476,6 @@ var OpenCodeClient = class {
553
476
  this.logPromptRequest("warn", {
554
477
  lastProgressAt,
555
478
  messageId: messageId ?? void 0,
556
- sdkFlavor: this.sdkFlavor,
557
479
  sessionId: input.sessionId,
558
480
  stage: "final-scan",
559
481
  timeoutMs,
@@ -561,31 +483,26 @@ var OpenCodeClient = class {
561
483
  }, "OpenCode prompt recovery timed out");
562
484
  throw error;
563
485
  }
564
- return resolved;
486
+ return requestScopedResolved;
565
487
  }
566
- async fetchPromptMessage(sessionId, messageId) {
488
+ async fetchPromptMessage(sessionId, messageId, signal) {
567
489
  try {
568
490
  return await this.runPromptRequestWithTimeout({
569
491
  sessionId,
570
492
  stage: "poll-message",
571
493
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
572
494
  messageId
573
- }, async (signal) => {
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;
495
+ }, async (requestSignal) => {
576
496
  return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
577
- legacyOptions: { path: {
578
- id: sessionId,
579
- messageID: messageId
580
- } },
581
- signal,
582
- v2Parameters: {
497
+ parameters: {
583
498
  sessionID: sessionId,
584
499
  messageID: messageId
585
- }
500
+ },
501
+ signal: requestSignal
586
502
  }));
587
- });
503
+ }, signal);
588
504
  } catch (error) {
505
+ if (isPromptRequestAbort(error)) throw error;
589
506
  this.logPromptRequestFailure(error, {
590
507
  sessionId,
591
508
  stage: "poll-message",
@@ -595,36 +512,28 @@ var OpenCodeClient = class {
595
512
  return null;
596
513
  }
597
514
  }
598
- async captureKnownMessageIds(sessionId) {
599
- const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages");
515
+ async captureKnownMessageIds(sessionId, signal) {
516
+ const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages", signal);
600
517
  if (!messages) return /* @__PURE__ */ new Set();
601
518
  return new Set(messages.map((message) => extractMessageId(message.info)).filter((id) => typeof id === "string" && id.length > 0));
602
519
  }
603
- async fetchRecentPromptMessages(sessionId, stage) {
520
+ async fetchRecentPromptMessages(sessionId, stage, signal) {
604
521
  try {
605
522
  return await this.runPromptRequestWithTimeout({
606
523
  sessionId,
607
524
  stage,
608
525
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
609
- }, async (signal) => {
610
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", buildRawSessionRequest(this.sdkFlavor, sessionId, "/message", {
611
- query: { limit: PROMPT_MESSAGE_POLL_LIMIT },
612
- signal
613
- })));
614
- if (typeof this.client.session.messages !== "function") return null;
526
+ }, async (requestSignal) => {
615
527
  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: {
528
+ parameters: {
622
529
  sessionID: sessionId,
623
530
  limit: PROMPT_MESSAGE_POLL_LIMIT
624
- }
531
+ },
532
+ signal: requestSignal
625
533
  }));
626
- });
534
+ }, signal);
627
535
  } catch (error) {
536
+ if (isPromptRequestAbort(error)) throw error;
628
537
  this.logPromptRequestFailure(error, {
629
538
  sessionId,
630
539
  stage,
@@ -633,14 +542,15 @@ var OpenCodeClient = class {
633
542
  return null;
634
543
  }
635
544
  }
636
- async fetchPromptSessionStatus(sessionId) {
545
+ async fetchPromptSessionStatus(sessionId, signal) {
637
546
  try {
638
547
  return (await this.runPromptRequestWithTimeout({
639
548
  sessionId,
640
549
  stage: "poll-status",
641
550
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
642
- }, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
551
+ }, async (requestSignal) => this.loadSessionStatuses(requestSignal), signal))[sessionId] ?? null;
643
552
  } catch (error) {
553
+ if (isPromptRequestAbort(error)) throw error;
644
554
  this.logPromptRequestFailure(error, {
645
555
  sessionId,
646
556
  stage: "poll-status",
@@ -649,8 +559,8 @@ var OpenCodeClient = class {
649
559
  return null;
650
560
  }
651
561
  }
652
- async findLatestPromptResponse(sessionId, options, stage) {
653
- const messages = await this.fetchRecentPromptMessages(sessionId, stage);
562
+ async findLatestPromptResponse(sessionId, options, stage, signal) {
563
+ const messages = await this.fetchRecentPromptMessages(sessionId, stage, signal);
654
564
  if (!messages || messages.length === 0) return null;
655
565
  return selectPromptResponseCandidate(messages, options);
656
566
  }
@@ -666,11 +576,9 @@ var OpenCodeClient = class {
666
576
  return models;
667
577
  }
668
578
  async loadConfig() {
669
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config" });
670
579
  return this.callScopedSdkMethod("config", "get", {});
671
580
  }
672
581
  async loadProviderCatalog() {
673
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config/providers" });
674
582
  return this.callScopedSdkMethod("config", "providers", {});
675
583
  }
676
584
  async sendPromptRequest(input, parts) {
@@ -686,26 +594,6 @@ var OpenCodeClient = class {
686
594
  ...requestBody
687
595
  };
688
596
  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
- })));
709
597
  if (typeof this.client.session?.promptAsync === "function") {
710
598
  await this.runPromptRequestWithTimeout({
711
599
  sessionId: input.sessionId,
@@ -713,15 +601,11 @@ var OpenCodeClient = class {
713
601
  timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
714
602
  }, async (signal) => {
715
603
  await this.callScopedSdkMethod("session", "promptAsync", {
716
- legacyOptions: {
717
- body: requestBody,
718
- path: { id: input.sessionId }
719
- },
720
- signal,
721
- v2Parameters: requestParameters
604
+ parameters: requestParameters,
605
+ signal
722
606
  });
723
- });
724
- return null;
607
+ }, input.signal);
608
+ return;
725
609
  }
726
610
  } catch (error) {
727
611
  this.logPromptRequestFailure(error, {
@@ -731,27 +615,42 @@ var OpenCodeClient = class {
731
615
  });
732
616
  throw error;
733
617
  }
734
- throw new Error("OpenCode SDK client does not expose a compatible prompt endpoint.");
735
- }
736
- async requestRaw(method, options) {
737
- const handler = getRawSdkClient(this.client)?.[method];
738
- if (typeof handler !== "function") throw new Error(`OpenCode SDK client does not expose a compatible raw ${method.toUpperCase()} method.`);
739
- return unwrapSdkData(await handler({
740
- ...SDK_OPTIONS,
741
- ...options
742
- }));
618
+ throw new Error("OpenCode SDK client does not expose session.promptAsync().");
743
619
  }
744
620
  async loadSessionStatuses(signal) {
745
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
746
- url: "/session/status",
747
- ...signal ? { signal } : {}
621
+ return this.callScopedSdkMethod("session", "status", {
622
+ signal,
623
+ parameters: void 0
748
624
  });
749
- return this.callScopedSdkMethod("session", "status", { signal });
750
625
  }
751
- async runPromptRequestWithTimeout(input, operation) {
626
+ async callRawSdkGet(url, signal) {
627
+ const rawClient = getRawSdkRequestClient(this.client);
628
+ if (typeof rawClient?.get !== "function") throw new Error(`OpenCode SDK client does not expose a compatible raw GET endpoint for ${url}.`);
629
+ return unwrapSdkData(await rawClient.get({
630
+ ...SDK_OPTIONS,
631
+ ...signal ? { signal } : {},
632
+ url
633
+ }));
634
+ }
635
+ async runPromptRequestWithTimeout(input, operation, signal) {
752
636
  const startedAt = Date.now();
753
637
  const controller = new AbortController();
638
+ const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
754
639
  let timeoutHandle = null;
640
+ let removeAbortListener = () => void 0;
641
+ const abortPromise = signal ? new Promise((_, reject) => {
642
+ const onAbort = () => {
643
+ reject(normalizeAbortReason(signal.reason));
644
+ };
645
+ if (signal.aborted) {
646
+ onAbort();
647
+ return;
648
+ }
649
+ signal.addEventListener("abort", onAbort, { once: true });
650
+ removeAbortListener = () => {
651
+ signal.removeEventListener("abort", onAbort);
652
+ };
653
+ }) : null;
755
654
  const timeoutPromise = new Promise((_, reject) => {
756
655
  timeoutHandle = setTimeout(() => {
757
656
  reject(createOpenCodePromptTimeoutError({
@@ -765,9 +664,14 @@ var OpenCodeClient = class {
765
664
  }, input.timeoutMs);
766
665
  });
767
666
  try {
768
- return await Promise.race([operation(controller.signal), timeoutPromise]);
667
+ return await Promise.race([
668
+ operation(requestSignal),
669
+ timeoutPromise,
670
+ ...abortPromise ? [abortPromise] : []
671
+ ]);
769
672
  } finally {
770
673
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
674
+ removeAbortListener();
771
675
  }
772
676
  }
773
677
  logPromptRequestFailure(error, input) {
@@ -776,7 +680,6 @@ var OpenCodeClient = class {
776
680
  endpointKind: resolvePromptEndpointKind(error.data.stage),
777
681
  elapsedMs: error.data.elapsedMs,
778
682
  messageId: error.data.messageId,
779
- sdkFlavor: this.sdkFlavor,
780
683
  sessionId: error.data.sessionId,
781
684
  stage: error.data.stage,
782
685
  timeoutMs: error.data.timeoutMs
@@ -787,7 +690,6 @@ var OpenCodeClient = class {
787
690
  endpointKind: resolvePromptEndpointKind(input.stage),
788
691
  error,
789
692
  messageId: input.messageId ?? void 0,
790
- sdkFlavor: this.sdkFlavor,
791
693
  sessionId: input.sessionId,
792
694
  stage: input.stage,
793
695
  timeoutMs: input.timeoutMs
@@ -978,8 +880,23 @@ function extractMessageId(message) {
978
880
  if (!isPlainRecord(message)) return null;
979
881
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
980
882
  }
981
- function delay(ms) {
982
- return new Promise((resolve) => setTimeout(resolve, ms));
883
+ function delay(ms, signal) {
884
+ return new Promise((resolve, reject) => {
885
+ const handle = setTimeout(() => {
886
+ signal?.removeEventListener("abort", onAbort);
887
+ resolve();
888
+ }, ms);
889
+ const onAbort = () => {
890
+ clearTimeout(handle);
891
+ signal?.removeEventListener("abort", onAbort);
892
+ reject(normalizeAbortReason(signal?.reason));
893
+ };
894
+ if (signal?.aborted) {
895
+ onAbort();
896
+ return;
897
+ }
898
+ signal?.addEventListener("abort", onAbort, { once: true });
899
+ });
983
900
  }
984
901
  function didPromptResponseAdvance(previous, next, structured) {
985
902
  return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
@@ -1056,6 +973,13 @@ function unwrapSdkData(response) {
1056
973
  if (response && typeof response === "object" && "data" in response) return response.data;
1057
974
  return response;
1058
975
  }
976
+ function getRawSdkRequestClient(client) {
977
+ const compatibleClient = client;
978
+ return compatibleClient.client ?? compatibleClient._client ?? null;
979
+ }
980
+ function isMissingScopedSdkMethodError(error, scope, method) {
981
+ return error instanceof Error && error.message === `OpenCode SDK client does not expose a compatible ${scope}.${method} method.`;
982
+ }
1059
983
  function resolvePromptTimeoutPolicy(input) {
1060
984
  return {
1061
985
  pollRequestTimeoutMs: input.pollRequestTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.pollRequestTimeoutMs,
@@ -1063,46 +987,6 @@ function resolvePromptTimeoutPolicy(input) {
1063
987
  waitTimeoutMs: input.waitTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.waitTimeoutMs
1064
988
  };
1065
989
  }
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
- }
1100
- function getRawSdkClient(client) {
1101
- return client.client ?? client._client ?? null;
1102
- }
1103
- function hasRawSdkMethod(client, method) {
1104
- return typeof getRawSdkClient(client)?.[method] === "function";
1105
- }
1106
990
  function normalizeAssistantError(value) {
1107
991
  if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
1108
992
  return {
@@ -1120,23 +1004,6 @@ function isCompletedEmptyPromptResponse(data, structured) {
1120
1004
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1121
1005
  return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1122
1006
  }
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
- }
1140
1007
  function extractStructuredPayload(message) {
1141
1008
  if (!isPlainRecord(message)) return null;
1142
1009
  if ("structured" in message && message.structured !== void 0) return message.structured;
@@ -1151,7 +1018,7 @@ function selectPromptResponseCandidate(candidates, options) {
1151
1018
  function comparePromptResponseCandidates(left, right, options) {
1152
1019
  const leftRank = getPromptResponseCandidateRank(left, options);
1153
1020
  const rightRank = getPromptResponseCandidateRank(right, options);
1154
- return Number(rightRank.isUsable) - Number(leftRank.isUsable) || Number(rightRank.isInitial) - Number(leftRank.isInitial) || Number(rightRank.sharesParent) - Number(leftRank.sharesParent) || Number(rightRank.isNewSinceRequestStart) - Number(leftRank.isNewSinceRequestStart) || rightRank.createdAt - leftRank.createdAt;
1021
+ return Number(rightRank.isInitial) - Number(leftRank.isInitial) || Number(rightRank.sharesParent) - Number(leftRank.sharesParent) || Number(rightRank.isNewSinceRequestStart) - Number(leftRank.isNewSinceRequestStart) || Number(rightRank.isUsable) - Number(leftRank.isUsable) || rightRank.createdAt - leftRank.createdAt;
1155
1022
  }
1156
1023
  function getPromptResponseCandidateRank(message, options) {
1157
1024
  const assistant = toAssistantMessage(message.info);
@@ -1166,7 +1033,7 @@ function getPromptResponseCandidateRank(message, options) {
1166
1033
  };
1167
1034
  }
1168
1035
  function resolvePromptCandidateStartTime(startedAt, initialMessage) {
1169
- if (!initialMessage) return null;
1036
+ if (!initialMessage) return startedAt;
1170
1037
  const initialCreatedAt = coerceFiniteNumber(toAssistantMessage(initialMessage.info)?.time?.created);
1171
1038
  if (initialCreatedAt === null) return startedAt;
1172
1039
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
@@ -1190,10 +1057,28 @@ function isPromptResponseNewSinceRequestStart(messageId, createdAt, knownMessage
1190
1057
  if (requestStartedAt === null) return true;
1191
1058
  return createdAt >= requestStartedAt;
1192
1059
  }
1060
+ function isPromptResponseForCurrentRequest(response, options) {
1061
+ const rank = getPromptResponseCandidateRank(response, options);
1062
+ return rank.isInitial || rank.sharesParent || rank.isNewSinceRequestStart;
1063
+ }
1193
1064
  function areComparablePromptTimestamps(left, right) {
1194
1065
  const epochThresholdMs = 0xe8d4a51000;
1195
1066
  return left >= epochThresholdMs && right >= epochThresholdMs;
1196
1067
  }
1068
+ function isPromptRequestAbort(error) {
1069
+ return error instanceof OpenCodeMessageAbortedError || error instanceof Error && error.name === "AbortError" || isNamedAbortError(error);
1070
+ }
1071
+ function isNamedAbortError(error) {
1072
+ return !!error && typeof error === "object" && "name" in error && error.name === "MessageAbortedError";
1073
+ }
1074
+ function normalizeAbortReason(reason) {
1075
+ if (reason instanceof Error || isNamedAbortError(reason)) return reason;
1076
+ return createMessageAbortedError();
1077
+ }
1078
+ function throwIfAborted(signal) {
1079
+ if (!signal?.aborted) return;
1080
+ throw normalizeAbortReason(signal.reason);
1081
+ }
1197
1082
  function isPlainRecord(value) {
1198
1083
  return value !== null && typeof value === "object" && !Array.isArray(value);
1199
1084
  }
@@ -1361,85 +1246,119 @@ function extractErrorMessage(error) {
1361
1246
  //#endregion
1362
1247
  //#region src/services/session-activity/foreground-session-tracker.ts
1363
1248
  var ForegroundSessionTracker = class {
1364
- chatStacks = /* @__PURE__ */ new Map();
1365
- counts = /* @__PURE__ */ new Map();
1249
+ requests = /* @__PURE__ */ new Map();
1366
1250
  sessionChats = /* @__PURE__ */ new Map();
1251
+ acquire(chatId) {
1252
+ if (this.requests.has(chatId)) return null;
1253
+ const state = {
1254
+ chatId,
1255
+ controller: new AbortController(),
1256
+ sessionId: null
1257
+ };
1258
+ this.requests.set(chatId, state);
1259
+ return {
1260
+ signal: state.controller.signal,
1261
+ attachSession: (sessionId) => {
1262
+ this.attachSession(chatId, sessionId);
1263
+ },
1264
+ dispose: () => {
1265
+ this.release(chatId);
1266
+ }
1267
+ };
1268
+ }
1269
+ abort(chatId, reason = createMessageAbortedError()) {
1270
+ const state = this.requests.get(chatId);
1271
+ if (!state) return false;
1272
+ if (!state.controller.signal.aborted) state.controller.abort(reason);
1273
+ return true;
1274
+ }
1367
1275
  begin(chatId, sessionId) {
1368
- const currentCount = this.counts.get(sessionId) ?? 0;
1369
- this.counts.set(sessionId, currentCount + 1);
1370
- this.incrementChat(chatId, sessionId);
1276
+ const lease = this.acquire(chatId);
1277
+ if (!lease) return () => void 0;
1278
+ lease.attachSession(sessionId);
1371
1279
  return () => {
1372
- this.decrement(chatId, sessionId);
1280
+ lease.dispose();
1373
1281
  };
1374
1282
  }
1375
1283
  clear(sessionId) {
1376
- const wasForeground = this.counts.has(sessionId);
1377
- const chatCounts = this.sessionChats.get(sessionId);
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);
1284
+ const chatIds = this.listChatIds(sessionId);
1285
+ if (chatIds.length === 0) return false;
1286
+ for (const chatId of chatIds) {
1287
+ const state = this.requests.get(chatId);
1288
+ if (state?.sessionId === sessionId) state.sessionId = null;
1389
1289
  }
1390
- return wasForeground;
1290
+ this.sessionChats.delete(sessionId);
1291
+ return true;
1292
+ }
1293
+ fail(sessionId, error) {
1294
+ const chatIds = this.listChatIds(sessionId);
1295
+ if (chatIds.length === 0) return false;
1296
+ this.clear(sessionId);
1297
+ for (const chatId of chatIds) this.abort(chatId, error);
1298
+ return true;
1391
1299
  }
1392
1300
  getActiveSessionId(chatId) {
1393
- return this.chatStacks.get(chatId)?.at(-1) ?? null;
1301
+ return this.requests.get(chatId)?.sessionId ?? null;
1302
+ }
1303
+ hasActiveRequest(chatId) {
1304
+ return this.requests.has(chatId);
1394
1305
  }
1395
1306
  isForeground(sessionId) {
1396
- return this.counts.has(sessionId);
1307
+ return this.sessionChats.has(sessionId);
1397
1308
  }
1398
1309
  listChatIds(sessionId) {
1399
- return [...this.sessionChats.get(sessionId)?.keys() ?? []];
1400
- }
1401
- decrement(chatId, sessionId) {
1402
- const currentCount = this.counts.get(sessionId);
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);
1310
+ return [...this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Set()];
1311
+ }
1312
+ attachSession(chatId, sessionId) {
1313
+ const normalizedSessionId = sessionId.trim();
1314
+ const state = this.requests.get(chatId);
1315
+ if (!state || normalizedSessionId.length === 0) return;
1316
+ if (state.sessionId === normalizedSessionId) return;
1317
+ if (state.sessionId) this.detachChatFromSession(chatId, state.sessionId);
1318
+ state.sessionId = normalizedSessionId;
1319
+ const chatIds = this.sessionChats.get(normalizedSessionId) ?? /* @__PURE__ */ new Set();
1320
+ chatIds.add(chatId);
1321
+ this.sessionChats.set(normalizedSessionId, chatIds);
1322
+ }
1323
+ release(chatId) {
1324
+ const state = this.requests.get(chatId);
1325
+ if (!state) return;
1326
+ if (state.sessionId) this.detachChatFromSession(chatId, state.sessionId);
1327
+ this.requests.delete(chatId);
1328
+ }
1329
+ detachChatFromSession(chatId, sessionId) {
1330
+ const chatIds = this.sessionChats.get(sessionId);
1331
+ if (!chatIds) return;
1332
+ chatIds.delete(chatId);
1333
+ if (chatIds.size === 0) {
1334
+ this.sessionChats.delete(sessionId);
1335
+ return;
1423
1336
  }
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);
1337
+ this.sessionChats.set(sessionId, chatIds);
1431
1338
  }
1432
1339
  };
1433
1340
  var NOOP_FOREGROUND_SESSION_TRACKER = {
1341
+ acquire() {
1342
+ return null;
1343
+ },
1344
+ abort() {
1345
+ return false;
1346
+ },
1434
1347
  begin() {
1435
1348
  return () => void 0;
1436
1349
  },
1437
1350
  clear() {
1438
1351
  return false;
1439
1352
  },
1353
+ fail() {
1354
+ return false;
1355
+ },
1440
1356
  getActiveSessionId() {
1441
1357
  return null;
1442
1358
  },
1359
+ hasActiveRequest() {
1360
+ return false;
1361
+ },
1443
1362
  isForeground() {
1444
1363
  return false;
1445
1364
  },
@@ -1456,23 +1375,40 @@ var AbortPromptUseCase = class {
1456
1375
  this.foregroundSessionTracker = foregroundSessionTracker;
1457
1376
  }
1458
1377
  async execute(input) {
1378
+ const hasForegroundRequest = this.foregroundSessionTracker.hasActiveRequest(input.chatId);
1459
1379
  const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
1460
1380
  const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
1461
1381
  const sessionId = activeSessionId ?? binding?.sessionId ?? null;
1462
- if (!sessionId) return {
1382
+ if (!hasForegroundRequest && !sessionId) return {
1463
1383
  sessionId: null,
1464
1384
  status: "no_session",
1465
1385
  sessionStatus: null
1466
1386
  };
1467
- const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
1387
+ const sessionStatuses = sessionId ? await this.opencodeClient.getSessionStatuses() : {};
1388
+ const sessionStatus = sessionId ? sessionStatuses[sessionId] ?? null : null;
1389
+ if (hasForegroundRequest) {
1390
+ if (sessionId && sessionStatus && sessionStatus.type !== "idle") await this.opencodeClient.abortSession(sessionId);
1391
+ this.foregroundSessionTracker.abort(input.chatId, createMessageAbortedError());
1392
+ return {
1393
+ sessionId,
1394
+ status: "aborted",
1395
+ sessionStatus
1396
+ };
1397
+ }
1468
1398
  if (!sessionStatus || sessionStatus.type === "idle") return {
1469
1399
  sessionId,
1470
1400
  status: "not_running",
1471
1401
  sessionStatus
1472
1402
  };
1403
+ if (!sessionId) return {
1404
+ sessionId: null,
1405
+ status: "not_running",
1406
+ sessionStatus
1407
+ };
1408
+ const runningSessionId = sessionId;
1473
1409
  return {
1474
- sessionId,
1475
- status: await this.opencodeClient.abortSession(sessionId) ? "aborted" : "not_running",
1410
+ sessionId: runningSessionId,
1411
+ status: await this.opencodeClient.abortSession(runningSessionId) ? "aborted" : "not_running",
1476
1412
  sessionStatus
1477
1413
  };
1478
1414
  }
@@ -1914,11 +1850,10 @@ var RenameSessionUseCase = class {
1914
1850
  //#endregion
1915
1851
  //#region src/use-cases/send-prompt.usecase.ts
1916
1852
  var SendPromptUseCase = class {
1917
- constructor(sessionRepo, opencodeClient, logger, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1853
+ constructor(sessionRepo, opencodeClient, logger) {
1918
1854
  this.sessionRepo = sessionRepo;
1919
1855
  this.opencodeClient = opencodeClient;
1920
1856
  this.logger = logger;
1921
- this.foregroundSessionTracker = foregroundSessionTracker;
1922
1857
  }
1923
1858
  async execute(input) {
1924
1859
  const files = input.files ?? [];
@@ -1972,7 +1907,7 @@ var SendPromptUseCase = class {
1972
1907
  }
1973
1908
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1974
1909
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1975
- const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
1910
+ input.onExecutionSession?.(executionSessionId);
1976
1911
  let result;
1977
1912
  try {
1978
1913
  result = await this.opencodeClient.promptSession({
@@ -1982,10 +1917,10 @@ var SendPromptUseCase = class {
1982
1917
  ...selectedAgent ? { agent: selectedAgent.name } : {},
1983
1918
  structured: true,
1984
1919
  ...model ? { model } : {},
1920
+ ...input.signal ? { signal: input.signal } : {},
1985
1921
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
1986
1922
  });
1987
1923
  } finally {
1988
- endForegroundSession();
1989
1924
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1990
1925
  }
1991
1926
  await this.sessionRepo.touch(input.chatId);
@@ -2253,7 +2188,7 @@ function createContainer(config, opencodeClient, logger) {
2253
2188
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2254
2189
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2255
2190
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2256
- const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
2191
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger);
2257
2192
  const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
2258
2193
  const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
2259
2194
  const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, logger);
@@ -2360,21 +2295,25 @@ function escapeMarkdownV2(value) {
2360
2295
  async function handleTelegramBotPluginEvent(runtime, event) {
2361
2296
  switch (event.type) {
2362
2297
  case "permission.asked":
2363
- await handlePermissionAsked(runtime, event.properties);
2298
+ await handlePermissionAsked(runtime, event);
2364
2299
  return;
2365
2300
  case "permission.replied":
2366
- await handlePermissionReplied(runtime, event.properties.requestID, event.properties.reply);
2301
+ await handlePermissionReplied(runtime, event);
2367
2302
  return;
2368
2303
  case "session.error":
2369
- await handleSessionError(runtime, event.properties.sessionID, event.properties.error);
2304
+ await handleSessionError(runtime, event);
2370
2305
  return;
2371
2306
  case "session.idle":
2372
- await handleSessionIdle(runtime, event.properties.sessionID);
2307
+ await handleSessionIdle(runtime, event);
2308
+ return;
2309
+ case "session.status":
2310
+ await handleSessionStatus(runtime, event);
2373
2311
  return;
2374
2312
  default: return;
2375
2313
  }
2376
2314
  }
2377
- async function handlePermissionAsked(runtime, request) {
2315
+ async function handlePermissionAsked(runtime, event) {
2316
+ const request = event.properties;
2378
2317
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2379
2318
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2380
2319
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2403,7 +2342,9 @@ async function handlePermissionAsked(runtime, request) {
2403
2342
  }
2404
2343
  }
2405
2344
  }
2406
- async function handlePermissionReplied(runtime, requestId, reply) {
2345
+ async function handlePermissionReplied(runtime, event) {
2346
+ const requestId = event.properties.requestID;
2347
+ const reply = event.properties.reply;
2407
2348
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
2408
2349
  await Promise.all(approvals.map(async (approval) => {
2409
2350
  try {
@@ -2418,27 +2359,37 @@ async function handlePermissionReplied(runtime, requestId, reply) {
2418
2359
  await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2419
2360
  }));
2420
2361
  }
2421
- async function handleSessionError(runtime, sessionId, error) {
2362
+ async function handleSessionError(runtime, event) {
2363
+ const sessionId = event.properties.sessionID;
2364
+ const error = event.properties.error;
2422
2365
  if (!sessionId) {
2423
2366
  runtime.container.logger.error({ error }, "session error received without a session id");
2424
2367
  return;
2425
2368
  }
2426
- if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2369
+ if (runtime.container.foregroundSessionTracker.fail(sessionId, error ?? /* @__PURE__ */ new Error("Unknown session error."))) {
2427
2370
  runtime.container.logger.warn({
2428
2371
  error,
2429
2372
  sessionId
2430
2373
  }, "session error suppressed for foreground Telegram session");
2431
2374
  return;
2432
2375
  }
2433
- await notifyBoundChats(runtime, sessionId, `Session failed.\n\nSession: ${sessionId}\nError: ${error?.data?.message?.trim() || error?.name?.trim() || "Unknown session error."}`);
2376
+ await notifyBoundChats(runtime, sessionId, `Session failed.\n\nSession: ${sessionId}\nError: ${(typeof error?.data?.message === "string" ? error.data.message.trim() : "") || error?.name?.trim() || "Unknown session error."}`);
2434
2377
  }
2435
- async function handleSessionIdle(runtime, sessionId) {
2378
+ async function handleSessionIdle(runtime, event) {
2379
+ const sessionId = event.properties.sessionID;
2436
2380
  if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2437
2381
  runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
2438
2382
  return;
2439
2383
  }
2440
2384
  await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2441
2385
  }
2386
+ async function handleSessionStatus(runtime, event) {
2387
+ if (event.properties.status.type !== "idle") return;
2388
+ await handleSessionIdle(runtime, {
2389
+ type: "session.idle",
2390
+ properties: { sessionID: event.properties.sessionID }
2391
+ });
2392
+ }
2442
2393
  async function notifyBoundChats(runtime, sessionId, text) {
2443
2394
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2444
2395
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
@@ -4726,23 +4677,26 @@ function parseSessionActionTarget(data, prefix) {
4726
4677
  }
4727
4678
  //#endregion
4728
4679
  //#region src/bot/handlers/prompt.handler.ts
4729
- var activePromptChats = /* @__PURE__ */ new Set();
4730
4680
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4731
4681
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4732
- if (activePromptChats.has(ctx.chat.id)) {
4682
+ const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4683
+ if (!foregroundRequest) {
4733
4684
  await ctx.reply(copy.status.alreadyProcessing);
4734
4685
  return;
4735
4686
  }
4736
4687
  let processingMessage = null;
4737
4688
  let sentTerminalReply = false;
4738
4689
  try {
4739
- activePromptChats.add(ctx.chat.id);
4740
4690
  processingMessage = await ctx.reply(copy.status.processing);
4741
4691
  const promptInput = await resolvePrompt();
4742
4692
  const telegramReply = buildTelegramPromptReply(normalizePromptReplyForDisplay((await dependencies.sendPromptUseCase.execute({
4743
4693
  chatId: ctx.chat.id,
4744
- text: promptInput.text,
4745
- files: promptInput.files
4694
+ files: promptInput.files,
4695
+ onExecutionSession: (sessionId) => {
4696
+ foregroundRequest.attachSession(sessionId);
4697
+ },
4698
+ signal: foregroundRequest.signal,
4699
+ text: promptInput.text
4746
4700
  })).assistantReply, copy, dependencies), copy);
4747
4701
  try {
4748
4702
  await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
@@ -4756,7 +4710,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4756
4710
  await ctx.reply(presentError(error, copy));
4757
4711
  sentTerminalReply = true;
4758
4712
  } finally {
4759
- activePromptChats.delete(ctx.chat.id);
4713
+ foregroundRequest.dispose();
4760
4714
  if (processingMessage && sentTerminalReply) try {
4761
4715
  await ctx.api.deleteMessage(ctx.chat.id, processingMessage.message_id);
4762
4716
  } catch (error) {