opencode-tbot 0.1.25 → 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
@@ -1,4 +1,4 @@
1
- import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-CCeFjxSf.js";
1
+ import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-DNeV2Ckw.js";
2
2
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -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,
@@ -227,11 +238,13 @@ var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
227
238
  1e3
228
239
  ];
229
240
  var PROMPT_MESSAGE_POLL_INTERVAL_MS = 2e3;
230
- var PROMPT_POLL_REQUEST_TIMEOUT_MS = 15e3;
231
- var PROMPT_SEND_TIMEOUT_MS = 3e4;
232
- var PROMPT_MESSAGE_POLL_TIMEOUT_MS = 6e4;
233
241
  var PROMPT_MESSAGE_POLL_LIMIT = 20;
234
242
  var PROMPT_LOG_SERVICE = "opencode-tbot";
243
+ var DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY = {
244
+ pollRequestTimeoutMs: 15e3,
245
+ recoveryInactivityTimeoutMs: 12e4,
246
+ waitTimeoutMs: 18e5
247
+ };
235
248
  var STRUCTURED_REPLY_SCHEMA = {
236
249
  type: "json_schema",
237
250
  retryCount: 2,
@@ -253,134 +266,94 @@ var StructuredReplySchema = z.object({ body_md: z.string() });
253
266
  var OpenCodeClient = class {
254
267
  client;
255
268
  fetchFn;
256
- promptRequestTimeouts = {
257
- pollRequestMs: PROMPT_POLL_REQUEST_TIMEOUT_MS,
258
- sendMs: PROMPT_SEND_TIMEOUT_MS,
259
- totalPollMs: PROMPT_MESSAGE_POLL_TIMEOUT_MS
260
- };
269
+ promptTimeoutPolicy;
261
270
  modelCache = {
262
271
  expiresAt: 0,
263
272
  promise: null,
264
273
  value: null
265
274
  };
266
- constructor(options, client, fetchFn = fetch) {
275
+ constructor(options, client, fetchFn = fetch, promptTimeoutPolicy = {}) {
267
276
  if (!options && !client) throw new Error("OpenCodeClient requires either base URL options or an injected SDK client.");
268
277
  this.client = client ?? createOpencodeClient(buildOpenCodeSdkConfig(options));
269
278
  this.fetchFn = fetchFn;
279
+ this.promptTimeoutPolicy = resolvePromptTimeoutPolicy(promptTimeoutPolicy);
280
+ }
281
+ configurePromptTimeoutPolicy(promptTimeoutPolicy) {
282
+ this.promptTimeoutPolicy = resolvePromptTimeoutPolicy({
283
+ ...this.promptTimeoutPolicy,
284
+ ...promptTimeoutPolicy
285
+ });
286
+ }
287
+ async callScopedSdkMethod(scope, method, input) {
288
+ const target = this.client[scope];
289
+ const handler = target?.[method];
290
+ if (typeof handler !== "function") throw new Error(`OpenCode SDK client does not expose a compatible ${scope}.${method} method.`);
291
+ const options = {
292
+ ...SDK_OPTIONS,
293
+ ...input.signal ? { signal: input.signal } : {}
294
+ };
295
+ return unwrapSdkData(input.parameters === void 0 ? await handler.call(target, options) : await handler.call(target, input.parameters, options));
270
296
  }
271
297
  async getHealth() {
272
- const rawClient = getRawSdkClient(this.client);
273
- if (rawClient?.get) return await this.requestRaw("get", { url: "/global/health" });
274
- const healthEndpoint = this.client.global?.health;
275
- if (typeof healthEndpoint === "function") return unwrapSdkData(await healthEndpoint.call(this.client.global, SDK_OPTIONS));
276
- if (!rawClient?.get) throw new Error("OpenCode SDK client does not expose a compatible health endpoint.");
277
- return this.requestRaw("get", { url: "/global/health" });
298
+ return this.callScopedSdkMethod("global", "health", {});
278
299
  }
279
300
  async abortSession(sessionId) {
280
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
281
- url: "/session/{sessionID}/abort",
282
- path: { sessionID: sessionId }
283
- });
284
- return unwrapSdkData(await this.client.session.abort({ sessionID: sessionId }, SDK_OPTIONS));
301
+ return this.callScopedSdkMethod("session", "abort", { parameters: { sessionID: sessionId } });
285
302
  }
286
303
  async deleteSession(sessionId) {
287
- if (hasRawSdkMethod(this.client, "delete")) return this.requestRaw("delete", {
288
- url: "/session/{sessionID}",
289
- path: { sessionID: sessionId }
290
- });
291
- return unwrapSdkData(await this.client.session.delete({ sessionID: sessionId }, SDK_OPTIONS));
304
+ return this.callScopedSdkMethod("session", "delete", { parameters: { sessionID: sessionId } });
292
305
  }
293
306
  async forkSession(sessionId, messageId) {
294
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
295
- url: "/session/{sessionID}/fork",
296
- path: { sessionID: sessionId },
297
- ...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
298
- });
299
- return unwrapSdkData(await this.client.session.fork({
307
+ return this.callScopedSdkMethod("session", "fork", { parameters: {
300
308
  sessionID: sessionId,
301
309
  ...messageId?.trim() ? { messageID: messageId.trim() } : {}
302
- }, SDK_OPTIONS));
310
+ } });
303
311
  }
304
312
  async getPath() {
305
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/path" });
306
- return unwrapSdkData(await this.client.path.get(void 0, SDK_OPTIONS));
313
+ return this.callScopedSdkMethod("path", "get", {});
307
314
  }
308
315
  async listLspStatuses(directory) {
309
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
310
- url: "/lsp",
311
- ...directory ? { query: { directory } } : {}
312
- });
313
- return unwrapSdkData(await this.client.lsp.status(directory ? { directory } : void 0, SDK_OPTIONS));
316
+ return this.callScopedSdkMethod("lsp", "status", { parameters: directory ? { directory } : void 0 });
314
317
  }
