opencode-tbot 0.1.26 → 0.1.27

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,72 @@ 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
+ return this.callScopedSdkMethod("global", "health", {});
299
299
  }
300
300
  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
- });
301
+ return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
306
302
  }
307
303
  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
- });
304
+ return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
313
305
  }
314
306
  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
- });
307
+ return this.callScopedSdkMethod("session", "fork", { parameters: {
308
+ sessionID: sessionId,
309
+ ...messageId?.trim() ? { messageID: messageId.trim() } : {}
310
+ } });
326
311
  }
327
312
  async getPath() {
328
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
329
313
  return this.callScopedSdkMethod("path", "get", {});
330
314
  }
331
315
  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
- });
316
+ return this.callScopedSdkMethod("lsp", "status", { parameters: directory ? { directory } : void 0 });
340
317
  }
341
318
  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
- });
319
+ return this.callScopedSdkMethod("mcp", "status", { parameters: directory ? { directory } : void 0 });
350
320
  }
351
321
  async getSessionStatuses() {
352
322
  return this.loadSessionStatuses();
353
323
  }
354
324
  async listProjects() {
355
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
356
325
  return this.callScopedSdkMethod("project", "list", {});
357
326
  }
358
327
  async listSessions() {
359
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/session" });
360
328
  return this.callScopedSdkMethod("session", "list", {});
361
329
  }
362
330
  async getCurrentProject() {
363
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project/current" });
364
331
  return this.callScopedSdkMethod("project", "current", {});
365
332
  }
366
333
  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
- });
334
+ return this.callScopedSdkMethod("session", "create", { parameters: title ? {
335
+ directory,
336
+ title
337
+ } : { directory } });
382
338
  }
383
339
  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
- });
340
+ return this.callScopedSdkMethod("session", "update", { parameters: {
341
+ sessionID: sessionId,
342
+ title
343
+ } });
395
344
  }
396
345
  async listAgents() {
397
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/agent" });
398
346
  return this.callScopedSdkMethod("app", "agents", {});
399
347
  }
400
348
  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
- });
349
+ return this.callScopedSdkMethod("permission", "list", { parameters: directory ? { directory } : void 0 });
409
350
  }
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
- });
351
+ async replyToPermission(requestId, reply, message, _sessionId) {
352
+ return this.callScopedSdkMethod("permission", "reply", { parameters: {
353
+ requestID: requestId,
354
+ reply,
355
+ ...message?.trim() ? { message: message.trim() } : {}
356
+ } });
445
357
  }
446
358
  async listModels() {
447
359
  const now = Date.now();
@@ -466,9 +378,10 @@ var OpenCodeClient = class {
466
378
  url: file.url
467
379
  }))];
