@llblab/pi-kit 0.5.1 → 0.5.2
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +16 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -0
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
- package/node_modules/@llblab/pi-telegram/index.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +60 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +101 -90
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +18 -7
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +10 -3
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +97 -66
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +27 -35
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -353,6 +353,7 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
353
353
|
typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
|
|
354
354
|
>[0];
|
|
355
355
|
waitForActivityIdle?: () => Promise<void>;
|
|
356
|
+
enqueue?: Activity.TelegramActivityPublicationRuntime["enqueue"];
|
|
356
357
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
357
358
|
}): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
|
|
358
359
|
const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
|
|
@@ -364,6 +365,7 @@ export function createTelegramAssistantOutputBindingRuntime<
|
|
|
364
365
|
);
|
|
365
366
|
const runtime = Activity.createTelegramAssistantOutputRuntime({
|
|
366
367
|
...authority,
|
|
368
|
+
enqueue: deps.enqueue,
|
|
367
369
|
async send(event, authority, isAuthorityActive) {
|
|
368
370
|
await deps.waitForActivityIdle?.();
|
|
369
371
|
if (!isAuthorityActive()) return;
|
|
@@ -390,7 +392,14 @@ type TelegramAssistantOutputAuthority<TTransportStamp> = ReturnType<
|
|
|
390
392
|
Routing.TelegramAssistantOutputAuthorityRuntime<TTransportStamp>["captureAuthority"]
|
|
391
393
|
>;
|
|
392
394
|
|
|
395
|
+
export interface TelegramBridgePublicationRuntime {
|
|
396
|
+
enqueue: Activity.TelegramActivityPublicationRuntime["enqueue"];
|
|
397
|
+
reserve: Activity.TelegramActivityPublicationRuntime["reserve"];
|
|
398
|
+
capture: () => { target?: Queue.TelegramQueueTarget; isCurrent: () => boolean };
|
|
399
|
+
}
|
|
400
|
+
|
|
393
401
|
export interface TelegramActivityBindingRuntime {
|
|
402
|
+
publicationRuntime: TelegramBridgePublicationRuntime;
|
|
394
403
|
activityRuntime: Activity.TelegramActivityRuntime;
|
|
395
404
|
activityVerbosityRuntime: ActivityVerbosity.TelegramActivityVerbosityRuntime;
|
|
396
405
|
assistantOutputRuntime: Activity.TelegramAssistantOutputRuntime;
|
|
@@ -403,7 +412,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
|
|
|
403
412
|
Parameters<
|
|
404
413
|
typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>
|
|
405
414
|
>[0],
|
|
406
|
-
"waitForActivityIdle"
|
|
415
|
+
"waitForActivityIdle" | "enqueue"
|
|
407
416
|
>;
|
|
408
417
|
activityVerbosity: Omit<
|
|
409
418
|
Parameters<
|
|
@@ -411,19 +420,19 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
|
|
|
411
420
|
TelegramAssistantOutputAuthority<TTransportStamp>
|
|
412
421
|
>
|
|
413
422
|
>[0],
|
|
414
|
-
"captureAuthority" | "isAuthorityActive" | "recordFailure"
|
|
423
|
+
"captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue"
|
|
415
424
|
>;
|
|
416
425
|
}): TelegramActivityBindingRuntime {
|
|
417
|
-
const
|
|
418
|
-
ActivityVerbosity.createTelegramActivityVerbosityBinding();
|
|
426
|
+
const publication = Activity.createTelegramActivityPublicationRuntime();
|
|
419
427
|
const assistantOutputBinding =
|
|
420
428
|
createTelegramAssistantOutputBindingRuntime({
|
|
421
429
|
...deps.assistantOutput,
|
|
422
|
-
|
|
430
|
+
enqueue: publication.enqueue,
|
|
423
431
|
});
|
|
424
432
|
const activityVerbosityRuntime =
|
|
425
433
|
ActivityVerbosity.createTelegramActivityVerbosityRuntime({
|
|
426
434
|
...deps.activityVerbosity,
|
|
435
|
+
enqueue: publication.enqueue,
|
|
427
436
|
captureAuthority: assistantOutputBinding.authority.captureAuthority,
|
|
428
437
|
isAuthorityActive: assistantOutputBinding.authority.isAuthorityActive,
|
|
429
438
|
recordFailure(operation, event, error) {
|
|
@@ -434,7 +443,6 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
|
|
|
434
443
|
});
|
|
435
444
|
},
|
|
436
445
|
});
|
|
437
|
-
activityVerbosityBinding.bind(activityVerbosityRuntime);
|
|
438
446
|
const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
|
|
439
447
|
generation: deps.generation,
|
|
440
448
|
observeEvent(event) {
|
|
@@ -450,9 +458,30 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
|
|
|
450
458
|
},
|
|
451
459
|
});
|
|
452
460
|
return {
|
|
453
|
-
activityRuntime
|
|
461
|
+
activityRuntime: {
|
|
462
|
+
...activityRuntime,
|
|
463
|
+
onSessionStart() {
|
|
464
|
+
publication.reset();
|
|
465
|
+
activityRuntime.onSessionStart?.();
|
|
466
|
+
},
|
|
467
|
+
onSessionShutdown() {
|
|
468
|
+
publication.reset();
|
|
469
|
+
activityRuntime.onSessionShutdown();
|
|
470
|
+
},
|
|
471
|
+
},
|
|
454
472
|
activityVerbosityRuntime,
|
|
455
473
|
assistantOutputRuntime: assistantOutputBinding.runtime,
|
|
474
|
+
publicationRuntime: {
|
|
475
|
+
enqueue: publication.enqueue,
|
|
476
|
+
reserve: publication.reserve,
|
|
477
|
+
capture() {
|
|
478
|
+
const authority = assistantOutputBinding.authority.captureAuthority();
|
|
479
|
+
return {
|
|
480
|
+
target: authority.target ? { ...authority.target } : undefined,
|
|
481
|
+
isCurrent: () => assistantOutputBinding.authority.isAuthorityActive(authority),
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
},
|
|
456
485
|
};
|
|
457
486
|
}
|
|
458
487
|
|
|
@@ -680,6 +709,7 @@ export function registerTelegramCommandsAndTools({
|
|
|
680
709
|
|
|
681
710
|
interface TelegramLifecycleBindingDeps {
|
|
682
711
|
pi: Pi.ExtensionAPI;
|
|
712
|
+
publicationRuntime: TelegramBridgePublicationRuntime;
|
|
683
713
|
activityRuntime: Activity.TelegramActivityRuntime;
|
|
684
714
|
activityVerbosityRuntime?: ActivityVerbosity.TelegramActivityVerbosityRuntime;
|
|
685
715
|
assistantOutputRuntime: Pick<
|
|
@@ -770,6 +800,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
770
800
|
|
|
771
801
|
export function registerTelegramLifecycleRuntimeHooks({
|
|
772
802
|
pi,
|
|
803
|
+
publicationRuntime,
|
|
773
804
|
activityRuntime,
|
|
774
805
|
activityVerbosityRuntime,
|
|
775
806
|
assistantOutputRuntime,
|
|
@@ -936,24 +967,19 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
936
967
|
{ replyToPrompt: false },
|
|
937
968
|
);
|
|
938
969
|
};
|
|
939
|
-
let
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
timer.unref?.();
|
|
947
|
-
});
|
|
948
|
-
await task();
|
|
949
|
-
})().catch((error) => {
|
|
950
|
-
recordRuntimeEvent("delivery", error, {
|
|
951
|
-
phase: "agent-end-background-delivery",
|
|
952
|
-
});
|
|
953
|
-
});
|
|
970
|
+
let pendingFinalPublication: {
|
|
971
|
+
turn: Queue.PendingTelegramTurn;
|
|
972
|
+
reservation: Activity.TelegramActivityPublicationReservation;
|
|
973
|
+
} | undefined;
|
|
974
|
+
const cancelPendingFinalPublication = (): void => {
|
|
975
|
+
pendingFinalPublication?.reservation.cancel();
|
|
976
|
+
pendingFinalPublication = undefined;
|
|
954
977
|
};
|
|
955
|
-
const
|
|
956
|
-
|
|
978
|
+
const recordPublicationFailure = (error: unknown): void => {
|
|
979
|
+
recordRuntimeEvent("delivery", error, { phase: "agent-end-background-delivery" });
|
|
980
|
+
};
|
|
981
|
+
const scheduleActiveTurnDelivery = (task: () => Promise<void>): void => {
|
|
982
|
+
void publicationRuntime.enqueue(task).catch(recordPublicationFailure);
|
|
957
983
|
};
|
|
958
984
|
const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
|
|
959
985
|
Queue.PendingTelegramTurn,
|
|
@@ -989,14 +1015,22 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
989
1015
|
isSessionActive: isSessionContextActive,
|
|
990
1016
|
isTurnTransportActive,
|
|
991
1017
|
waitForTypingIdle: typing.waitForIdle,
|
|
992
|
-
async waitForActivityIdle() {
|
|
993
|
-
await activityVerbosityRuntime?.waitForIdle();
|
|
994
|
-
await assistantOutputRuntime.waitForIdle();
|
|
995
|
-
},
|
|
996
1018
|
dispatchNextQueuedTelegramTurn,
|
|
997
1019
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
998
1020
|
deferredQueueDispatchRuntime.request,
|
|
999
1021
|
scheduleActiveTurnDelivery,
|
|
1022
|
+
reserveActiveTurnDelivery() {
|
|
1023
|
+
const pending = pendingFinalPublication;
|
|
1024
|
+
pendingFinalPublication = undefined;
|
|
1025
|
+
const matches = pending?.turn === activeTurnRuntime.get();
|
|
1026
|
+
if (!matches) pending?.reservation.cancel();
|
|
1027
|
+
const reservation = matches && pending ? pending.reservation : publicationRuntime.reserve();
|
|
1028
|
+
return {
|
|
1029
|
+
schedule: (task) => { void reservation.publish(task).catch(recordPublicationFailure); },
|
|
1030
|
+
cancel: reservation.cancel,
|
|
1031
|
+
};
|
|
1032
|
+
},
|
|
1033
|
+
preparePreviewClear: previewRuntime.prepareClear,
|
|
1000
1034
|
clearPreview: previewRuntime.clear,
|
|
1001
1035
|
setPreviewPendingText: previewRuntime.setPendingText,
|
|
1002
1036
|
finalizeMarkdownPreview,
|
|
@@ -1038,25 +1072,24 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1038
1072
|
};
|
|
1039
1073
|
let observedAutomaticCompaction = false;
|
|
1040
1074
|
let agentWorkActive = false;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const sendCompactionNotice = async (text: string): Promise<void> => {
|
|
1075
|
+
const prepareCompactionNotice = (text: string, ctx: Pi.ExtensionContext): (() => Promise<void>) => {
|
|
1076
|
+
const authority = publicationRuntime.capture();
|
|
1044
1077
|
const turn = activeTurnRuntime.get();
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1078
|
+
const selectedTarget = turn?.target ?? authority.target;
|
|
1079
|
+
const target = selectedTarget ? { ...selectedTarget } : undefined;
|
|
1080
|
+
const replyToMessageId = turn?.replyToMessageId;
|
|
1081
|
+
return async () => {
|
|
1082
|
+
if (!target || !isSessionContextActive(ctx) || !authority.isCurrent()) return;
|
|
1083
|
+
if (turn && isTurnTransportActive?.(turn) === false) return;
|
|
1084
|
+
try {
|
|
1085
|
+
await sendMarkdownReply(target.chatId, replyToMessageId, text, { target });
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
recordRuntimeEvent("delivery", error, { phase: "compaction-notice" });
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1056
1090
|
};
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
for (const notice of notices) await sendCompactionNotice(notice);
|
|
1091
|
+
const sendCompactionNotice = (text: string, ctx: Pi.ExtensionContext): void => {
|
|
1092
|
+
scheduleActiveTurnDelivery(prepareCompactionNotice(text, ctx));
|
|
1060
1093
|
};
|
|
1061
1094
|
const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
|
|
1062
1095
|
isContextActive: isSessionContextActive,
|
|
@@ -1070,7 +1103,6 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1070
1103
|
recordRuntimeEvent,
|
|
1071
1104
|
onCompactionAbandoned: () => {
|
|
1072
1105
|
observedAutomaticCompaction = false;
|
|
1073
|
-
deferredAutomaticCompactionNotices.length = 0;
|
|
1074
1106
|
activityRuntime.onCompactionAbandoned();
|
|
1075
1107
|
},
|
|
1076
1108
|
});
|
|
@@ -1091,6 +1123,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1091
1123
|
activityRuntime.recordInputSource(event.source ?? "unknown");
|
|
1092
1124
|
},
|
|
1093
1125
|
async onSessionStart(event, ctx) {
|
|
1126
|
+
cancelPendingFinalPublication();
|
|
1094
1127
|
previewRuntime.invalidate();
|
|
1095
1128
|
assistantOutputRuntime.start();
|
|
1096
1129
|
activityRuntime.onSessionStart?.();
|
|
@@ -1106,9 +1139,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1106
1139
|
assistantOutputRuntime.stop();
|
|
1107
1140
|
observedAutomaticCompaction = false;
|
|
1108
1141
|
agentWorkActive = false;
|
|
1109
|
-
|
|
1142
|
+
cancelPendingFinalPublication();
|
|
1110
1143
|
uiPromptActive = false;
|
|
1111
|
-
deferredAutomaticCompactionNotices.length = 0;
|
|
1112
1144
|
compactionObserver.onSessionShutdown();
|
|
1113
1145
|
if (event.reason === "quit" && disconnectOnQuit) {
|
|
1114
1146
|
try {
|
|
@@ -1129,19 +1161,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1129
1161
|
if (shouldNotify) observedAutomaticCompaction = true;
|
|
1130
1162
|
activityRuntime.onCompactionStart(Pi.getSessionCompactionReason(event));
|
|
1131
1163
|
compactionObserver.onSessionBeforeCompact(event, ctx);
|
|
1132
|
-
if (shouldNotify)
|
|
1133
|
-
if (terminalAssistantMessagePendingDelivery) {
|
|
1134
|
-
deferredAutomaticCompactionNotices.push(
|
|
1135
|
-
Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
|
|
1136
|
-
);
|
|
1137
|
-
} else {
|
|
1138
|
-
await waitForActiveTurnDelivery();
|
|
1139
|
-
if (!isSessionContextActive(ctx)) return;
|
|
1140
|
-
await sendCompactionNotice(
|
|
1141
|
-
Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN,
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1164
|
+
if (shouldNotify) sendCompactionNotice(Commands.TELEGRAM_COMPACTION_STARTED_MARKDOWN, ctx);
|
|
1145
1165
|
},
|
|
1146
1166
|
async onSessionCompact(event, ctx) {
|
|
1147
1167
|
if (!isSessionContextActive(ctx)) return;
|
|
@@ -1149,38 +1169,23 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1149
1169
|
compactionObserver.onSessionCompact(event, ctx);
|
|
1150
1170
|
if (observedAutomaticCompaction) {
|
|
1151
1171
|
observedAutomaticCompaction = false;
|
|
1152
|
-
|
|
1153
|
-
deferredAutomaticCompactionNotices.push(
|
|
1154
|
-
Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
|
|
1155
|
-
);
|
|
1156
|
-
} else {
|
|
1157
|
-
await sendCompactionNotice(
|
|
1158
|
-
Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN,
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1172
|
+
sendCompactionNotice(Commands.TELEGRAM_COMPACTION_COMPLETED_MARKDOWN, ctx);
|
|
1161
1173
|
}
|
|
1162
1174
|
},
|
|
1163
1175
|
async onSessionCompactFailed(event, ctx) {
|
|
1164
1176
|
if (!isSessionContextActive(ctx)) return;
|
|
1165
1177
|
const shouldNotify = observedAutomaticCompaction;
|
|
1166
|
-
const deferredNotices = deferredAutomaticCompactionNotices.splice(0);
|
|
1167
|
-
const shouldDefer =
|
|
1168
|
-
deferredNotices.length > 0 || terminalAssistantMessagePendingDelivery;
|
|
1169
1178
|
compactionObserver.onSessionCompactFailed(event, ctx);
|
|
1170
1179
|
if (!shouldNotify) return;
|
|
1171
1180
|
const notice = event.aborted
|
|
1172
1181
|
? "**⚠️ Compaction cancelled.**"
|
|
1173
1182
|
: "**⚠️ Compaction failed.**";
|
|
1174
|
-
|
|
1175
|
-
deferredAutomaticCompactionNotices.push(...deferredNotices, notice);
|
|
1176
|
-
} else {
|
|
1177
|
-
await sendCompactionNotice(notice);
|
|
1178
|
-
}
|
|
1183
|
+
sendCompactionNotice(notice, ctx);
|
|
1179
1184
|
},
|
|
1180
1185
|
async onAgentStart(event, ctx) {
|
|
1181
1186
|
if (!isSessionContextActive(ctx)) return;
|
|
1182
1187
|
agentWorkActive = true;
|
|
1183
|
-
|
|
1188
|
+
cancelPendingFinalPublication();
|
|
1184
1189
|
await agentStartWithDedupReset(event, ctx);
|
|
1185
1190
|
activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
|
|
1186
1191
|
startAgentActivityTypingLoop(ctx);
|
|
@@ -1230,11 +1235,13 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1230
1235
|
if (event.message.role === "assistant") {
|
|
1231
1236
|
activityRuntime.onAssistantMessageEnd(event.message.stopReason);
|
|
1232
1237
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
+
if (event.message.role !== "assistant" || event.message.stopReason === "toolUse" || event.message.stopReason === "aborted") return;
|
|
1239
|
+
const turn = activeTurnRuntime.get();
|
|
1240
|
+
if (!turn || turn.guestQueryId || pendingFinalPublication?.turn === turn) return;
|
|
1241
|
+
cancelPendingFinalPublication();
|
|
1242
|
+
const assistant = Replies.extractLatestAssistantMessageText([event.message]);
|
|
1243
|
+
if (!assistant.text && assistant.stopReason !== "error" && turn.queuedAttachments.length === 0) return;
|
|
1244
|
+
pendingFinalPublication = { turn, reservation: publicationRuntime.reserve() };
|
|
1238
1245
|
},
|
|
1239
1246
|
onUiPromptStart(event, ctx) {
|
|
1240
1247
|
if (!isSessionContextActive(ctx)) return;
|
|
@@ -1254,18 +1261,22 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1254
1261
|
},
|
|
1255
1262
|
async onAgentEnd(event, ctx) {
|
|
1256
1263
|
if (!isSessionContextActive(ctx)) return;
|
|
1264
|
+
if (pendingFinalPublication && pendingFinalPublication.turn !== activeTurnRuntime.get()) {
|
|
1265
|
+
cancelPendingFinalPublication();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1257
1268
|
activityRuntime.onAgentEnd();
|
|
1258
1269
|
await agentLifecycleHooks.onAgentEnd(event, ctx);
|
|
1259
1270
|
},
|
|
1260
1271
|
async onAgentSettled(event, ctx) {
|
|
1261
1272
|
if (!isSessionContextActive(ctx)) return;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
await
|
|
1265
|
-
|
|
1266
|
-
|
|
1273
|
+
const pending = pendingFinalPublication;
|
|
1274
|
+
try {
|
|
1275
|
+
await agentLifecycleHooks.onAgentSettled(event, ctx);
|
|
1276
|
+
} finally {
|
|
1277
|
+
if (pendingFinalPublication === pending) cancelPendingFinalPublication();
|
|
1267
1278
|
}
|
|
1268
|
-
|
|
1279
|
+
if (!isSessionContextActive(ctx)) return;
|
|
1269
1280
|
agentWorkActive = false;
|
|
1270
1281
|
activityRuntime.onAgentSettled();
|
|
1271
1282
|
modelContextAvailabilityRuntime.reconcile();
|
|
@@ -450,8 +450,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
450
450
|
const setTimer = deps.setTimer ?? setTimeout;
|
|
451
451
|
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
452
452
|
let fallbackTimer: TelegramLifecycleTimer | undefined;
|
|
453
|
+
let observationGeneration = 0;
|
|
453
454
|
let typingStartedByObserver = false;
|
|
454
455
|
const clearFallbackTimer = (): void => {
|
|
456
|
+
observationGeneration += 1;
|
|
455
457
|
if (!fallbackTimer) return;
|
|
456
458
|
clearTimer(fallbackTimer);
|
|
457
459
|
fallbackTimer = undefined;
|
|
@@ -470,7 +472,10 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
470
472
|
!!deps.startTypingLoop && typingStartResult !== false;
|
|
471
473
|
deps.updateStatus(ctx);
|
|
472
474
|
clearFallbackTimer();
|
|
475
|
+
const admittedGeneration = observationGeneration;
|
|
473
476
|
fallbackTimer = setTimer(() => {
|
|
477
|
+
if (observationGeneration !== admittedGeneration) return;
|
|
478
|
+
observationGeneration += 1;
|
|
474
479
|
fallbackTimer = undefined;
|
|
475
480
|
if (deps.isContextActive && !deps.isContextActive(ctx)) return;
|
|
476
481
|
deps.setCompactionInProgress(false);
|
|
@@ -487,8 +492,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
487
492
|
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
488
493
|
},
|
|
489
494
|
onSessionCompact: (_event, ctx) => {
|
|
490
|
-
clearFallbackTimer();
|
|
491
495
|
if (deps.isContextActive && !deps.isContextActive(ctx)) return;
|
|
496
|
+
clearFallbackTimer();
|
|
492
497
|
deps.setCompactionInProgress(false);
|
|
493
498
|
if (typingStartedByObserver) deps.stopTypingLoop?.();
|
|
494
499
|
typingStartedByObserver = false;
|
|
@@ -496,8 +501,8 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
496
501
|
requestDispatch();
|
|
497
502
|
},
|
|
498
503
|
onSessionCompactFailed: (_event, ctx) => {
|
|
499
|
-
clearFallbackTimer();
|
|
500
504
|
if (deps.isContextActive && !deps.isContextActive(ctx)) return;
|
|
505
|
+
clearFallbackTimer();
|
|
501
506
|
deps.setCompactionInProgress(false);
|
|
502
507
|
if (typingStartedByObserver) deps.stopTypingLoop?.();
|
|
503
508
|
typingStartedByObserver = false;
|
|
@@ -219,8 +219,9 @@ export function createTelegramRichOutboundAttachmentSender(
|
|
|
219
219
|
return async (
|
|
220
220
|
turn: TelegramQueuedOutboundAttachmentTurnView,
|
|
221
221
|
markdown: string,
|
|
222
|
-
options?: { replyMarkup?: unknown },
|
|
222
|
+
options?: { replyMarkup?: unknown; isDeliveryActive?: () => boolean },
|
|
223
223
|
): Promise<boolean> => {
|
|
224
|
+
if (options?.isDeliveryActive?.() === false) return false;
|
|
224
225
|
const plan = planTelegramRichOutboundAttachment({
|
|
225
226
|
turn,
|
|
226
227
|
markdown,
|
|
@@ -246,11 +247,13 @@ export function createTelegramRichOutboundAttachmentSender(
|
|
|
246
247
|
new Error("Successful Rich media upload omitted message_id."),
|
|
247
248
|
);
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
if (options?.isDeliveryActive?.() !== false) {
|
|
251
|
+
deps.recordOwnership?.({
|
|
252
|
+
chatId: turn.chatId,
|
|
253
|
+
messageId,
|
|
254
|
+
target: turn.target,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
254
257
|
return true;
|
|
255
258
|
} catch (error) {
|
|
256
259
|
if (isTelegramRichAttachmentCommitUnknownError(error)) throw error;
|
|
@@ -580,6 +583,7 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
|
|
|
580
583
|
) => void;
|
|
581
584
|
statPath?: (path: string) => Promise<{ size: number }>;
|
|
582
585
|
maxAttachmentSizeBytes?: number;
|
|
586
|
+
isDeliveryActive?: () => boolean;
|
|
583
587
|
}
|
|
584
588
|
|
|
585
589
|
export async function queueTelegramOutboundAttachments(options: {
|
|
@@ -925,9 +929,13 @@ export async function sendTelegramOutboundFiles(options: {
|
|
|
925
929
|
export function createTelegramQueuedOutboundAttachmentSender(
|
|
926
930
|
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
927
931
|
) {
|
|
928
|
-
return async (
|
|
932
|
+
return async (
|
|
933
|
+
turn: TelegramQueuedOutboundAttachmentTurnView,
|
|
934
|
+
options?: { isDeliveryActive?: () => boolean },
|
|
935
|
+
): Promise<void> => {
|
|
929
936
|
await sendQueuedTelegramOutboundAttachments(turn, {
|
|
930
937
|
...deps,
|
|
938
|
+
isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
|
|
931
939
|
maxAttachmentSizeBytes:
|
|
932
940
|
deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
|
|
933
941
|
});
|
|
@@ -939,9 +947,11 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
939
947
|
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
940
948
|
): Promise<void> {
|
|
941
949
|
for (const attachment of turn.queuedAttachments) {
|
|
950
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
942
951
|
try {
|
|
943
952
|
if (deps.maxAttachmentSizeBytes !== undefined) {
|
|
944
953
|
const stats = await (deps.statPath ?? stat)(attachment.path);
|
|
954
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
945
955
|
if (stats.size > deps.maxAttachmentSizeBytes) {
|
|
946
956
|
throw new Error(
|
|
947
957
|
formatTelegramOutboundAttachmentSizeLimitError(
|
|
@@ -971,6 +981,7 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
971
981
|
attachment.fileName,
|
|
972
982
|
);
|
|
973
983
|
} catch (error) {
|
|
984
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
974
985
|
const message = error instanceof Error ? error.message : String(error);
|
|
975
986
|
deps.recordRuntimeEvent?.("attachment", error, {
|
|
976
987
|
fileName: attachment.fileName,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type TelegramTarget,
|
|
15
15
|
} from "./target.ts";
|
|
16
16
|
import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
|
|
17
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
17
18
|
|
|
18
19
|
export interface TelegramVoiceReplyTurnView {
|
|
19
20
|
chatId: number;
|
|
@@ -47,6 +48,7 @@ export interface TelegramVoiceReplySenderDeps {
|
|
|
47
48
|
) => Promise<unknown>;
|
|
48
49
|
sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
|
|
49
50
|
sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
|
|
51
|
+
isDeliveryActive?: () => boolean;
|
|
50
52
|
getHandlers?: () => unknown[] | undefined;
|
|
51
53
|
cwd?: string;
|
|
52
54
|
tempDir?: string;
|
|
@@ -123,9 +125,12 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
123
125
|
replyMarkup?: unknown;
|
|
124
126
|
},
|
|
125
127
|
): Promise<void> => {
|
|
128
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
126
129
|
const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
|
|
127
130
|
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
131
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
128
132
|
await sendVoiceChatAction(deps, turn.chatId);
|
|
133
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
129
134
|
const replyParameters = buildVoiceReplyParameters(
|
|
130
135
|
turn.chatId,
|
|
131
136
|
options?.replyToPrompt,
|
|
@@ -171,6 +176,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
171
176
|
): Promise<void> => {
|
|
172
177
|
for (const handler of ports.findVoiceHandlers?.(deps.getHandlers?.()) ??
|
|
173
178
|
[]) {
|
|
179
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
174
180
|
try {
|
|
175
181
|
const filePath = await ports.generateVoiceFile?.(text, {
|
|
176
182
|
lang: options?.lang,
|
|
@@ -187,6 +193,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
187
193
|
});
|
|
188
194
|
return;
|
|
189
195
|
} catch (error) {
|
|
196
|
+
if (isTelegramApiCommitUnknownError(error)) throw error;
|
|
190
197
|
deps.recordRuntimeEvent?.("voice", error, {
|
|
191
198
|
phase: "template-handler-send",
|
|
192
199
|
});
|
|
@@ -194,6 +201,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
for (const handler of ports.getProgrammaticVoiceHandlers?.() ?? []) {
|
|
204
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
197
205
|
try {
|
|
198
206
|
const filePath = await handler(text, {
|
|
199
207
|
lang: options?.lang,
|
|
@@ -206,6 +214,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
206
214
|
});
|
|
207
215
|
return;
|
|
208
216
|
} catch (error) {
|
|
217
|
+
if (isTelegramApiCommitUnknownError(error)) throw error;
|
|
209
218
|
deps.recordRuntimeEvent?.("voice", error, {
|
|
210
219
|
phase: "programmatic-handler-send",
|
|
211
220
|
});
|
|
@@ -215,6 +224,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
215
224
|
const providers = getTelegramVoiceSynthesisProviders();
|
|
216
225
|
|
|
217
226
|
for (const provider of providers) {
|
|
227
|
+
if (deps.isDeliveryActive?.() === false) return;
|
|
218
228
|
let voiceFilePath: string | undefined;
|
|
219
229
|
let originalFilePath: string | undefined;
|
|
220
230
|
|
|
@@ -252,6 +262,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
252
262
|
});
|
|
253
263
|
return;
|
|
254
264
|
} catch (error) {
|
|
265
|
+
if (isTelegramApiCommitUnknownError(error)) throw error;
|
|
255
266
|
deps.recordRuntimeEvent?.("voice", error, { phase: "send" });
|
|
256
267
|
} finally {
|
|
257
268
|
if (voiceFilePath && voiceFilePath !== originalFilePath) {
|
|
@@ -11,6 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import type { TelegramAssistantSegmentEvent } from "./activity.ts";
|
|
12
12
|
import { resolveTelegramTempDir } from "./paths.ts";
|
|
13
13
|
import * as Replies from "./replies.ts";
|
|
14
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
14
15
|
import type {
|
|
15
16
|
TelegramEditMessageTextBody,
|
|
16
17
|
TelegramSendMessageBody,
|
|
@@ -137,6 +138,7 @@ export interface TelegramVoiceReplySenderDeps {
|
|
|
137
138
|
) => Promise<unknown>;
|
|
138
139
|
sendChatAction?: (chatId: number, action: string) => Promise<unknown>;
|
|
139
140
|
sendRecordVoiceAction?: (chatId: number) => Promise<unknown>;
|
|
141
|
+
isDeliveryActive?: () => boolean;
|
|
140
142
|
getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
141
143
|
cwd?: string;
|
|
142
144
|
tempDir?: string;
|
|
@@ -877,15 +879,17 @@ export function createTelegramOutboundReplyPlanner(
|
|
|
877
879
|
export function createTelegramOutboundReplyArtifactSender(
|
|
878
880
|
deps: TelegramVoiceReplySenderDeps,
|
|
879
881
|
) {
|
|
880
|
-
const sendVoiceReply = createTelegramVoiceReplySender(deps);
|
|
881
882
|
return async (
|
|
882
883
|
turn: TelegramVoiceReplyTurnView,
|
|
883
884
|
plan: Pick<
|
|
884
885
|
TelegramOutboundReplyPlan,
|
|
885
886
|
"voiceText" | "voiceReplies" | "lang" | "rate" | "replyMarkup"
|
|
886
887
|
>,
|
|
887
|
-
options?: { replyToPrompt?: boolean },
|
|
888
|
+
options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
|
|
888
889
|
): Promise<void> => {
|
|
890
|
+
const isDeliveryActive = () =>
|
|
891
|
+
deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
|
|
892
|
+
const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
|
|
889
893
|
// Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
|
|
890
894
|
const voiceReplies = plan.voiceReplies?.length
|
|
891
895
|
? plan.voiceReplies
|
|
@@ -896,6 +900,7 @@ export function createTelegramOutboundReplyArtifactSender(
|
|
|
896
900
|
let anyDelivered = false;
|
|
897
901
|
|
|
898
902
|
for (const reply of voiceReplies) {
|
|
903
|
+
if (!isDeliveryActive()) return;
|
|
899
904
|
try {
|
|
900
905
|
await sendVoiceReply(turn, reply.text, {
|
|
901
906
|
lang: reply.lang ?? plan.lang,
|
|
@@ -905,11 +910,13 @@ export function createTelegramOutboundReplyArtifactSender(
|
|
|
905
910
|
replyMarkup: !anyDelivered ? plan.replyMarkup : undefined,
|
|
906
911
|
});
|
|
907
912
|
anyDelivered = true;
|
|
908
|
-
} catch {
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (isTelegramApiCommitUnknownError(error)) throw error;
|
|
909
915
|
// sendVoiceReply already recorded the error; continue to next reply
|
|
910
916
|
}
|
|
911
917
|
}
|
|
912
918
|
|
|
919
|
+
if (!isDeliveryActive()) return;
|
|
913
920
|
if (!anyDelivered) {
|
|
914
921
|
throw new Error(
|
|
915
922
|
"Failed to send voice reply: every voice synthesis provider failed.",
|
|
@@ -140,6 +140,10 @@ export interface TelegramPreviewControllerDeps {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
export interface TelegramPreviewController {
|
|
143
|
+
prepareClear: (
|
|
144
|
+
chatId: number,
|
|
145
|
+
options?: { target?: TelegramTarget; isDeliveryActive?: () => boolean },
|
|
146
|
+
) => () => Promise<void>;
|
|
143
147
|
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
144
148
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
145
149
|
setPendingText: (text: string) => void;
|
|
@@ -322,6 +326,17 @@ export function createTelegramPreviewController(
|
|
|
322
326
|
generation += 1;
|
|
323
327
|
state = undefined;
|
|
324
328
|
},
|
|
329
|
+
prepareClear: (chatId, options) => {
|
|
330
|
+
const admittedState = state;
|
|
331
|
+
const runtime = getRuntimeDeps();
|
|
332
|
+
return async () => {
|
|
333
|
+
if (state !== admittedState) return;
|
|
334
|
+
await clearTelegramPreview(chatId, runtime, {
|
|
335
|
+
...options,
|
|
336
|
+
isDeliveryActive: () => runtime.canSend?.() !== false && options?.isDeliveryActive?.() !== false,
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
},
|
|
325
340
|
clear: (chatId, options) =>
|
|
326
341
|
clearTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
327
342
|
flush: (chatId, options) =>
|
|
@@ -454,17 +469,18 @@ export function shouldUseTelegramDraftPreview(_options: {
|
|
|
454
469
|
export async function clearTelegramPreview(
|
|
455
470
|
chatId: number,
|
|
456
471
|
deps: TelegramPreviewRuntimeDeps,
|
|
457
|
-
options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
|
|
472
|
+
options: { awaitFlush?: boolean; target?: TelegramTarget; isDeliveryActive?: () => boolean } = {},
|
|
458
473
|
): Promise<void> {
|
|
459
474
|
const state = deps.getState();
|
|
460
|
-
if (!state) return;
|
|
475
|
+
if (!state || options.isDeliveryActive?.() === false) return;
|
|
461
476
|
if (state.flushPromise && options.awaitFlush !== false) {
|
|
462
477
|
state.flushRequested = false;
|
|
463
478
|
await state.flushPromise.catch(() => {});
|
|
464
479
|
if (deps.getState() !== state) return;
|
|
465
480
|
}
|
|
481
|
+
if (options.isDeliveryActive?.() === false) return;
|
|
466
482
|
deps.setState(undefined);
|
|
467
|
-
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
483
|
+
if (state.mode === "draft" && state.draftId !== undefined && deps.canSend?.() !== false) {
|
|
468
484
|
try {
|
|
469
485
|
await deps.sendDraft(chatId, state.draftId, undefined, {
|
|
470
486
|
...getTelegramTargetThreadParams(options.target ?? { chatId }),
|