315
318
  async listMcpStatuses(directory) {
316
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
317
- url: "/mcp",
318
- ...directory ? { query: { directory } } : {}
319
- });
320
- return unwrapSdkData(await this.client.mcp.status(directory ? { directory } : void 0, SDK_OPTIONS));
319
+ return this.callScopedSdkMethod("mcp", "status", { parameters: directory ? { directory } : void 0 });
321
320
  }
322
321
  async getSessionStatuses() {
323
322
  return this.loadSessionStatuses();
324
323
  }
325
324
  async listProjects() {
326
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
327
- return unwrapSdkData(await this.client.project.list(void 0, SDK_OPTIONS));
325
+ return this.callScopedSdkMethod("project", "list", {});
328
326
  }
329
327
  async listSessions() {
330
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/session" });
331
- return unwrapSdkData(await this.client.session.list(void 0, SDK_OPTIONS));
328
+ return this.callScopedSdkMethod("session", "list", {});
332
329
  }
333
330
  async getCurrentProject() {
334
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project/current" });
335
- return unwrapSdkData(await this.client.project.current(void 0, SDK_OPTIONS));
331
+ return this.callScopedSdkMethod("project", "current", {});
336
332
  }
337
333
  async createSessionForDirectory(directory, title) {
338
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
339
- url: "/session",
340
- query: { directory },
341
- ...title ? { body: { title } } : {}
342
- });
343
- return unwrapSdkData(await this.client.session.create(title ? {
334
+ return this.callScopedSdkMethod("session", "create", { parameters: title ? {
344
335
  directory,
345
336
  title
346
- } : { directory }, SDK_OPTIONS));
337
+ } : { directory } });
347
338
  }
348
339
  async renameSession(sessionId, title) {
349
- if (hasRawSdkMethod(this.client, "patch")) return this.requestRaw("patch", {
350
- url: "/session/{sessionID}",
351
- path: { sessionID: sessionId },
352
- body: { title }
353
- });
354
- return unwrapSdkData(await this.client.session.update({
340
+ return this.callScopedSdkMethod("session", "update", { parameters: {
355
341
  sessionID: sessionId,
356
342
  title
357
- }, SDK_OPTIONS));
343
+ } });
358
344
  }
359
345
  async listAgents() {
360
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/agent" });
361
- return unwrapSdkData(await this.client.app.agents(void 0, SDK_OPTIONS));
346
+ return this.callScopedSdkMethod("app", "agents", {});
362
347
  }
363
348
  async listPendingPermissions(directory) {
364
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
365
- url: "/permission",
366
- ...directory ? { query: { directory } } : {}
367
- });
368
- return unwrapSdkData(await this.client.permission.list(directory ? { directory } : void 0, SDK_OPTIONS));
369
- }
370
- async replyToPermission(requestId, reply, message) {
371
- if (hasRawSdkMethod(this.client, "post")) return this.requestRaw("post", {
372
- url: "/permission/{requestID}/reply",
373
- path: { requestID: requestId },
374
- body: {
375
- reply,
376
- ...message?.trim() ? { message: message.trim() } : {}
377
- }
378
- });
379
- return unwrapSdkData(await this.client.permission.reply({
349
+ return this.callScopedSdkMethod("permission", "list", { parameters: directory ? { directory } : void 0 });
350
+ }
351
+ async replyToPermission(requestId, reply, message, _sessionId) {
352
+ return this.callScopedSdkMethod("permission", "reply", { parameters: {
380
353
  requestID: requestId,
381
354
  reply,
382
355
  ...message?.trim() ? { message: message.trim() } : {}
383
- }, SDK_OPTIONS));
356
+ } });
384
357
  }
385
358
  async listModels() {
386
359
  const now = Date.now();
@@ -405,9 +378,10 @@ var OpenCodeClient = class {
405
378
  url: file.url
406
379
  }))];
407
380
  if (parts.length === 0) throw new Error("Prompt requires text or file attachments.");
