opencode-plugin-teleprompt 0.1.12 → 0.2.1
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.md +65 -64
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/opencode/binding.js +2 -6
- package/dist/opencode/binding.js.map +1 -1
- package/dist/opencode/events.d.ts +1 -0
- package/dist/opencode/events.js +13 -3
- package/dist/opencode/events.js.map +1 -1
- package/dist/opencode/permissions.js +12 -14
- package/dist/opencode/permissions.js.map +1 -1
- package/dist/opencode/submit.d.ts +1 -1
- package/dist/opencode/submit.js +35 -15
- package/dist/opencode/submit.js.map +1 -1
- package/dist/runtime/controller.d.ts +3 -0
- package/dist/runtime/controller.js +151 -85
- package/dist/runtime/controller.js.map +1 -1
- package/dist/telegram/api.js +13 -5
- package/dist/telegram/api.js.map +1 -1
- package/dist/telegram/delay.d.ts +1 -0
- package/dist/telegram/delay.js +18 -0
- package/dist/telegram/delay.js.map +1 -0
- package/dist/telegram/parser.js +72 -185
- package/dist/telegram/parser.js.map +1 -1
- package/dist/telegram/poller.js +11 -1
- package/dist/telegram/poller.js.map +1 -1
- package/dist/tui-types.d.ts +13 -0
- package/dist/tui.js +80 -26
- package/dist/tui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
3
4
|
import { getCurrentOrCreateSessionID } from "../opencode/binding.js";
|
|
4
5
|
import { replyPermission, toPendingPermission, formatPermissionRequestMessage } from "../opencode/permissions.js";
|
|
5
6
|
import { createTelegramUserMessageID, submitPrompt } from "../opencode/submit.js";
|
|
@@ -119,6 +120,7 @@ function resolvePresetModel(preset, models) {
|
|
|
119
120
|
export class BridgeController {
|
|
120
121
|
api;
|
|
121
122
|
config;
|
|
123
|
+
storePath;
|
|
122
124
|
deps;
|
|
123
125
|
instanceID;
|
|
124
126
|
client;
|
|
@@ -136,9 +138,11 @@ export class BridgeController {
|
|
|
136
138
|
lastEscAt = 0;
|
|
137
139
|
sessionCredentials;
|
|
138
140
|
activePromptCheckInFlight = false;
|
|
141
|
+
completionInFlight = false;
|
|
139
142
|
constructor(api, config, storePath, deps) {
|
|
140
143
|
this.api = api;
|
|
141
144
|
this.config = config;
|
|
145
|
+
this.storePath = storePath;
|
|
142
146
|
this.deps = { ...DEFAULT_DEPS, ...deps };
|
|
143
147
|
this.instanceID = this.deps.randomID();
|
|
144
148
|
this.client = api.client;
|
|
@@ -146,6 +150,16 @@ export class BridgeController {
|
|
|
146
150
|
this.store = new BridgeStore(storePath, config.channelID ?? "");
|
|
147
151
|
this.lease = new LeaseManager(this.instanceID, this.deps.now, config.leaseTtlMs);
|
|
148
152
|
}
|
|
153
|
+
log(message) {
|
|
154
|
+
try {
|
|
155
|
+
const logPath = this.storePath.replace(/\.json$/, ".log");
|
|
156
|
+
const timestamp = new Date().toISOString();
|
|
157
|
+
appendFileSync(logPath, `[${timestamp}] ${message}\n`, "utf8");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
149
163
|
async init() {
|
|
150
164
|
this.data = await this.store.load();
|
|
151
165
|
}
|
|
@@ -156,7 +170,17 @@ export class BridgeController {
|
|
|
156
170
|
this.config.botToken = resolvedCredentials.botToken;
|
|
157
171
|
this.config.channelID = resolvedCredentials.channelID;
|
|
158
172
|
this.store.setChannelID(resolvedCredentials.channelID);
|
|
173
|
+
this.log(`bindCurrent: api.route.current = ${JSON.stringify(this.api.route.current)}`);
|
|
174
|
+
try {
|
|
175
|
+
const listResponse = await this.client.session.list({ directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
176
|
+
const sessions = Array.isArray(listResponse) ? listResponse : (listResponse?.sessions || listResponse?.items || listResponse?.data || []);
|
|
177
|
+
this.log(`bindCurrent: available sessions: ${JSON.stringify(sessions.map((s) => ({ id: s.id, title: s.title })))}`);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
this.log(`bindCurrent: failed to list sessions: ${String(e)}`);
|
|
181
|
+
}
|
|
159
182
|
const sessionID = await getCurrentOrCreateSessionID(this.api, this.client);
|
|
183
|
+
this.log(`bindCurrent: getCurrentOrCreateSessionID returned ${sessionID}`);
|
|
160
184
|
const state = await this.syncState();
|
|
161
185
|
const claimed = this.lease.claim(state);
|
|
162
186
|
const next = this.resetBoundSessionState(claimed, sessionID, resolvedCredentials.channelID);
|
|
@@ -222,10 +246,12 @@ export class BridgeController {
|
|
|
222
246
|
return lines.join("\n");
|
|
223
247
|
}
|
|
224
248
|
async handleTelegramCommand(command) {
|
|
249
|
+
this.log(`handleTelegramCommand: kind=${command.command.kind}, updateID=${command.updateID}, messageID=${command.messageID}`);
|
|
225
250
|
const state = await this.syncState();
|
|
226
251
|
const isOwner = this.lease.isOwner(state);
|
|
227
252
|
const alwaysAllowed = new Set(["status", "who", "health", "reclaim", "version"]);
|
|
228
253
|
if (!isOwner && !alwaysAllowed.has(command.command.kind)) {
|
|
254
|
+
this.log(`handleTelegramCommand: rejected because not owner (isOwner=${isOwner}, kind=${command.command.kind})`);
|
|
229
255
|
await this.telegram.sendMessage(this.config.channelID, "Bridge is currently owned by another OpenCode instance. Use /tp:reclaim first.", { replyToMessageID: command.messageID });
|
|
230
256
|
return;
|
|
231
257
|
}
|
|
@@ -308,9 +334,11 @@ export class BridgeController {
|
|
|
308
334
|
return;
|
|
309
335
|
}
|
|
310
336
|
if (state.bound.status !== "online" || !state.bound.sessionID) {
|
|
337
|
+
this.log(`handleTelegramCommand: rejected because offline or missing session ID (status=${state.bound.status}, sessionID=${state.bound.sessionID})`);
|
|
311
338
|
await this.telegram.sendMessage(this.config.channelID, "Bridge is offline. Run /tp:start in OpenCode first.", { replyToMessageID: command.messageID });
|
|
312
339
|
return;
|
|
313
340
|
}
|
|
341
|
+
this.log(`handleTelegramCommand: accepting prompt, sending accepted to Telegram`);
|
|
314
342
|
await this.telegram.sendMessage(this.config.channelID, "accepted", { replyToMessageID: command.messageID });
|
|
315
343
|
const job = {
|
|
316
344
|
telegramUpdateID: command.updateID,
|
|
@@ -414,19 +442,32 @@ export class BridgeController {
|
|
|
414
442
|
});
|
|
415
443
|
}
|
|
416
444
|
async processPromptQueue() {
|
|
417
|
-
if (this.processingQueue)
|
|
445
|
+
if (this.processingQueue) {
|
|
446
|
+
this.log("processPromptQueue: queue processing already in progress");
|
|
418
447
|
return;
|
|
448
|
+
}
|
|
419
449
|
this.processingQueue = true;
|
|
450
|
+
this.log("processPromptQueue: started processing queue");
|
|
420
451
|
try {
|
|
421
452
|
while (true) {
|
|
422
453
|
const state = await this.syncState();
|
|
423
|
-
if (!this.lease.isOwner(state))
|
|
454
|
+
if (!this.lease.isOwner(state)) {
|
|
455
|
+
this.log(`processPromptQueue: stopped - not owner of lease`);
|
|
424
456
|
return;
|
|
425
|
-
|
|
457
|
+
}
|
|
458
|
+
if (state.activePrompt) {
|
|
459
|
+
this.log(`processPromptQueue: stopped - another prompt is active (activePromptID=${state.activePrompt.userMessageID})`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (state.promptQueue.length === 0) {
|
|
463
|
+
this.log(`processPromptQueue: queue is empty`);
|
|
426
464
|
return;
|
|
465
|
+
}
|
|
427
466
|
const job = state.promptQueue[0];
|
|
428
|
-
if (!state.bound.sessionID)
|
|
467
|
+
if (!state.bound.sessionID) {
|
|
468
|
+
this.log(`processPromptQueue: error - missing sessionID in state`);
|
|
429
469
|
return;
|
|
470
|
+
}
|
|
430
471
|
const startedAt = this.deps.now();
|
|
431
472
|
const activeJob = {
|
|
432
473
|
...job,
|
|
@@ -438,16 +479,20 @@ export class BridgeController {
|
|
|
438
479
|
promptQueue: state.promptQueue.slice(1),
|
|
439
480
|
};
|
|
440
481
|
await this.persist(next);
|
|
482
|
+
this.log(`processPromptQueue: activeJob set to userMessageID=${activeJob.userMessageID}`);
|
|
441
483
|
this.api.ui.toast({
|
|
442
484
|
variant: "info",
|
|
443
485
|
message: `Teleprompt: sending prompt to session ${state.bound.sessionID}...`,
|
|
444
486
|
});
|
|
445
|
-
await this.telegram.sendMessage(this.config.channelID, "running", { replyToMessageID: activeJob.telegramMessageID });
|
|
446
487
|
try {
|
|
447
|
-
|
|
488
|
+
this.log(`processPromptQueue: submitting to client: prompt="${job.prompt}"`);
|
|
489
|
+
const submitted = await submitPrompt(this.client, state.bound.sessionID, job.prompt, job.telegramUpdateID, this.api.state.path.directory, state.bound.model);
|
|
490
|
+
this.log(`processPromptQueue: submitted successfully. userMessageID=${submitted.userMessageID}, assistantMessageID=${submitted.assistantMessageID}. Waiting for completion...`);
|
|
448
491
|
await this.waitForPromptCompletion(state.bound.sessionID, submitted.userMessageID, submitted.assistantMessageID);
|
|
492
|
+
this.log(`processPromptQueue: completed successfully`);
|
|
449
493
|
}
|
|
450
494
|
catch (error) {
|
|
495
|
+
this.log(`processPromptQueue: execution failed: ${String(error)}`);
|
|
451
496
|
const latest = await this.requireState();
|
|
452
497
|
if (latest.activePrompt?.userMessageID === activeJob.userMessageID) {
|
|
453
498
|
await this.persist(this.appendPromptHistory({
|
|
@@ -468,37 +513,52 @@ export class BridgeController {
|
|
|
468
513
|
}
|
|
469
514
|
finally {
|
|
470
515
|
this.processingQueue = false;
|
|
516
|
+
this.log("processPromptQueue: finished processing queue");
|
|
471
517
|
}
|
|
472
518
|
}
|
|
473
|
-
async onAssistantCompleted(
|
|
474
|
-
|
|
519
|
+
async onAssistantCompleted(_sessionID, _assistantMessageID, _parentUserMessageID) {
|
|
520
|
+
// Intentionally a no-op. Completion is handled by waitForPromptCompletion
|
|
521
|
+
// polling. The event stream fires for EVERY completed assistant message,
|
|
522
|
+
// including intermediate ones during the agent tool-call loop, so reacting
|
|
523
|
+
// here would prematurely abort the agent before the final answer arrives.
|
|
475
524
|
}
|
|
476
525
|
async completeActivePrompt(sessionID, assistantMessageID, diffMessageID, directParts) {
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
return;
|
|
480
|
-
if (!state.activePrompt)
|
|
526
|
+
// Guard against duplicate completion from event stream + polling race
|
|
527
|
+
if (this.completionInFlight)
|
|
481
528
|
return;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
summary
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
529
|
+
this.completionInFlight = true;
|
|
530
|
+
try {
|
|
531
|
+
const state = await this.syncState();
|
|
532
|
+
if (!this.lease.isOwner(state))
|
|
533
|
+
return;
|
|
534
|
+
if (!state.activePrompt)
|
|
535
|
+
return;
|
|
536
|
+
if (state.bound.sessionID !== sessionID)
|
|
537
|
+
return;
|
|
538
|
+
// Removed session.abort() because we want the agent to finish its turn naturally,
|
|
539
|
+
// avoiding "interrupted" UI states.
|
|
540
|
+
const summary = await this.buildSummary(sessionID, assistantMessageID, diffMessageID, directParts);
|
|
541
|
+
const completedAt = this.deps.now();
|
|
542
|
+
const elapsed = completedAt - (state.activePrompt.startedAt ?? state.activePrompt.createdAt);
|
|
543
|
+
const resultText = formatSummaryForTelegram(summary, this.config.summaryMaxChars);
|
|
544
|
+
const combinedMessage = `✅ completed in ${formatAgeMs(Math.max(0, elapsed))}\n\n${resultText}`;
|
|
545
|
+
await this.telegram.sendMessage(this.config.channelID, combinedMessage, { replyToMessageID: state.activePrompt.telegramMessageID });
|
|
546
|
+
await this.persist(this.appendPromptHistory({
|
|
547
|
+
...state,
|
|
548
|
+
activePrompt: undefined,
|
|
549
|
+
}, {
|
|
550
|
+
jobID: state.activePrompt.userMessageID,
|
|
551
|
+
prompt: state.activePrompt.prompt,
|
|
552
|
+
summary: summary.text,
|
|
553
|
+
changedFiles: summary.changedFiles,
|
|
554
|
+
status: "completed",
|
|
555
|
+
at: completedAt,
|
|
556
|
+
}));
|
|
557
|
+
await this.processPromptQueue();
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
this.completionInFlight = false;
|
|
561
|
+
}
|
|
502
562
|
}
|
|
503
563
|
async onUserMessage(sessionID, userMessageID) {
|
|
504
564
|
const state = await this.syncState();
|
|
@@ -508,12 +568,12 @@ export class BridgeController {
|
|
|
508
568
|
return;
|
|
509
569
|
if (state.bound.sessionID !== sessionID)
|
|
510
570
|
return;
|
|
511
|
-
if (userMessageID.startsWith("
|
|
571
|
+
if (userMessageID.startsWith("msg_tg_"))
|
|
512
572
|
return;
|
|
513
573
|
if (state.activePrompt)
|
|
514
574
|
return;
|
|
515
575
|
try {
|
|
516
|
-
await this.client.session.abort({
|
|
576
|
+
await this.client.session.abort({ sessionID, directory: this.api.state.path.directory });
|
|
517
577
|
}
|
|
518
578
|
catch { }
|
|
519
579
|
this.api.ui.toast({
|
|
@@ -653,14 +713,7 @@ export class BridgeController {
|
|
|
653
713
|
.trim();
|
|
654
714
|
let changedFiles = [];
|
|
655
715
|
try {
|
|
656
|
-
const diffResponse = await this.client.session.message({
|
|
657
|
-
path: {
|
|
658
|
-
id: sessionID,
|
|
659
|
-
messageID: userMessageID,
|
|
660
|
-
},
|
|
661
|
-
responseStyle: "data",
|
|
662
|
-
throwOnError: true,
|
|
663
|
-
});
|
|
716
|
+
const diffResponse = await this.client.session.message({ sessionID, messageID: userMessageID, directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
664
717
|
const diff = diffResponse?.diff;
|
|
665
718
|
changedFiles = (diff || []).map((item) => item.file);
|
|
666
719
|
}
|
|
@@ -804,11 +857,7 @@ export class BridgeController {
|
|
|
804
857
|
return;
|
|
805
858
|
}
|
|
806
859
|
try {
|
|
807
|
-
await this.client.session.summarize({
|
|
808
|
-
path: { id: state.bound.sessionID },
|
|
809
|
-
responseStyle: "data",
|
|
810
|
-
throwOnError: true,
|
|
811
|
-
});
|
|
860
|
+
await this.client.session.summarize({ sessionID: state.bound.sessionID, directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
812
861
|
await this.telegram.sendMessage(this.config.channelID, `Compaction requested for session ${state.bound.sessionID}.`, { replyToMessageID });
|
|
813
862
|
}
|
|
814
863
|
catch (error) {
|
|
@@ -932,7 +981,7 @@ export class BridgeController {
|
|
|
932
981
|
return;
|
|
933
982
|
}
|
|
934
983
|
try {
|
|
935
|
-
await this.client.session.abort({
|
|
984
|
+
await this.client.session.abort({ sessionID: state.bound.sessionID, directory: this.api.state.path.directory });
|
|
936
985
|
const latest = await this.requireState();
|
|
937
986
|
if (latest.activePrompt?.userMessageID === state.activePrompt.userMessageID) {
|
|
938
987
|
await this.persist(this.appendPromptHistory({
|
|
@@ -1006,11 +1055,7 @@ export class BridgeController {
|
|
|
1006
1055
|
if (!sessionID)
|
|
1007
1056
|
return undefined;
|
|
1008
1057
|
try {
|
|
1009
|
-
const response = await this.client.session.get({
|
|
1010
|
-
path: { id: sessionID },
|
|
1011
|
-
responseStyle: "data",
|
|
1012
|
-
throwOnError: true,
|
|
1013
|
-
});
|
|
1058
|
+
const response = await this.client.session.get({ sessionID, directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
1014
1059
|
const title = response?.title ?? response?.session?.title;
|
|
1015
1060
|
if (typeof title === "string" && title.trim().length > 0)
|
|
1016
1061
|
return title.trim();
|
|
@@ -1271,13 +1316,7 @@ export class BridgeController {
|
|
|
1271
1316
|
return;
|
|
1272
1317
|
this.activePromptCheckInFlight = true;
|
|
1273
1318
|
try {
|
|
1274
|
-
const response = await this.client.session.messages({
|
|
1275
|
-
path: {
|
|
1276
|
-
id: state.bound.sessionID,
|
|
1277
|
-
},
|
|
1278
|
-
responseStyle: "data",
|
|
1279
|
-
throwOnError: true,
|
|
1280
|
-
});
|
|
1319
|
+
const response = await this.client.session.messages({ sessionID: state.bound.sessionID, directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
1281
1320
|
const messages = Array.isArray(response) ? response : (response?.messages || response?.items || response?.data || []);
|
|
1282
1321
|
const threshold = (state.activePrompt.startedAt ?? state.activePrompt.createdAt) - 1_000;
|
|
1283
1322
|
const latestCompletedAssistant = [...messages]
|
|
@@ -1300,17 +1339,17 @@ export class BridgeController {
|
|
|
1300
1339
|
}
|
|
1301
1340
|
async clearTuiPrompt() {
|
|
1302
1341
|
try {
|
|
1303
|
-
await this.client.tui.clearPrompt({
|
|
1304
|
-
responseStyle: "data",
|
|
1305
|
-
throwOnError: true,
|
|
1306
|
-
});
|
|
1342
|
+
await this.client.tui.clearPrompt({}, { responseStyle: "data", throwOnError: true });
|
|
1307
1343
|
}
|
|
1308
1344
|
catch { }
|
|
1309
1345
|
}
|
|
1310
|
-
async waitForPromptCompletion(sessionID, userMessageID,
|
|
1346
|
+
async waitForPromptCompletion(sessionID, userMessageID, _assistantMessageID) {
|
|
1311
1347
|
const startedAt = this.deps.now();
|
|
1312
1348
|
const timeoutMs = 10 * 60 * 1000;
|
|
1313
1349
|
const pollEveryMs = 2000;
|
|
1350
|
+
const stableDelayMs = 3000; // Wait 3s of no new messages before declaring done
|
|
1351
|
+
let lastSeenAssistantID;
|
|
1352
|
+
let stableSince;
|
|
1314
1353
|
while (this.deps.now() - startedAt < timeoutMs) {
|
|
1315
1354
|
const state = await this.syncState();
|
|
1316
1355
|
if (!this.lease.isOwner(state))
|
|
@@ -1320,41 +1359,68 @@ export class BridgeController {
|
|
|
1320
1359
|
if (!state.activePrompt || state.activePrompt.userMessageID !== userMessageID)
|
|
1321
1360
|
return;
|
|
1322
1361
|
try {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
},
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1362
|
+
// 1. Poll the session status
|
|
1363
|
+
let isIdle = false;
|
|
1364
|
+
try {
|
|
1365
|
+
const statusResponse = await this.client.session.status({ query: { directory: this.api.state.path.directory } }, { responseStyle: "data", throwOnError: true });
|
|
1366
|
+
const statusType = statusResponse?.type || statusResponse?.data?.type;
|
|
1367
|
+
isIdle = statusType === "idle";
|
|
1368
|
+
}
|
|
1369
|
+
catch (statusErr) {
|
|
1370
|
+
this.log(`waitForPromptCompletion: failed to get session status: ${String(statusErr)}`);
|
|
1371
|
+
}
|
|
1372
|
+
const response = await this.client.session.messages({ sessionID, directory: this.api.state.path.directory }, { responseStyle: "data", throwOnError: true });
|
|
1330
1373
|
const messages = Array.isArray(response) ? response : (response?.messages || response?.items || response?.data || []);
|
|
1374
|
+
// 2. Find the user message created after we started
|
|
1375
|
+
const userMessage = [...messages]
|
|
1376
|
+
.reverse()
|
|
1377
|
+
.find((entry) => {
|
|
1378
|
+
const info = entry?.info;
|
|
1379
|
+
if (info?.role !== "user")
|
|
1380
|
+
return false;
|
|
1381
|
+
const created = info?.time?.created || 0;
|
|
1382
|
+
if (created > 0 && created < startedAt - 5000)
|
|
1383
|
+
return false;
|
|
1384
|
+
return true;
|
|
1385
|
+
});
|
|
1386
|
+
const actualUserMessageID = userMessage?.info?.id || userMessageID;
|
|
1387
|
+
// Find the LATEST completed assistant message created after we started
|
|
1331
1388
|
const completedAssistant = [...messages]
|
|
1332
1389
|
.reverse()
|
|
1333
1390
|
.find((entry) => {
|
|
1334
1391
|
const info = entry?.info;
|
|
1335
1392
|
if (info?.role !== "assistant")
|
|
1336
1393
|
return false;
|
|
1337
|
-
if (
|
|
1394
|
+
if (!info?.time?.completed)
|
|
1338
1395
|
return false;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const isCompleted = typeof info?.time?.completed === "number";
|
|
1342
|
-
if (!isCompleted)
|
|
1396
|
+
const created = info?.time?.created || 0;
|
|
1397
|
+
if (created > 0 && created < startedAt - 5000)
|
|
1343
1398
|
return false;
|
|
1344
|
-
// If we don't have a specific ID, ensure this message wasn't created before we started
|
|
1345
|
-
if (!assistantMessageID) {
|
|
1346
|
-
const created = info?.time?.created || 0;
|
|
1347
|
-
if (created > 0 && created < startedAt - 5000)
|
|
1348
|
-
return false; // 5s buffer
|
|
1349
|
-
}
|
|
1350
1399
|
return true;
|
|
1351
1400
|
});
|
|
1352
1401
|
if (completedAssistant?.info?.id) {
|
|
1353
1402
|
if (completedAssistant.info.error) {
|
|
1354
1403
|
throw new Error(`Assistant error: ${JSON.stringify(completedAssistant.info.error)}`);
|
|
1355
1404
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1405
|
+
// If the session status is idle, we can fast-path immediately
|
|
1406
|
+
if (isIdle) {
|
|
1407
|
+
this.log(`waitForPromptCompletion: session is idle, fast-path completing`);
|
|
1408
|
+
await this.completeActivePrompt(sessionID, completedAssistant.info.id, actualUserMessageID, completedAssistant.parts);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
// Stable-state detection: wait until no NEW assistant messages appear
|
|
1412
|
+
if (completedAssistant.info.id !== lastSeenAssistantID) {
|
|
1413
|
+
// New message appeared — reset the stability timer
|
|
1414
|
+
lastSeenAssistantID = completedAssistant.info.id;
|
|
1415
|
+
stableSince = this.deps.now();
|
|
1416
|
+
this.log(`waitForPromptCompletion: new assistant message ${completedAssistant.info.id}, resetting stability timer`);
|
|
1417
|
+
}
|
|
1418
|
+
else if (stableSince && (this.deps.now() - stableSince >= stableDelayMs)) {
|
|
1419
|
+
// Same message for stableDelayMs — model has stopped, we're done
|
|
1420
|
+
this.log(`waitForPromptCompletion: stable for ${stableDelayMs}ms, completing`);
|
|
1421
|
+
await this.completeActivePrompt(sessionID, completedAssistant.info.id, actualUserMessageID, completedAssistant.parts);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1358
1424
|
}
|
|
1359
1425
|
}
|
|
1360
1426
|
catch (err) {
|