468
380
  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), {
381
+ throwIfAborted(input.signal);
382
+ const knownMessageIds = await this.captureKnownMessageIds(input.sessionId, input.signal);
383
+ await this.sendPromptRequest(input, parts);
384
+ return buildPromptSessionResult(await this.resolvePromptResponse(input, null, knownMessageIds, startedAt), {
472
385
  emptyResponseText: EMPTY_RESPONSE_TEXT,
473
386
  finishedAt: Date.now(),
474
387
  startedAt,
@@ -488,10 +401,12 @@ var OpenCodeClient = class {
488
401
  };
489
402
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
490
403
  let lastProgressAt = Date.now();
404
+ let lastStatus = null;
491
405
  const deadlineAt = startedAt + this.promptTimeoutPolicy.waitTimeoutMs;
492
406
  let idleStatusSeen = false;
493
407
  let attempt = 0;
494
408
  while (true) {
409
+ throwIfAborted(input.signal);
495
410
  const remainingWaitMs = deadlineAt - Date.now();
496
411
  const remainingInactivityMs = this.promptTimeoutPolicy.recoveryInactivityTimeoutMs - (Date.now() - lastProgressAt);
497
412
  if (remainingWaitMs <= 0 || remainingInactivityMs <= 0) break;
@@ -500,10 +415,10 @@ var OpenCodeClient = class {
500
415
  if (delayMs > 0) {
501
416
  const remainingMs = Math.min(remainingWaitMs, remainingInactivityMs);
502
417
  if (remainingMs <= 0) break;
503
- await delay(Math.min(delayMs, remainingMs));
418
+ await delay(Math.min(delayMs, remainingMs), input.signal);
504
419
  }
505
420
  if (messageId) {
506
- const next = await this.fetchPromptMessage(input.sessionId, messageId);
421
+ const next = await this.fetchPromptMessage(input.sessionId, messageId, input.signal);
507
422
  if (next) {
508
423
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
509
424
  if (nextCandidate) {
@@ -513,10 +428,10 @@ var OpenCodeClient = class {
513
428
  }
514
429
  bestCandidate = nextCandidate;
515
430
  }
516
- if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
431
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
517
432
  }
518
433
  }
519
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
434
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
520
435
  if (latest) {
521
436
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
522
437
  if (nextCandidate) {
@@ -526,9 +441,10 @@ var OpenCodeClient = class {
526
441
  }
527
442
  bestCandidate = nextCandidate;
528
443
  }
529
- if (bestCandidate && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
444
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
530
445
  }
531
- const status = await this.fetchPromptSessionStatus(input.sessionId);
446
+ const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
447
+ lastStatus = status;
532
448
  if (status?.type === "busy" || status?.type === "retry") {
533
449
  lastProgressAt = Date.now();
534
450
  idleStatusSeen = false;
@@ -536,12 +452,14 @@ var OpenCodeClient = class {
536
452
  if (idleStatusSeen) break;
537
453
  idleStatusSeen = true;
538
454
  }
539
- if (bestCandidate && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
455
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
540
456
  if (Date.now() >= deadlineAt) break;
541
457
  }
542
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
458
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
543
459
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
544
- if (!resolved || shouldPollPromptMessage(resolved, structured)) {
460
+ const requestScopedResolved = resolved && isPromptResponseForCurrentRequest(resolved, candidateOptions) ? resolved : null;
461
+ if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured))) throw createMessageAbortedError();
462
+ if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved, structured)) {
545
463
  const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
546
464
  const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
547
465
  const error = createOpenCodePromptTimeoutError({
@@ -553,7 +471,6 @@ var OpenCodeClient = class {
553
471
  this.logPromptRequest("warn", {
554
472
  lastProgressAt,
555
473
  messageId: messageId ?? void 0,
556
- sdkFlavor: this.sdkFlavor,
557
474
  sessionId: input.sessionId,
558
475
  stage: "final-scan",
559
476
  timeoutMs,
@@ -561,31 +478,26 @@ var OpenCodeClient = class {
561
478
  }, "OpenCode prompt recovery timed out");
562
479
  throw error;
563
480
  }
564
- return resolved;
481
+ return requestScopedResolved;
565
482
  }
566
- async fetchPromptMessage(sessionId, messageId) {
483
+ async fetchPromptMessage(sessionId, messageId, signal) {
567
484
  try {
568
485
  return await this.runPromptRequestWithTimeout({
569
486
  sessionId,
570
487
  stage: "poll-message",
571
488
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
572
489
  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;
490
+ }, async (requestSignal) => {
576
491
  return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
577
- legacyOptions: { path: {
578
- id: sessionId,
579
- messageID: messageId
580
- } },
581
- signal,
582
- v2Parameters: {
492
+ parameters: {
583
493
  sessionID: sessionId,
584
494
  messageID: messageId
585
- }
495
+ },
496
+ signal: requestSignal
586
497
  }));
587
- });
498
+ }, signal);
588
499
  } catch (error) {
500
+ if (isPromptRequestAbort(error)) throw error;
589
501
  this.logPromptRequestFailure(error, {
590
502
  sessionId,
591
503
  stage: "poll-message",
@@ -595,36 +507,28 @@ var OpenCodeClient = class {
595
507
  return null;
596
508
  }
597
509
  }
598
- async captureKnownMessageIds(sessionId) {
599
- const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages");
510
+ async captureKnownMessageIds(sessionId, signal) {
511
+ const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages", signal);
600
512
  if (!messages) return /* @__PURE__ */ new Set();
601
513
  return new Set(messages.map((message) => extractMessageId(message.info)).filter((id) => typeof id === "string" && id.length > 0));
602
514
  }
603
- async fetchRecentPromptMessages(sessionId, stage) {
515
+ async fetchRecentPromptMessages(sessionId, stage, signal) {
604
516
  try {
605
517
  return await this.runPromptRequestWithTimeout({
606
518
  sessionId,
607
519
  stage,
608
520
  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;
521
+ }, async (requestSignal) => {
615
522
  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: {
523
+ parameters: {
622
524
  sessionID: sessionId,
623
525
  limit: PROMPT_MESSAGE_POLL_LIMIT
624
- }
526
+ },
527
+ signal: requestSignal
625
528
  }));
626
- });
529
+ }, signal);
627
530
  } catch (error) {
531
+ if (isPromptRequestAbort(error)) throw error;
628
532
  this.logPromptRequestFailure(error, {
629
533
  sessionId,
630
534
  stage,
@@ -633,14 +537,15 @@ var OpenCodeClient = class {
633
537
  return null;
634
538
  }
635
539
  }
636
- async fetchPromptSessionStatus(sessionId) {
540
+ async fetchPromptSessionStatus(sessionId, signal) {
637
541
  try {
638
542
  return (await this.runPromptRequestWithTimeout({
639
543
  sessionId,
640
544
  stage: "poll-status",
641
545
  timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
642
- }, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
546
+ }, async (requestSignal) => this.loadSessionStatuses(requestSignal), signal))[sessionId] ?? null;
643
547
  } catch (error) {
548
+ if (isPromptRequestAbort(error)) throw error;
644
549
  this.logPromptRequestFailure(error, {
645
550
  sessionId,
646
551
  stage: "poll-status",
@@ -649,8 +554,8 @@ var OpenCodeClient = class {
649
554
  return null;
650
555
  }
651
556
  }
652
- async findLatestPromptResponse(sessionId, options, stage) {
653
- const messages = await this.fetchRecentPromptMessages(sessionId, stage);
557
+ async findLatestPromptResponse(sessionId, options, stage, signal) {
558
+ const messages = await this.fetchRecentPromptMessages(sessionId, stage, signal);
654
559
  if (!messages || messages.length === 0) return null;
655
560
  return selectPromptResponseCandidate(messages, options);
656
561
  }
@@ -666,11 +571,9 @@ var OpenCodeClient = class {
666
571
  return models;
667
572
  }
668
573
  async loadConfig() {
669
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config" });
670
574
  return this.callScopedSdkMethod("config", "get", {});
671
575
  }
672
576
  async loadProviderCatalog() {
673
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config/providers" });
674
577
  return this.callScopedSdkMethod("config", "providers", {});
675
578
  }
676
579
  async sendPromptRequest(input, parts) {
@@ -686,26 +589,6 @@ var OpenCodeClient = class {
686
589
  ...requestBody
687
590
  };
688
591
  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
592
  if (typeof this.client.session?.promptAsync === "function") {
710
593
  await this.runPromptRequestWithTimeout({
711
594
  sessionId: input.sessionId,
@@ -713,15 +596,11 @@ var OpenCodeClient = class {
713
596
  timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
714
597
  }, async (signal) => {
715
598
  await this.callScopedSdkMethod("session", "promptAsync", {
716
- legacyOptions: {
717
- body: requestBody,
718
- path: { id: input.sessionId }
719
- },
720
- signal,
721
- v2Parameters: requestParameters
599
+ parameters: requestParameters,
600
+ signal
722
601
  });
723
- });
724
- return null;
602
+ }, input.signal);
603
+ return;
725
604
  }
726
605
  } catch (error) {
727
606
  this.logPromptRequestFailure(error, {
@@ -731,27 +610,33 @@ var OpenCodeClient = class {
731
610
  });
732
611
  throw error;
733
612
  }
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
- }));
613
+ throw new Error("OpenCode SDK client does not expose session.promptAsync().");
743
614
  }
744
615
  async loadSessionStatuses(signal) {
745
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
746
- url: "/session/status",
747
- ...signal ? { signal } : {}
616
+ return this.callScopedSdkMethod("session", "status", {
617
+ signal,
618
+ parameters: void 0
748
619
  });
749
- return this.callScopedSdkMethod("session", "status", { signal });
750
620
  }
751
- async runPromptRequestWithTimeout(input, operation) {
621
+ async runPromptRequestWithTimeout(input, operation, signal) {
752
622
  const startedAt = Date.now();
753
623
  const controller = new AbortController();
624
+ const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
754
625
  let timeoutHandle = null;
626
+ let removeAbortListener = () => void 0;
627
+ const abortPromise = signal ? new Promise((_, reject) => {
628
+ const onAbort = () => {
629
+ reject(normalizeAbortReason(signal.reason));
630
+ };
631
+ if (signal.aborted) {
632
+ onAbort();
633
+ return;
634
+ }
635
+ signal.addEventListener("abort", onAbort, { once: true });
636
+ removeAbortListener = () => {
637
+ signal.removeEventListener("abort", onAbort);
638
+ };
639
+ }) : null;
755
640
  const timeoutPromise = new Promise((_, reject) => {
756
641
  timeoutHandle = setTimeout(() => {
757
642
  reject(createOpenCodePromptTimeoutError({
@@ -765,9 +650,14 @@ var OpenCodeClient = class {
765
650
  }, input.timeoutMs);
766
651
  });
767
652
  try {
768
- return await Promise.race([operation(controller.signal), timeoutPromise]);
653
+ return await Promise.race([
654
+ operation(requestSignal),
655
+ timeoutPromise,
656
+ ...abortPromise ? [abortPromise] : []
657
+ ]);
769
658
  } finally {
770
659
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
660
+ removeAbortListener();
771
661
  }
772
662
  }
773
663
  logPromptRequestFailure(error, input) {
@@ -776,7 +666,6 @@ var OpenCodeClient = class {
776
666
  endpointKind: resolvePromptEndpointKind(error.data.stage),
777
667
  elapsedMs: error.data.elapsedMs,
778
668
  messageId: error.data.messageId,
779
- sdkFlavor: this.sdkFlavor,
780
669
  sessionId: error.data.sessionId,
781
670
  stage: error.data.stage,
782
671
  timeoutMs: error.data.timeoutMs
@@ -787,7 +676,6 @@ var OpenCodeClient = class {
787
676
  endpointKind: resolvePromptEndpointKind(input.stage),
788
677
  error,
789
678
  messageId: input.messageId ?? void 0,
790
- sdkFlavor: this.sdkFlavor,
791
679
  sessionId: input.sessionId,
792
680
  stage: input.stage,
793
681
  timeoutMs: input.timeoutMs
@@ -978,8 +866,23 @@ function extractMessageId(message) {
978
866
  if (!isPlainRecord(message)) return null;
979
867
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
980
868
  }
981
- function delay(ms) {
982
- return new Promise((resolve) => setTimeout(resolve, ms));
869
+ function delay(ms, signal) {
870
+ return new Promise((resolve, reject) => {
871
+ const handle = setTimeout(() => {
872
+ signal?.removeEventListener("abort", onAbort);
873
+ resolve();
874
+ }, ms);
875
+ const onAbort = () => {
876
+ clearTimeout(handle);
877
+ signal?.removeEventListener("abort", onAbort);
878
+ reject(normalizeAbortReason(signal?.reason));
879
+ };
880
+ if (signal?.aborted) {
881
+ onAbort();
882
+ return;
883
+ }
884
+ signal?.addEventListener("abort", onAbort, { once: true });
885
+ });
983
886
  }
984
887
  function didPromptResponseAdvance(previous, next, structured) {
985
888
  return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
@@ -1063,46 +966,6 @@ function resolvePromptTimeoutPolicy(input) {
1063
966
  waitTimeoutMs: input.waitTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.waitTimeoutMs
1064
967
  };
1065
968
  }
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
969
  function normalizeAssistantError(value) {
1107
970
  if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
1108
971
  return {
@@ -1120,23 +983,6 @@ function isCompletedEmptyPromptResponse(data, structured) {
1120
983
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
1121
984
  return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
1122
985
  }
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
986
  function extractStructuredPayload(message) {
1141
987
  if (!isPlainRecord(message)) return null;
1142
988
  if ("structured" in message && message.structured !== void 0) return message.structured;
@@ -1151,7 +997,7 @@ function selectPromptResponseCandidate(candidates, options) {
1151
997
  function comparePromptResponseCandidates(left, right, options) {
1152
998
  const leftRank = getPromptResponseCandidateRank(left, options);
1153
999
  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;
1000
+ 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
1001
  }
1156
1002
  function getPromptResponseCandidateRank(message, options) {
1157
1003
  const assistant = toAssistantMessage(message.info);
@@ -1166,7 +1012,7 @@ function getPromptResponseCandidateRank(message, options) {
1166
1012
  };
1167
1013
  }
1168
1014
  function resolvePromptCandidateStartTime(startedAt, initialMessage) {
1169
- if (!initialMessage) return null;
1015
+ if (!initialMessage) return startedAt;
1170
1016
  const initialCreatedAt = coerceFiniteNumber(toAssistantMessage(initialMessage.info)?.time?.created);
1171
1017
  if (initialCreatedAt === null) return startedAt;
1172
1018
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
@@ -1190,10 +1036,28 @@ function isPromptResponseNewSinceRequestStart(messageId, createdAt, knownMessage
1190
1036
  if (requestStartedAt === null) return true;
1191
1037
  return createdAt >= requestStartedAt;
1192
1038
  }
1039
+ function isPromptResponseForCurrentRequest(response, options) {
1040
+ const rank = getPromptResponseCandidateRank(response, options);
1041
+ return rank.isInitial || rank.sharesParent || rank.isNewSinceRequestStart;
1042
+ }
1193
1043
  function areComparablePromptTimestamps(left, right) {
1194
1044
  const epochThresholdMs = 0xe8d4a51000;
1195
1045
  return left >= epochThresholdMs && right >= epochThresholdMs;
1196
1046
  }
1047
+ function isPromptRequestAbort(error) {
1048
+ return error instanceof OpenCodeMessageAbortedError || error instanceof Error && error.name === "AbortError" || isNamedAbortError(error);
1049
+ }
1050
+ function isNamedAbortError(error) {
1051
+ return !!error && typeof error === "object" && "name" in error && error.name === "MessageAbortedError";
1052
+ }
1053
+ function normalizeAbortReason(reason) {
1054
+ if (reason instanceof Error || isNamedAbortError(reason)) return reason;
1055
+ return createMessageAbortedError();
1056
+ }
1057
+ function throwIfAborted(signal) {
1058
+ if (!signal?.aborted) return;
1059
+ throw normalizeAbortReason(signal.reason);
1060
+ }
1197
1061
  function isPlainRecord(value) {
1198
1062
  return value !== null && typeof value === "object" && !Array.isArray(value);
1199
1063
  }
@@ -1361,85 +1225,119 @@ function extractErrorMessage(error) {
1361
1225
  //#endregion
1362
1226
  //#region src/services/session-activity/foreground-session-tracker.ts
1363
1227
  var ForegroundSessionTracker = class {
1364
- chatStacks = /* @__PURE__ */ new Map();
1365
- counts = /* @__PURE__ */ new Map();
1228
+ requests = /* @__PURE__ */ new Map();
1366
1229
  sessionChats = /* @__PURE__ */ new Map();
1230
+ acquire(chatId) {
1231
+ if (this.requests.has(chatId)) return null;
1232
+ const state = {
1233
+ chatId,
1234
+ controller: new AbortController(),
1235
+ sessionId: null
1236
+ };
1237
+ this.requests.set(chatId, state);
1238
+ return {
1239
+ signal: state.controller.signal,
1240
+ attachSession: (sessionId) => {
1241
+ this.attachSession(chatId, sessionId);
1242
+ },
1243
+ dispose: () => {
1244
+ this.release(chatId);
1245
+ }
1246
+ };
1247
+ }
1248
+ abort(chatId, reason = createMessageAbortedError()) {
1249
+ const state = this.requests.get(chatId);
1250
+ if (!state) return false;
1251
+ if (!state.controller.signal.aborted) state.controller.abort(reason);
1252
+ return true;
1253
+ }
1367
1254
  begin(chatId, sessionId) {
1368
- const currentCount = this.counts.get(sessionId) ?? 0;
1369
- this.counts.set(sessionId, currentCount + 1);
1370
- this.incrementChat(chatId, sessionId);
1255
+ const lease = this.acquire(chatId);
1256
+ if (!lease) return () => void 0;
1257
+ lease.attachSession(sessionId);
1371
1258
  return () => {
1372
- this.decrement(chatId, sessionId);
1259
+ lease.dispose();
1373
1260
  };
1374
1261
  }
1375
1262
  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);
1263
+ const chatIds = this.listChatIds(sessionId);
1264
+ if (chatIds.length === 0) return false;
1265
+ for (const chatId of chatIds) {
1266
+ const state = this.requests.get(chatId);
1267
+ if (state?.sessionId === sessionId) state.sessionId = null;
1389
1268
  }
1390
- return wasForeground;
1269
+ this.sessionChats.delete(sessionId);
1270
+ return true;
1271
+ }
1272
+ fail(sessionId, error) {
1273
+ const chatIds = this.listChatIds(sessionId);
1274
+ if (chatIds.length === 0) return false;
1275
+ this.clear(sessionId);
1276
+ for (const chatId of chatIds) this.abort(chatId, error);
1277
+ return true;
1391
1278
  }
1392
1279
  getActiveSessionId(chatId) {
1393
- return this.chatStacks.get(chatId)?.at(-1) ?? null;
1280
+ return this.requests.get(chatId)?.sessionId ?? null;
1281
+ }
1282
+ hasActiveRequest(chatId) {
1283
+ return this.requests.has(chatId);
1394
1284
  }
1395
1285
  isForeground(sessionId) {
1396
- return this.counts.has(sessionId);
1286
+ return this.sessionChats.has(sessionId);
1397
1287
  }
1398
1288
  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);
1289
+ return [...this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Set()];
1290
+ }
1291
+ attachSession(chatId, sessionId) {
1292
+ const normalizedSessionId = sessionId.trim();
1293
+ const state = this.requests.get(chatId);
1294
+ if (!state || normalizedSessionId.length === 0) return;
1295
+ if (state.sessionId === normalizedSessionId) return;
1296
+ if (state.sessionId) this.detachChatFromSession(chatId, state.sessionId);
1297
+ state.sessionId = normalizedSessionId;
1298
+ const chatIds = this.sessionChats.get(normalizedSessionId) ?? /* @__PURE__ */ new Set();
1299
+ chatIds.add(chatId);
1300
+ this.sessionChats.set(normalizedSessionId, chatIds);
1301
+ }
1302
+ release(chatId) {
1303
+ const state = this.requests.get(chatId);
1304
+ if (!state) return;
1305
+ if (state.sessionId) this.detachChatFromSession(chatId, state.sessionId);
1306
+ this.requests.delete(chatId);
1307
+ }
1308
+ detachChatFromSession(chatId, sessionId) {
1309
+ const chatIds = this.sessionChats.get(sessionId);
1310
+ if (!chatIds) return;
1311
+ chatIds.delete(chatId);
1312
+ if (chatIds.size === 0) {
1313
+ this.sessionChats.delete(sessionId);
1314
+ return;
1423
1315
  }
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);
1316
+ this.sessionChats.set(sessionId, chatIds);
1431
1317
  }
1432
1318
  };
1433
1319
  var NOOP_FOREGROUND_SESSION_TRACKER = {
1320
+ acquire() {
1321
+ return null;
1322
+ },
1323
+ abort() {
1324
+ return false;
1325
+ },
1434
1326
  begin() {
1435
1327
  return () => void 0;
1436
1328
  },
1437
1329
  clear() {
1438
1330
  return false;
1439
1331
  },
1332
+ fail() {
1333
+ return false;
1334
+ },
1440
1335
  getActiveSessionId() {
1441
1336
  return null;
1442
1337
  },
1338
+ hasActiveRequest() {
1339
+ return false;
1340
+ },
1443
1341
  isForeground() {
1444
1342
  return false;
1445
1343
  },
@@ -1456,23 +1354,40 @@ var AbortPromptUseCase = class {
1456
1354
  this.foregroundSessionTracker = foregroundSessionTracker;
1457
1355
  }
1458
1356
  async execute(input) {
1357
+ const hasForegroundRequest = this.foregroundSessionTracker.hasActiveRequest(input.chatId);
1459
1358
  const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
1460
1359
  const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
1461
1360
  const sessionId = activeSessionId ?? binding?.sessionId ?? null;
1462
- if (!sessionId) return {
1361
+ if (!hasForegroundRequest && !sessionId) return {
1463
1362
  sessionId: null,
1464
1363
  status: "no_session",
1465
1364
  sessionStatus: null
1466
1365
  };
1467
- const sessionStatus = (await this.opencodeClient.getSessionStatuses())[sessionId] ?? null;
1366
+ const sessionStatuses = sessionId ? await this.opencodeClient.getSessionStatuses() : {};
1367
+ const sessionStatus = sessionId ? sessionStatuses[sessionId] ?? null : null;
1368
+ if (hasForegroundRequest) {
1369
+ if (sessionId && sessionStatus && sessionStatus.type !== "idle") await this.opencodeClient.abortSession(sessionId);
1370
+ this.foregroundSessionTracker.abort(input.chatId, createMessageAbortedError());
1371
+ return {
1372
+ sessionId,
1373
+ status: "aborted",
1374
+ sessionStatus
1375
+ };
1376
+ }
1468
1377
  if (!sessionStatus || sessionStatus.type === "idle") return {
1469
1378
  sessionId,
1470
1379
  status: "not_running",
1471
1380
  sessionStatus
1472
1381
  };
1382
+ if (!sessionId) return {
1383
+ sessionId: null,
1384
+ status: "not_running",
1385
+ sessionStatus
1386
+ };
1387
+ const runningSessionId = sessionId;
1473
1388
  return {
1474
- sessionId,
1475
- status: await this.opencodeClient.abortSession(sessionId) ? "aborted" : "not_running",
1389
+ sessionId: runningSessionId,
1390
+ status: await this.opencodeClient.abortSession(runningSessionId) ? "aborted" : "not_running",
1476
1391
  sessionStatus
1477
1392
  };
1478
1393
  }
@@ -1914,11 +1829,10 @@ var RenameSessionUseCase = class {
1914
1829
  //#endregion
1915
1830
  //#region src/use-cases/send-prompt.usecase.ts
1916
1831
  var SendPromptUseCase = class {
1917
- constructor(sessionRepo, opencodeClient, logger, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1832
+ constructor(sessionRepo, opencodeClient, logger) {
1918
1833
  this.sessionRepo = sessionRepo;
1919
1834
  this.opencodeClient = opencodeClient;
1920
1835
  this.logger = logger;
1921
- this.foregroundSessionTracker = foregroundSessionTracker;
1922
1836
  }
1923
1837
  async execute(input) {
1924
1838
  const files = input.files ?? [];
@@ -1972,7 +1886,7 @@ var SendPromptUseCase = class {
1972
1886
  }
1973
1887
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1974
1888
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1975
- const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
1889
+ input.onExecutionSession?.(executionSessionId);
1976
1890
  let result;
1977
1891
  try {
1978
1892
  result = await this.opencodeClient.promptSession({
@@ -1982,10 +1896,10 @@ var SendPromptUseCase = class {
1982
1896
  ...selectedAgent ? { agent: selectedAgent.name } : {},
1983
1897
  structured: true,
1984
1898
  ...model ? { model } : {},
1899
+ ...input.signal ? { signal: input.signal } : {},
1985
1900
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
1986
1901
  });
1987
1902
  } finally {
1988
- endForegroundSession();
1989
1903
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1990
1904
  }
1991
1905
  await this.sessionRepo.touch(input.chatId);
@@ -2253,7 +2167,7 @@ function createContainer(config, opencodeClient, logger) {
2253
2167
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2254
2168
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2255
2169
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2256
- const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
2170
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger);
2257
2171
  const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
2258
2172
  const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
2259
2173
  const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, logger);
@@ -2360,21 +2274,25 @@ function escapeMarkdownV2(value) {
2360
2274
  async function handleTelegramBotPluginEvent(runtime, event) {
2361
2275
  switch (event.type) {
2362
2276
  case "permission.asked":
2363
- await handlePermissionAsked(runtime, event.properties);
2277
+ await handlePermissionAsked(runtime, event);
2364
2278
  return;
2365
2279
  case "permission.replied":
2366
- await handlePermissionReplied(runtime, event.properties.requestID, event.properties.reply);
2280
+ await handlePermissionReplied(runtime, event);
2367
2281
  return;
2368
2282
  case "session.error":
2369
- await handleSessionError(runtime, event.properties.sessionID, event.properties.error);
2283
+ await handleSessionError(runtime, event);
2370
2284
  return;
2371
2285
  case "session.idle":
2372
- await handleSessionIdle(runtime, event.properties.sessionID);
2286
+ await handleSessionIdle(runtime, event);
2287
+ return;
2288
+ case "session.status":
2289
+ await handleSessionStatus(runtime, event);
2373
2290
  return;
2374
2291
  default: return;
2375
2292
  }
2376
2293
  }
2377
- async function handlePermissionAsked(runtime, request) {
2294
+ async function handlePermissionAsked(runtime, event) {
2295
+ const request = event.properties;
2378
2296
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2379
2297
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2380
2298
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2403,7 +2321,9 @@ async function handlePermissionAsked(runtime, request) {
2403
2321
  }
2404
2322
  }
2405
2323
  }
2406
- async function handlePermissionReplied(runtime, requestId, reply) {
2324
+ async function handlePermissionReplied(runtime, event) {
2325
+ const requestId = event.properties.requestID;
2326
+ const reply = event.properties.reply;
2407
2327
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
2408
2328
  await Promise.all(approvals.map(async (approval) => {
2409
2329
  try {
@@ -2418,27 +2338,37 @@ async function handlePermissionReplied(runtime, requestId, reply) {
2418
2338
  await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2419
2339
  }));
2420
2340
  }
2421
- async function handleSessionError(runtime, sessionId, error) {
2341
+ async function handleSessionError(runtime, event) {
2342
+ const sessionId = event.properties.sessionID;
2343
+ const error = event.properties.error;
2422
2344
  if (!sessionId) {
2423
2345
  runtime.container.logger.error({ error }, "session error received without a session id");
2424
2346
  return;
2425
2347
  }
2426
- if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2348
+ if (runtime.container.foregroundSessionTracker.fail(sessionId, error ?? /* @__PURE__ */ new Error("Unknown session error."))) {
2427
2349
  runtime.container.logger.warn({
2428
2350
  error,
2429
2351
  sessionId
2430
2352
  }, "session error suppressed for foreground Telegram session");
2431
2353
  return;
2432
2354
  }
2433
- await notifyBoundChats(runtime, sessionId, `Session failed.\n\nSession: ${sessionId}\nError: ${error?.data?.message?.trim() || error?.name?.trim() || "Unknown session error."}`);
2355
+ 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
2356
  }
2435
- async function handleSessionIdle(runtime, sessionId) {
2357
+ async function handleSessionIdle(runtime, event) {
2358
+ const sessionId = event.properties.sessionID;
2436
2359
  if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2437
2360
  runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
2438
2361
  return;
2439
2362
  }
2440
2363
  await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2441
2364
  }
2365
+ async function handleSessionStatus(runtime, event) {
2366
+ if (event.properties.status.type !== "idle") return;
2367
+ await handleSessionIdle(runtime, {
2368
+ type: "session.idle",
2369
+ properties: { sessionID: event.properties.sessionID }
2370
+ });
2371
+ }
2442
2372
  async function notifyBoundChats(runtime, sessionId, text) {
2443
2373
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2444
2374
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
@@ -4726,23 +4656,26 @@ function parseSessionActionTarget(data, prefix) {
4726
4656
  }
4727
4657
  //#endregion
4728
4658
  //#region src/bot/handlers/prompt.handler.ts
4729
- var activePromptChats = /* @__PURE__ */ new Set();
4730
4659
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4731
4660
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4732
- if (activePromptChats.has(ctx.chat.id)) {
4661
+ const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4662
+ if (!foregroundRequest) {
4733
4663
  await ctx.reply(copy.status.alreadyProcessing);
4734
4664
  return;
4735
4665
  }
4736
4666
  let processingMessage = null;
4737
4667
  let sentTerminalReply = false;
4738
4668
  try {
4739
- activePromptChats.add(ctx.chat.id);
4740
4669
  processingMessage = await ctx.reply(copy.status.processing);
4741
4670
  const promptInput = await resolvePrompt();
4742
4671
  const telegramReply = buildTelegramPromptReply(normalizePromptReplyForDisplay((await dependencies.sendPromptUseCase.execute({
4743
4672
  chatId: ctx.chat.id,
4744
- text: promptInput.text,
4745
- files: promptInput.files
4673
+ files: promptInput.files,
4674
+ onExecutionSession: (sessionId) => {
4675
+ foregroundRequest.attachSession(sessionId);
4676
+ },
4677
+ signal: foregroundRequest.signal,
4678
+ text: promptInput.text
4746
4679
  })).assistantReply, copy, dependencies), copy);
4747
4680
  try {
4748
4681
  await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
@@ -4756,7 +4689,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4756
4689
  await ctx.reply(presentError(error, copy));
4757
4690
  sentTerminalReply = true;
4758
4691
  } finally {
4759
- activePromptChats.delete(ctx.chat.id);
4692
+ foregroundRequest.dispose();
4760
4693
  if (processingMessage && sentTerminalReply) try {
4761
4694
  await ctx.api.deleteMessage(ctx.chat.id, processingMessage.message_id);
4762
4695
  } catch (error) {