408
- const knownMessageIds = await this.captureKnownMessageIds(input.sessionId);
409
- const initialData = await this.sendPromptRequest(input, parts);
410
- 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), {
411
385
  emptyResponseText: EMPTY_RESPONSE_TEXT,
412
386
  finishedAt: Date.now(),
413
387
  startedAt,
@@ -416,7 +390,7 @@ var OpenCodeClient = class {
416
390
  }
417
391
  async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
418
392
  const structured = input.structured ?? false;
419
- if (data && !shouldPollPromptMessage(data, structured)) return data;
393
+ if (data && shouldReturnPromptResponseImmediately(data, structured)) return data;
420
394
  const messageId = data ? extractMessageId(data.info) : null;
421
395
  const candidateOptions = {
422
396
  initialMessageId: messageId,
@@ -426,145 +400,162 @@ var OpenCodeClient = class {
426
400
  structured
427
401
  };
428
402
  let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
429
- const deadlineAt = Date.now() + this.promptRequestTimeouts.totalPollMs;
403
+ let lastProgressAt = Date.now();
404
+ let lastStatus = null;
405
+ const deadlineAt = startedAt + this.promptTimeoutPolicy.waitTimeoutMs;
430
406
  let idleStatusSeen = false;
431
407
  let attempt = 0;
432
408
  while (true) {
409
+ throwIfAborted(input.signal);
410
+ const remainingWaitMs = deadlineAt - Date.now();
411
+ const remainingInactivityMs = this.promptTimeoutPolicy.recoveryInactivityTimeoutMs - (Date.now() - lastProgressAt);
412
+ if (remainingWaitMs <= 0 || remainingInactivityMs <= 0) break;
433
413
  const delayMs = getPromptMessagePollDelayMs(attempt);
434
414
  attempt += 1;
435
415
  if (delayMs > 0) {
436
- const remainingMs = deadlineAt - Date.now();
416
+ const remainingMs = Math.min(remainingWaitMs, remainingInactivityMs);
437
417
  if (remainingMs <= 0) break;
438
- await delay(Math.min(delayMs, remainingMs));
418
+ await delay(Math.min(delayMs, remainingMs), input.signal);
439
419
  }
440
420
  if (messageId) {
441
- const next = await this.fetchPromptMessage(input.sessionId, messageId);
421
+ const next = await this.fetchPromptMessage(input.sessionId, messageId, input.signal);
442
422
  if (next) {
443
423
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
444
- if (nextCandidate) bestCandidate = nextCandidate;
445
- if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
424
+ if (nextCandidate) {
425
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
426
+ lastProgressAt = Date.now();
427
+ idleStatusSeen = false;
428
+ }
429
+ bestCandidate = nextCandidate;
430
+ }
431
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
446
432
  }
447
433
  }
448
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
434
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
449
435
  if (latest) {
450
436
  const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
451
- if (nextCandidate) bestCandidate = nextCandidate;
452
- if (bestCandidate && !shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
437
+ if (nextCandidate) {
438
+ if (didPromptResponseAdvance(bestCandidate, nextCandidate, structured)) {
439
+ lastProgressAt = Date.now();
440
+ idleStatusSeen = false;
441
+ }
442
+ bestCandidate = nextCandidate;
443
+ }
444
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate, structured)) return bestCandidate;
453
445
  }
454
- if ((await this.fetchPromptSessionStatus(input.sessionId))?.type === "idle") {
446
+ const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
447
+ lastStatus = status;
448
+ if (status?.type === "busy" || status?.type === "retry") {
449
+ lastProgressAt = Date.now();
450
+ idleStatusSeen = false;
451
+ } else if (status?.type === "idle") {
455
452
  if (idleStatusSeen) break;
456
453
  idleStatusSeen = true;
457
454
  }
455
+ if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate, structured) && status?.type !== "busy" && status?.type !== "retry") break;
458
456
  if (Date.now() >= deadlineAt) break;
459
457
  }
460
- const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
458
+ const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
461
459
  const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
462
- 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)) {
463
+ const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
464
+ const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
463
465
  const error = createOpenCodePromptTimeoutError({
464
466
  sessionId: input.sessionId,
465
467
  stage: "final-scan",
466
- timeoutMs: this.promptRequestTimeouts.totalPollMs,
468
+ timeoutMs,
467
469
  messageId: messageId ?? void 0
468
470
  });
469
- this.logPromptRequestFailure(error, {
471
+ this.logPromptRequest("warn", {
472
+ lastProgressAt,
473
+ messageId: messageId ?? void 0,
470
474
  sessionId: input.sessionId,
471
475
  stage: "final-scan",
472
- timeoutMs: this.promptRequestTimeouts.totalPollMs,
473
- messageId
474
- });
476
+ timeoutMs,
477
+ timeoutReason
478
+ }, "OpenCode prompt recovery timed out");
475
479
  throw error;
476
480
  }
477
- return resolved;
481
+ return requestScopedResolved;
478
482
  }
479
- async fetchPromptMessage(sessionId, messageId) {
483
+ async fetchPromptMessage(sessionId, messageId, signal) {
480
484
  try {
481
485
  return await this.runPromptRequestWithTimeout({
482
486
  sessionId,
483
487
  stage: "poll-message",
484
- timeoutMs: this.promptRequestTimeouts.pollRequestMs,
488
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
485
489
  messageId
486
- }, async (signal) => {
487
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", {
488
- url: "/session/{sessionID}/message/{messageID}",
489
- path: {
490
+ }, async (requestSignal) => {
491
+ return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
492
+ parameters: {
490
493
  sessionID: sessionId,
491
494
  messageID: messageId
492
495
  },
493
- signal
496
+ signal: requestSignal
494
497
  }));
495
- if (typeof this.client.session.message !== "function") return null;
496
- return normalizePromptResponse(unwrapSdkData(await this.client.session.message({
497
- sessionID: sessionId,
498
- messageID: messageId
499
- }, {
500
- ...SDK_OPTIONS,
501
- signal
502
- })));
503
- });
498
+ }, signal);
504
499
  } catch (error) {
500
+ if (isPromptRequestAbort(error)) throw error;
505
501
  this.logPromptRequestFailure(error, {
506
502
  sessionId,
507
503
  stage: "poll-message",
508
- timeoutMs: this.promptRequestTimeouts.pollRequestMs,
504
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs,
509
505
  messageId
510
506
  });
511
507
  return null;
512
508
  }
513
509
  }
514
- async captureKnownMessageIds(sessionId) {
515
- 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);
516
512
  if (!messages) return /* @__PURE__ */ new Set();
517
513
  return new Set(messages.map((message) => extractMessageId(message.info)).filter((id) => typeof id === "string" && id.length > 0));
518
514
  }
519
- async fetchRecentPromptMessages(sessionId, stage) {
515
+ async fetchRecentPromptMessages(sessionId, stage, signal) {
520
516
  try {
521
517
  return await this.runPromptRequestWithTimeout({
522
518
  sessionId,
523
519
  stage,
524
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
525
- }, async (signal) => {
526
- if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", {
527
- url: "/session/{sessionID}/message",
528
- path: { sessionID: sessionId },
529
- query: { limit: PROMPT_MESSAGE_POLL_LIMIT },
530
- signal
520
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
521
+ }, async (requestSignal) => {
522
+ return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
523
+ parameters: {
524
+ sessionID: sessionId,
525
+ limit: PROMPT_MESSAGE_POLL_LIMIT
526
+ },
527
+ signal: requestSignal
531
528
  }));
532
- if (typeof this.client.session.messages !== "function") return null;
533
- return normalizePromptResponses(unwrapSdkData(await this.client.session.messages({
534
- sessionID: sessionId,
535
- limit: PROMPT_MESSAGE_POLL_LIMIT
536
- }, {
537
- ...SDK_OPTIONS,
538
- signal
539
- })));
540
- });
529
+ }, signal);
541
530
  } catch (error) {
531
+ if (isPromptRequestAbort(error)) throw error;
542
532
  this.logPromptRequestFailure(error, {
543
533
  sessionId,
544
534
  stage,
545
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
535
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
546
536
  });
547
537
  return null;
548
538
  }
