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