549
539
  }
550
- async fetchPromptSessionStatus(sessionId) {
540
+ async fetchPromptSessionStatus(sessionId, signal) {
551
541
  try {
552
542
  return (await this.runPromptRequestWithTimeout({
553
543
  sessionId,
554
544
  stage: "poll-status",
555
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
556
- }, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
545
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
546
+ }, async (requestSignal) => this.loadSessionStatuses(requestSignal), signal))[sessionId] ?? null;
557
547
  } catch (error) {
548
+ if (isPromptRequestAbort(error)) throw error;
558
549
  this.logPromptRequestFailure(error, {
559
550
  sessionId,
560
551
  stage: "poll-status",
561
- timeoutMs: this.promptRequestTimeouts.pollRequestMs
552
+ timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
562
553
  });
563
554
  return null;
564
555
  }
565
556
  }
566
- async findLatestPromptResponse(sessionId, options, stage) {
567
- const messages = await this.fetchRecentPromptMessages(sessionId, stage);
557
+ async findLatestPromptResponse(sessionId, options, stage, signal) {
558
+ const messages = await this.fetchRecentPromptMessages(sessionId, stage, signal);
568
559
  if (!messages || messages.length === 0) return null;
569
560
  return selectPromptResponseCandidate(messages, options);
570
561
  }
@@ -580,12 +571,10 @@ var OpenCodeClient = class {
580
571
  return models;
581
572
  }
582
573
  async loadConfig() {
583
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config" });
584
- return unwrapSdkData(await this.client.config.get(void 0, SDK_OPTIONS));
574
+ return this.callScopedSdkMethod("config", "get", {});
585
575
  }
586
576
  async loadProviderCatalog() {
587
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/config/providers" });
588
- return unwrapSdkData(await this.client.config.providers(void 0, SDK_OPTIONS));
577
+ return this.callScopedSdkMethod("config", "providers", {});
589
578
  }
590
579
  async sendPromptRequest(input, parts) {
591
580
  const requestBody = {
@@ -604,62 +593,50 @@ var OpenCodeClient = class {
604
593
  await this.runPromptRequestWithTimeout({
605
594
  sessionId: input.sessionId,
606
595
  stage: "send-prompt",
607
- timeoutMs: this.promptRequestTimeouts.sendMs
596
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
608
597
  }, async (signal) => {
609
- await this.client.session.promptAsync(requestParameters, {
610
- ...SDK_OPTIONS,
598
+ await this.callScopedSdkMethod("session", "promptAsync", {
599
+ parameters: requestParameters,
611
600
  signal
612
601
  });
613
- });
614
- return null;
602
+ }, input.signal);
603
+ return;
615
604
  }
616
- return await this.runPromptRequestWithTimeout({
617
- sessionId: input.sessionId,
618
- stage: "send-prompt",
619
- timeoutMs: this.promptRequestTimeouts.sendMs
620
- }, async (signal) => {
621
- if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
622
- url: "/session/{sessionID}/message",
623
- path: { sessionID: input.sessionId },
624
- body: requestBody,
625
- signal
626
- }));
627
- return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt(requestParameters, {
628
- ...SDK_OPTIONS,
629
- signal
630
- })));
631
- });
632
605
  } catch (error) {
633
606
  this.logPromptRequestFailure(error, {
634
607
  sessionId: input.sessionId,
635
608
  stage: "send-prompt",
636
- timeoutMs: this.promptRequestTimeouts.sendMs
609
+ timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
637
610
  });
638
611
  throw error;
639
612
  }
640
- }
641
- async requestRaw(method, options) {
642
- const handler = getRawSdkClient(this.client)?.[method];
643
- if (typeof handler !== "function") throw new Error(`OpenCode SDK client does not expose a compatible raw ${method.toUpperCase()} method.`);
644
- return unwrapSdkData(await handler({
645
- ...SDK_OPTIONS,
646
- ...options
647
- }));
613
+ throw new Error("OpenCode SDK client does not expose session.promptAsync().");
648
614
  }
649
615
  async loadSessionStatuses(signal) {
650
- if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
651
- url: "/session/status",
652
- ...signal ? { signal } : {}
616
+ return this.callScopedSdkMethod("session", "status", {
617
+ signal,
618
+ parameters: void 0
653
619
  });
654
- return unwrapSdkData(await this.client.session.status(void 0, {
655
- ...SDK_OPTIONS,
656
- ...signal ? { signal } : {}
657
- }));
658
620
  }
659
- async runPromptRequestWithTimeout(input, operation) {
621
+ async runPromptRequestWithTimeout(input, operation, signal) {
660
622
  const startedAt = Date.now();
661
623
  const controller = new AbortController();
624
+ const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
662
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;
663
640
  const timeoutPromise = new Promise((_, reject) => {
664
641
  timeoutHandle = setTimeout(() => {
665
642
  reject(createOpenCodePromptTimeoutError({
@@ -673,14 +650,20 @@ var OpenCodeClient = class {
673
650
  }, input.timeoutMs);
674
651
  });
675
652
  try {
676
- return await Promise.race([operation(controller.signal), timeoutPromise]);
653
+ return await Promise.race([
654
+ operation(requestSignal),
655
+ timeoutPromise,
656
+ ...abortPromise ? [abortPromise] : []
657
+ ]);
677
658
  } finally {
678
659
  if (timeoutHandle !== null) clearTimeout(timeoutHandle);
660
+ removeAbortListener();
679
661
  }
680
662
  }
681
663
  logPromptRequestFailure(error, input) {
682
664
  if (error instanceof OpenCodePromptTimeoutError) {
683
665
  this.logPromptRequest("warn", {
666
+ endpointKind: resolvePromptEndpointKind(error.data.stage),
684
667
  elapsedMs: error.data.elapsedMs,
685
668
  messageId: error.data.messageId,
686
669
  sessionId: error.data.sessionId,
@@ -690,6 +673,7 @@ var OpenCodeClient = class {
690
673
  return;
691
674
  }
692
675
  this.logPromptRequest("warn", {
676
+ endpointKind: resolvePromptEndpointKind(input.stage),
693
677
  error,
694
678
  messageId: input.messageId ?? void 0,
695
679
  sessionId: input.sessionId,
@@ -708,8 +692,8 @@ var OpenCodeClient = class {
708
692
  }).catch(() => void 0);
709
693
  }
710
694
  };
711
- function createOpenCodeClientFromSdkClient(client, fetchFn = fetch) {
712
- return new OpenCodeClient(void 0, client, fetchFn);
695
+ function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
696
+ return new OpenCodeClient(void 0, client, fetchFn, promptTimeoutPolicy);
713
697
  }
714
698
  function buildSelectableModels(config, providers, providerAvailability = /* @__PURE__ */ new Map()) {
715
699
  const configuredProviders = config.provider ?? {};
@@ -816,7 +800,14 @@ function shouldPollPromptMessage(data, structured) {
816
800
  const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
817
801
  const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
818
802
  const hasAssistantError = !!assistantInfo?.error;
819
- return !hasText && !bodyMd && !hasAssistantError;
803
+ const isCompleted = isAssistantMessageCompleted(assistantInfo);
804
+ return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
805
+ }
806
+ function shouldReturnPromptResponseImmediately(data, structured) {
807
+ return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
808
+ }
809
+ function isPromptResponseUsable(data, structured) {
810
+ return !shouldPollPromptMessage(data, structured) && !isCompletedEmptyPromptResponse(data, structured);
820
811
  }
821
812
  function normalizePromptResponse(response) {
822
813
  return {
@@ -875,8 +866,26 @@ function extractMessageId(message) {
875
866
  if (!isPlainRecord(message)) return null;
876
867
  return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
877
868
  }
878
- function delay(ms) {
879
- 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
+ });
886
+ }
887
+ function didPromptResponseAdvance(previous, next, structured) {
888
+ return getPromptResponseProgressSignature(previous, structured) !== getPromptResponseProgressSignature(next, structured);
880
889
  }
881
890
  function createOpenCodePromptTimeoutError(input) {
882
891
  return new OpenCodePromptTimeoutError({
@@ -885,6 +894,16 @@ function createOpenCodePromptTimeoutError(input) {
885
894
  message: input.message ?? "The OpenCode host did not finish this request in time."
886
895
  });
887
896
  }
897
+ function resolvePromptEndpointKind(stage) {
898
+ switch (stage) {
899
+ case "capture-known-messages":
900
+ case "poll-messages":
901
+ case "final-scan": return "messages";
902
+ case "poll-message": return "message";
903
+ case "poll-status": return "status";
904
+ default: return "prompt";
905
+ }
906
+ }
888
907
  function getPromptMessagePollDelayMs(attempt) {
889
908
  return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
890
909
  }
@@ -940,11 +959,12 @@ function unwrapSdkData(response) {
940
959
  if (response && typeof response === "object" && "data" in response) return response.data;
941
960
  return response;
942
961
  }
943
- function getRawSdkClient(client) {
944
- return client.client ?? client._client ?? null;
945
- }
946
- function hasRawSdkMethod(client, method) {
947
- return typeof getRawSdkClient(client)?.[method] === "function";
962
+ function resolvePromptTimeoutPolicy(input) {
963
+ return {
964
+ pollRequestTimeoutMs: input.pollRequestTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.pollRequestTimeoutMs,
965
+ recoveryInactivityTimeoutMs: input.recoveryInactivityTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.recoveryInactivityTimeoutMs,
966
+ waitTimeoutMs: input.waitTimeoutMs ?? DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY.waitTimeoutMs
967
+ };
948
968
  }
949
969
  function normalizeAssistantError(value) {
950
970
  if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
@@ -954,6 +974,15 @@ function normalizeAssistantError(value) {
954
974
  ...isPlainRecord(value.data) ? { data: value.data } : {}
955
975
  };
956
976
  }
977
+ function isAssistantMessageCompleted(message) {
978
+ return !!message?.error || typeof message?.time?.completed === "number" || typeof message?.finish === "string" && message.finish.trim().length > 0;
979
+ }
980
+ function isCompletedEmptyPromptResponse(data, structured) {
981
+ const assistantInfo = toAssistantMessage(data.info);
982
+ const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
983
+ const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
984
+ return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
985
+ }
957
986
  function extractStructuredPayload(message) {
958
987
  if (!isPlainRecord(message)) return null;
959
988
  if ("structured" in message && message.structured !== void 0) return message.structured;
@@ -968,7 +997,7 @@ function selectPromptResponseCandidate(candidates, options) {
968
997
  function comparePromptResponseCandidates(left, right, options) {
969
998
  const leftRank = getPromptResponseCandidateRank(left, options);
970
999
  const rightRank = getPromptResponseCandidateRank(right, options);
971
- 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;
972
1001
  }
973
1002
  function getPromptResponseCandidateRank(message, options) {
974
1003
  const assistant = toAssistantMessage(message.info);
@@ -978,25 +1007,57 @@ function getPromptResponseCandidateRank(message, options) {
978
1007
  createdAt,
979
1008
  isInitial: !!id && id === options.initialMessageId,
980
1009
  isNewSinceRequestStart: isPromptResponseNewSinceRequestStart(id, createdAt, options.knownMessageIds, options.requestStartedAt),
981
- isUsable: !shouldPollPromptMessage(message, options.structured),
1010
+ isUsable: isPromptResponseUsable(message, options.structured),
982
1011
  sharesParent: !!assistant?.parentID && assistant.parentID === options.initialParentId
983
1012
  };
984
1013
  }
985
1014
  function resolvePromptCandidateStartTime(startedAt, initialMessage) {
986
- if (!initialMessage) return null;
1015
+ if (!initialMessage) return startedAt;
987
1016
  const initialCreatedAt = coerceFiniteNumber(toAssistantMessage(initialMessage.info)?.time?.created);
988
1017
  if (initialCreatedAt === null) return startedAt;
989
1018
  return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
990
1019
  }
1020
+ function getPromptResponseProgressSignature(response, structured) {
1021
+ if (!response) return "null";
1022
+ const assistant = toAssistantMessage(response.info);
1023
+ const responseParts = Array.isArray(response.parts) ? response.parts : [];
1024
+ return JSON.stringify({
1025
+ assistantError: assistant?.error?.name ?? null,
1026
+ bodyMd: structured ? extractStructuredMarkdown(extractStructuredPayload(assistant)) : null,
1027
+ completedAt: assistant?.time?.completed ?? null,
1028
+ finish: assistant?.finish ?? null,
1029
+ id: assistant?.id ?? null,
1030
+ partCount: responseParts.length,
1031
+ text: extractTextFromParts(responseParts)
1032
+ });
1033
+ }
991
1034
  function isPromptResponseNewSinceRequestStart(messageId, createdAt, knownMessageIds, requestStartedAt) {
992
1035
  if (!messageId || knownMessageIds.has(messageId)) return false;
993
1036
  if (requestStartedAt === null) return true;
994
1037
  return createdAt >= requestStartedAt;
995
1038
  }
1039
+ function isPromptResponseForCurrentRequest(response, options) {
1040
+ const rank = getPromptResponseCandidateRank(response, options);
1041
+ return rank.isInitial || rank.sharesParent || rank.isNewSinceRequestStart;
1042
+ }
996
1043
  function areComparablePromptTimestamps(left, right) {
997
1044
  const epochThresholdMs = 0xe8d4a51000;
998
1045
  return left >= epochThresholdMs && right >= epochThresholdMs;
999
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
+ }
1000
1061
  function isPlainRecord(value) {
1001
1062
  return value !== null && typeof value === "object" && !Array.isArray(value);
1002
1063
  }
@@ -1164,85 +1225,119 @@ function extractErrorMessage(error) {
1164
1225
  //#endregion
1165
1226
  //#region src/services/session-activity/foreground-session-tracker.ts
1166
1227
  var ForegroundSessionTracker = class {
1167
- chatStacks = /* @__PURE__ */ new Map();
1168
- counts = /* @__PURE__ */ new Map();
1228
+ requests = /* @__PURE__ */ new Map();
1169
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
+ }
1170
1254
  begin(chatId, sessionId) {
1171
- const currentCount = this.counts.get(sessionId) ?? 0;
1172
- this.counts.set(sessionId, currentCount + 1);
1173
- this.incrementChat(chatId, sessionId);
1255
+ const lease = this.acquire(chatId);
1256
+ if (!lease) return () => void 0;
1257
+ lease.attachSession(sessionId);
1174
1258
  return () => {
1175
- this.decrement(chatId, sessionId);
1259
+ lease.dispose();
1176
1260
  };
1177
1261
  }
1178
1262
  clear(sessionId) {
1179
- const wasForeground = this.counts.has(sessionId);
1180
- const chatCounts = this.sessionChats.get(sessionId);
1181
- this.counts.delete(sessionId);
1182
- this.sessionChats.delete(sessionId);
1183
- for (const chatId of chatCounts?.keys() ?? []) {
1184
- const stack = this.chatStacks.get(chatId);
1185
- if (!stack) continue;
1186
- const nextStack = stack.filter((trackedSessionId) => trackedSessionId !== sessionId);
1187
- if (nextStack.length === 0) {
1188
- this.chatStacks.delete(chatId);
1189
- continue;
1190
- }
1191
- 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;
1192
1268
  }
1193
- 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;
1194
1278
  }
1195
1279
  getActiveSessionId(chatId) {
1196
- 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);
1197
1284
  }
1198
1285
  isForeground(sessionId) {
1199
- return this.counts.has(sessionId);
1286
+ return this.sessionChats.has(sessionId);
1200
1287
  }
1201
1288
  listChatIds(sessionId) {
1202
- return [...this.sessionChats.get(sessionId)?.keys() ?? []];
1203
- }
1204
- decrement(chatId, sessionId) {
1205
- const currentCount = this.counts.get(sessionId);
1206
- if (!currentCount || currentCount <= 1) this.counts.delete(sessionId);
1207
- else this.counts.set(sessionId, currentCount - 1);
1208
- this.decrementChat(chatId, sessionId);
1209
- }
1210
- incrementChat(chatId, sessionId) {
1211
- const stack = this.chatStacks.get(chatId) ?? [];
1212
- stack.push(sessionId);
1213
- this.chatStacks.set(chatId, stack);
1214
- const chatCounts = this.sessionChats.get(sessionId) ?? /* @__PURE__ */ new Map();
1215
- const currentCount = chatCounts.get(chatId) ?? 0;
1216
- chatCounts.set(chatId, currentCount + 1);
1217
- this.sessionChats.set(sessionId, chatCounts);
1218
- }
1219
- decrementChat(chatId, sessionId) {
1220
- const stack = this.chatStacks.get(chatId);
1221
- if (stack) {
1222
- const index = stack.lastIndexOf(sessionId);
1223
- if (index >= 0) stack.splice(index, 1);
1224
- if (stack.length === 0) this.chatStacks.delete(chatId);
1225
- 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;
1226
1315
  }
1227
- const chatCounts = this.sessionChats.get(sessionId);
1228
- if (!chatCounts) return;
1229
- const currentCount = chatCounts.get(chatId) ?? 0;
1230
- if (currentCount <= 1) chatCounts.delete(chatId);
1231
- else chatCounts.set(chatId, currentCount - 1);
1232
- if (chatCounts.size === 0) this.sessionChats.delete(sessionId);
1233
- else this.sessionChats.set(sessionId, chatCounts);
1316
+ this.sessionChats.set(sessionId, chatIds);
1234
1317
  }
1235
1318
  };
1236
1319
  var NOOP_FOREGROUND_SESSION_TRACKER = {
1320
+ acquire() {
1321
+ return null;
1322
+ },
1323
+ abort() {
1324
+ return false;
1325
+ },
1237
1326
  begin() {
1238
1327
  return () => void 0;
1239
1328
  },
1240
1329
  clear() {
1241
1330
  return false;
1242
1331
  },
1332
+ fail() {
1333
+ return false;
1334
+ },
1243
1335
  getActiveSessionId() {
1244
1336
  return null;
1245
1337
  },
1338
+ hasActiveRequest() {
1339
+ return false;
1340
+ },
1246
1341
  isForeground() {
1247
1342
  return false;
1248
1343
  },
@@ -1259,23 +1354,40 @@ var AbortPromptUseCase = class {
1259
1354
  this.foregroundSessionTracker = foregroundSessionTracker;
1260
1355
  }
1261
1356
  async execute(input) {
1357
+ const hasForegroundRequest = this.foregroundSessionTracker.hasActiveRequest(input.chatId);
1262
1358
  const activeSessionId = this.foregroundSessionTracker.getActiveSessionId(input.chatId);
1263
1359
  const binding = activeSessionId ? null : await this.sessionRepo.getByChatId(input.chatId);
1264
1360
  const sessionId = activeSessionId ?? binding?.sessionId ?? null;
1265
- if (!sessionId) return {
1361
+ if (!hasForegroundRequest && !sessionId) return {
1266
1362
  sessionId: null,
1267
1363
  status: "no_session",
1268
1364
  sessionStatus: null
1269
1365
  };
1270
- 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
+ }
1271
1377
  if (!sessionStatus || sessionStatus.type === "idle") return {
1272
1378
  sessionId,
1273
1379
  status: "not_running",
1274
1380
  sessionStatus
1275
1381
  };
1382
+ if (!sessionId) return {
1383
+ sessionId: null,
1384
+ status: "not_running",
1385
+ sessionStatus
1386
+ };
1387
+ const runningSessionId = sessionId;
1276
1388
  return {
1277
- sessionId,
1278
- status: await this.opencodeClient.abortSession(sessionId) ? "aborted" : "not_running",
1389
+ sessionId: runningSessionId,
1390
+ status: await this.opencodeClient.abortSession(runningSessionId) ? "aborted" : "not_running",
1279
1391
  sessionStatus
1280
1392
  };
1281
1393
  }
@@ -1717,11 +1829,10 @@ var RenameSessionUseCase = class {
1717
1829
  //#endregion
1718
1830
  //#region src/use-cases/send-prompt.usecase.ts
1719
1831
  var SendPromptUseCase = class {
1720
- constructor(sessionRepo, opencodeClient, logger, foregroundSessionTracker = NOOP_FOREGROUND_SESSION_TRACKER) {
1832
+ constructor(sessionRepo, opencodeClient, logger) {
1721
1833
  this.sessionRepo = sessionRepo;
1722
1834
  this.opencodeClient = opencodeClient;
1723
1835
  this.logger = logger;
1724
- this.foregroundSessionTracker = foregroundSessionTracker;
1725
1836
  }
1726
1837
  async execute(input) {
1727
1838
  const files = input.files ?? [];
@@ -1775,7 +1886,7 @@ var SendPromptUseCase = class {
1775
1886
  }
1776
1887
  const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
1777
1888
  const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
1778
- const endForegroundSession = this.foregroundSessionTracker.begin(input.chatId, executionSessionId);
1889
+ input.onExecutionSession?.(executionSessionId);
1779
1890
  let result;
1780
1891
  try {
1781
1892
  result = await this.opencodeClient.promptSession({
@@ -1785,10 +1896,10 @@ var SendPromptUseCase = class {
1785
1896
  ...selectedAgent ? { agent: selectedAgent.name } : {},
1786
1897
  structured: true,
1787
1898
  ...model ? { model } : {},
1899
+ ...input.signal ? { signal: input.signal } : {},
1788
1900
  ...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
1789
1901
  });
1790
1902
  } finally {
1791
- endForegroundSession();
1792
1903
  if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
1793
1904
  }
1794
1905
  await this.sessionRepo.touch(input.chatId);
@@ -2025,7 +2136,11 @@ function resolveExtension(mimeType) {
2025
2136
  //#region src/app/container.ts
2026
2137
  function createAppContainer(config, client) {
2027
2138
  const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
2028
- return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
2139
+ return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
2140
+ waitTimeoutMs: config.promptWaitTimeoutMs,
2141
+ pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
2142
+ recoveryInactivityTimeoutMs: config.promptRecoveryInactivityTimeoutMs
2143
+ }), logger);
2029
2144
  }
2030
2145
  function createContainer(config, opencodeClient, logger) {
2031
2146
  const stateStore = new JsonStateStore({
@@ -2052,7 +2167,7 @@ function createContainer(config, opencodeClient, logger) {
2052
2167
  const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
2053
2168
  const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
2054
2169
  const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
2055
- const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
2170
+ const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger);
2056
2171
  const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, logger);
2057
2172
  const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, logger);
2058
2173
  const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, logger);
@@ -2159,21 +2274,25 @@ function escapeMarkdownV2(value) {
2159
2274
  async function handleTelegramBotPluginEvent(runtime, event) {
2160
2275
  switch (event.type) {
2161
2276
  case "permission.asked":
2162
- await handlePermissionAsked(runtime, event.properties);
2277
+ await handlePermissionAsked(runtime, event);
2163
2278
  return;
2164
2279
  case "permission.replied":
2165
- await handlePermissionReplied(runtime, event.properties.requestID, event.properties.reply);
2280
+ await handlePermissionReplied(runtime, event);
2166
2281
  return;
2167
2282
  case "session.error":
2168
- await handleSessionError(runtime, event.properties.sessionID, event.properties.error);
2283
+ await handleSessionError(runtime, event);
2169
2284
  return;
2170
2285
  case "session.idle":
2171
- await handleSessionIdle(runtime, event.properties.sessionID);
2286
+ await handleSessionIdle(runtime, event);
2287
+ return;
2288
+ case "session.status":
2289
+ await handleSessionStatus(runtime, event);
2172
2290
  return;
2173
2291
  default: return;
2174
2292
  }
2175
2293
  }
2176
- async function handlePermissionAsked(runtime, request) {
2294
+ async function handlePermissionAsked(runtime, event) {
2295
+ const request = event.properties;
2177
2296
  const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
2178
2297
  const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
2179
2298
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
@@ -2202,7 +2321,9 @@ async function handlePermissionAsked(runtime, request) {
2202
2321
  }
2203
2322
  }
2204
2323
  }
2205
- async function handlePermissionReplied(runtime, requestId, reply) {
2324
+ async function handlePermissionReplied(runtime, event) {
2325
+ const requestId = event.properties.requestID;
2326
+ const reply = event.properties.reply;
2206
2327
  const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(requestId);
2207
2328
  await Promise.all(approvals.map(async (approval) => {
2208
2329
  try {
@@ -2217,27 +2338,37 @@ async function handlePermissionReplied(runtime, requestId, reply) {
2217
2338
  await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
2218
2339
  }));
2219
2340
  }
2220
- async function handleSessionError(runtime, sessionId, error) {
2341
+ async function handleSessionError(runtime, event) {
2342
+ const sessionId = event.properties.sessionID;
2343
+ const error = event.properties.error;
2221
2344
  if (!sessionId) {
2222
2345
  runtime.container.logger.error({ error }, "session error received without a session id");
2223
2346
  return;
2224
2347
  }
2225
- if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2348
+ if (runtime.container.foregroundSessionTracker.fail(sessionId, error ?? /* @__PURE__ */ new Error("Unknown session error."))) {
2226
2349
  runtime.container.logger.warn({
2227
2350
  error,
2228
2351
  sessionId
2229
2352
  }, "session error suppressed for foreground Telegram session");
2230
2353
  return;
2231
2354
  }
2232
- 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."}`);
2233
2356
  }
2234
- async function handleSessionIdle(runtime, sessionId) {
2357
+ async function handleSessionIdle(runtime, event) {
2358
+ const sessionId = event.properties.sessionID;
2235
2359
  if (runtime.container.foregroundSessionTracker.clear(sessionId)) {
2236
2360
  runtime.container.logger.info({ sessionId }, "session idle notification suppressed for foreground Telegram session");
2237
2361
  return;
2238
2362
  }
2239
2363
  await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
2240
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
+ }
2241
2372
  async function notifyBoundChats(runtime, sessionId, text) {
2242
2373
  const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
2243
2374
  const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
@@ -4478,8 +4609,8 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
4478
4609
  const parsed = parsePermissionApprovalCallbackData(data);
4479
4610
  if (!parsed) return;
4480
4611
  try {
4481
- await dependencies.opencodeClient.replyToPermission(parsed.requestId, parsed.reply);
4482
4612
  const approval = (await dependencies.permissionApprovalRepo.listByRequestId(parsed.requestId)).find((item) => item.chatId === ctx.chat?.id);
4613
+ await dependencies.opencodeClient.replyToPermission(parsed.requestId, parsed.reply, void 0, approval?.sessionId);
4483
4614
  if (approval) await dependencies.permissionApprovalRepo.set({
4484
4615
  ...approval,
4485
4616
  status: parsed.reply,
@@ -4525,23 +4656,26 @@ function parseSessionActionTarget(data, prefix) {
4525
4656
  }
4526
4657
  //#endregion
4527
4658
  //#region src/bot/handlers/prompt.handler.ts
4528
- var activePromptChats = /* @__PURE__ */ new Set();
4529
4659
  async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4530
4660
  const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
4531
- if (activePromptChats.has(ctx.chat.id)) {
4661
+ const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
4662
+ if (!foregroundRequest) {
4532
4663
  await ctx.reply(copy.status.alreadyProcessing);
4533
4664
  return;
4534
4665
  }
4535
4666
  let processingMessage = null;
4536
4667
  let sentTerminalReply = false;
4537
4668
  try {
4538
- activePromptChats.add(ctx.chat.id);
4539
4669
  processingMessage = await ctx.reply(copy.status.processing);
4540
4670
  const promptInput = await resolvePrompt();
4541
4671
  const telegramReply = buildTelegramPromptReply(normalizePromptReplyForDisplay((await dependencies.sendPromptUseCase.execute({
4542
4672
  chatId: ctx.chat.id,
4543
- text: promptInput.text,
4544
- files: promptInput.files
4673
+ files: promptInput.files,
4674
+ onExecutionSession: (sessionId) => {
4675
+ foregroundRequest.attachSession(sessionId);
4676
+ },
4677
+ signal: foregroundRequest.signal,
4678
+ text: promptInput.text
4545
4679
  })).assistantReply, copy, dependencies), copy);
4546
4680
  try {
4547
4681
  await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
@@ -4555,7 +4689,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
4555
4689
  await ctx.reply(presentError(error, copy));
4556
4690
  sentTerminalReply = true;
4557
4691
  } finally {
4558
- activePromptChats.delete(ctx.chat.id);
4692
+ foregroundRequest.dispose();
4559
4693
  if (processingMessage && sentTerminalReply) try {
4560
4694
  await ctx.api.deleteMessage(ctx.chat.id, processingMessage.message_id);
4561
4695
  } catch (error) {