@llblab/pi-kit 0.5.0 → 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 +8 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -4
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
- package/node_modules/@llblab/pi-telegram/index.ts +10 -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/locks.ts +99 -16
- 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/polling.ts +142 -30
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +8 -6
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +101 -66
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
- package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +33 -35
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +2 -0
- package/package.json +2 -2
|
@@ -871,6 +871,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
871
871
|
options.runtimeGeneration ?? allocateTelegramLockRuntimeGeneration();
|
|
872
872
|
let ownedLockKey: string | undefined;
|
|
873
873
|
let ownedLock: TelegramLockEntry | undefined;
|
|
874
|
+
let deliveryRevoked = false;
|
|
874
875
|
const stateOptions = () => ({
|
|
875
876
|
nowMs: getNowMs(),
|
|
876
877
|
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
@@ -917,7 +918,8 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
917
918
|
);
|
|
918
919
|
if (
|
|
919
920
|
state.kind === "active-here" &&
|
|
920
|
-
hasSameLockOwner(current, expectedOwned)
|
|
921
|
+
hasSameLockOwner(current, expectedOwned) &&
|
|
922
|
+
!deliveryRevoked
|
|
921
923
|
) {
|
|
922
924
|
return {
|
|
923
925
|
result: {
|
|
@@ -958,6 +960,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
958
960
|
if (
|
|
959
961
|
!acquireOptions.election &&
|
|
960
962
|
(state.kind === "active-here" || state.kind === "active-elsewhere") &&
|
|
963
|
+
!(deliveryRevoked && hasSameLockOwner(current, expectedOwned)) &&
|
|
961
964
|
(!acquireOptions.force ||
|
|
962
965
|
!expectedReplacementMatches ||
|
|
963
966
|
!canReplaceCurrent)
|
|
@@ -978,6 +981,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
978
981
|
locks[effectiveKey] = lock;
|
|
979
982
|
ownedLockKey = effectiveKey;
|
|
980
983
|
ownedLock = lock;
|
|
984
|
+
deliveryRevoked = false;
|
|
981
985
|
return {
|
|
982
986
|
result: {
|
|
983
987
|
ok: true,
|
|
@@ -987,8 +991,10 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
987
991
|
changed: true,
|
|
988
992
|
};
|
|
989
993
|
}),
|
|
990
|
-
release: () =>
|
|
991
|
-
|
|
994
|
+
release: () => {
|
|
995
|
+
// Withdraw local send authority even if the durable release fails.
|
|
996
|
+
deliveryRevoked = true;
|
|
997
|
+
return withLockTransaction(locksPath, (locks) => {
|
|
992
998
|
const effectiveKey = resolveEffectiveKey();
|
|
993
999
|
const state = getLockState(
|
|
994
1000
|
parseTelegramLockEntry(locks[effectiveKey]),
|
|
@@ -1008,17 +1014,20 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1008
1014
|
ownedLock = undefined;
|
|
1009
1015
|
}
|
|
1010
1016
|
return { result: state, changed };
|
|
1011
|
-
})
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1012
1019
|
getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
|
|
1013
1020
|
getStatusLabel: () =>
|
|
1014
1021
|
formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
|
|
1015
1022
|
getOwnedLeaderEpoch: () => {
|
|
1023
|
+
if (deliveryRevoked) return undefined;
|
|
1016
1024
|
const effectiveKey = resolveEffectiveKey();
|
|
1017
1025
|
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
1018
1026
|
const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
|
|
1019
1027
|
return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
|
|
1020
1028
|
},
|
|
1021
1029
|
owns: (ctx) => {
|
|
1030
|
+
if (deliveryRevoked) return false;
|
|
1022
1031
|
const effectiveKey = resolveEffectiveKey();
|
|
1023
1032
|
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
1024
1033
|
return hasSameLockOwner(
|
|
@@ -1027,7 +1036,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1027
1036
|
);
|
|
1028
1037
|
},
|
|
1029
1038
|
commitIfOwned: (commit) =>
|
|
1030
|
-
withLockTransaction(locksPath, (locks) => {
|
|
1039
|
+
!deliveryRevoked && withLockTransaction(locksPath, (locks) => {
|
|
1031
1040
|
const effectiveKey = resolveEffectiveKey();
|
|
1032
1041
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1033
1042
|
const exactOwner =
|
|
@@ -1043,7 +1052,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1043
1052
|
return { result: true, changed: false };
|
|
1044
1053
|
}),
|
|
1045
1054
|
refresh: (ctx) =>
|
|
1046
|
-
withLockTransaction(locksPath, (locks) => {
|
|
1055
|
+
!deliveryRevoked && withLockTransaction(locksPath, (locks) => {
|
|
1047
1056
|
const effectiveKey = resolveEffectiveKey();
|
|
1048
1057
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1049
1058
|
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
@@ -1116,6 +1125,7 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1116
1125
|
) => Promise<TelegramLockedPollingStartResult>;
|
|
1117
1126
|
stop: () => Promise<string>;
|
|
1118
1127
|
suspend: () => Promise<void>;
|
|
1128
|
+
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
1119
1129
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
1120
1130
|
registerFollowerWithOwner?: (
|
|
1121
1131
|
ctx: TContext,
|
|
@@ -1130,6 +1140,7 @@ export interface TelegramLockedPollingRuntimeDeps<
|
|
|
1130
1140
|
lock: TelegramLockRuntime<TContext>;
|
|
1131
1141
|
hasBotToken: () => boolean;
|
|
1132
1142
|
canStartPolling?: (ctx: TContext) => boolean;
|
|
1143
|
+
isContextCurrent?: (ctx: TContext) => boolean;
|
|
1133
1144
|
formatStartBlockedMessage?: (ctx: TContext) => string;
|
|
1134
1145
|
startPolling: (
|
|
1135
1146
|
ctx: TContext,
|
|
@@ -1142,6 +1153,7 @@ export interface TelegramLockedPollingRuntimeDeps<
|
|
|
1142
1153
|
) => boolean | undefined | Promise<boolean | undefined>;
|
|
1143
1154
|
stopFollowerRegistration?: () => void;
|
|
1144
1155
|
onTransportAvailabilityChanged?: () => void;
|
|
1156
|
+
transportMonitor?: { start: (ctx: TContext) => void; stop: () => void };
|
|
1145
1157
|
updateStatus: (ctx: TContext) => void;
|
|
1146
1158
|
recordRuntimeEvent?: (
|
|
1147
1159
|
category: string,
|
|
@@ -1164,9 +1176,10 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1164
1176
|
let ownershipCheckInterval: ReturnType<typeof setInterval> | undefined;
|
|
1165
1177
|
let ownershipRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
1166
1178
|
let ownershipStop: Promise<void> | undefined;
|
|
1179
|
+
let activeContext: TContext | undefined;
|
|
1167
1180
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1168
1181
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1169
|
-
let
|
|
1182
|
+
let pollingGeneration = 0;
|
|
1170
1183
|
const ownershipCheckMs =
|
|
1171
1184
|
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1172
1185
|
const ownershipRefreshMs =
|
|
@@ -1178,7 +1191,9 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1178
1191
|
ownershipRefreshInterval = undefined;
|
|
1179
1192
|
};
|
|
1180
1193
|
const suspendPolling = async () => {
|
|
1181
|
-
|
|
1194
|
+
pollingGeneration += 1;
|
|
1195
|
+
activeContext = undefined;
|
|
1196
|
+
deps.transportMonitor?.stop();
|
|
1182
1197
|
deps.stopFollowerRegistration?.();
|
|
1183
1198
|
stopOwnershipWatcher();
|
|
1184
1199
|
if (sessionAutoStartRun) {
|
|
@@ -1192,6 +1207,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1192
1207
|
};
|
|
1193
1208
|
const stopAfterOwnershipLoss = () => {
|
|
1194
1209
|
if (ownershipStop) return;
|
|
1210
|
+
activeContext = undefined;
|
|
1211
|
+
deps.transportMonitor?.stop();
|
|
1195
1212
|
stopOwnershipWatcher();
|
|
1196
1213
|
deps.onTransportAvailabilityChanged?.();
|
|
1197
1214
|
ownershipStop = deps
|
|
@@ -1228,7 +1245,10 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1228
1245
|
const runOwnedPollingStart = async (
|
|
1229
1246
|
ctx: TContext,
|
|
1230
1247
|
options: TelegramLockedPollingStartOptions,
|
|
1248
|
+
isCurrent: () => boolean,
|
|
1231
1249
|
): Promise<boolean> => {
|
|
1250
|
+
if (!isCurrent()) return false;
|
|
1251
|
+
activeContext = ctx;
|
|
1232
1252
|
startOwnershipWatcher(ctx);
|
|
1233
1253
|
try {
|
|
1234
1254
|
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
@@ -1236,8 +1256,10 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1236
1256
|
return false;
|
|
1237
1257
|
}
|
|
1238
1258
|
await options.onAcquired?.();
|
|
1259
|
+
if (!isCurrent()) return false;
|
|
1239
1260
|
await deps.startPolling(ctx, options);
|
|
1240
1261
|
} catch (error) {
|
|
1262
|
+
if (!isCurrent()) return false;
|
|
1241
1263
|
stopOwnershipWatcher();
|
|
1242
1264
|
try {
|
|
1243
1265
|
await deps.stopPolling();
|
|
@@ -1246,14 +1268,22 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1246
1268
|
phase: "startup-rollback",
|
|
1247
1269
|
});
|
|
1248
1270
|
}
|
|
1271
|
+
if (!isCurrent()) return false;
|
|
1249
1272
|
deps.lock.release();
|
|
1250
1273
|
deps.onTransportAvailabilityChanged?.();
|
|
1251
1274
|
throw error;
|
|
1252
1275
|
}
|
|
1253
|
-
if (
|
|
1276
|
+
if (!isCurrent()) return false;
|
|
1277
|
+
if (deps.lock.owns(ctx)) {
|
|
1278
|
+
if (activeContext !== ctx) return false;
|
|
1279
|
+
deps.transportMonitor?.start(ctx);
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1254
1282
|
stopOwnershipWatcher();
|
|
1255
1283
|
if (ownershipStop) await ownershipStop;
|
|
1284
|
+
if (!isCurrent()) return false;
|
|
1256
1285
|
await deps.stopPolling();
|
|
1286
|
+
if (!isCurrent()) return false;
|
|
1257
1287
|
deps.onTransportAvailabilityChanged?.();
|
|
1258
1288
|
return false;
|
|
1259
1289
|
};
|
|
@@ -1270,6 +1300,17 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1270
1300
|
if (!canStartPolling(ctx)) {
|
|
1271
1301
|
return { ok: false, message: formatStartBlockedMessage(ctx) };
|
|
1272
1302
|
}
|
|
1303
|
+
const cancelled = {
|
|
1304
|
+
ok: false as const,
|
|
1305
|
+
canTakeover: false as const,
|
|
1306
|
+
message: "Telegram polling startup was cancelled or superseded.",
|
|
1307
|
+
};
|
|
1308
|
+
if (deps.isContextCurrent?.(ctx) === false) return cancelled;
|
|
1309
|
+
const generation = ++pollingGeneration;
|
|
1310
|
+
const isCurrent = () => generation === pollingGeneration &&
|
|
1311
|
+
(deps.isContextCurrent?.(ctx) ?? true);
|
|
1312
|
+
if (ownershipStop) await ownershipStop;
|
|
1313
|
+
if (!isCurrent()) return cancelled;
|
|
1273
1314
|
let acquired = deps.lock.acquire(ctx, {
|
|
1274
1315
|
force: options.force,
|
|
1275
1316
|
expectedOwner:
|
|
@@ -1306,6 +1347,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1306
1347
|
ctx,
|
|
1307
1348
|
acquired.lock,
|
|
1308
1349
|
);
|
|
1350
|
+
if (!isCurrent()) return cancelled;
|
|
1309
1351
|
if (registered) {
|
|
1310
1352
|
deps.updateStatus(ctx);
|
|
1311
1353
|
return { ok: true, canTakeover: false };
|
|
@@ -1337,13 +1379,15 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1337
1379
|
};
|
|
1338
1380
|
}
|
|
1339
1381
|
takeoverCandidate = undefined;
|
|
1340
|
-
if (!(await runOwnedPollingStart(ctx, options))) {
|
|
1382
|
+
if (!(await runOwnedPollingStart(ctx, options, isCurrent))) {
|
|
1383
|
+
if (!isCurrent()) return cancelled;
|
|
1341
1384
|
return {
|
|
1342
1385
|
ok: false,
|
|
1343
1386
|
canTakeover: false,
|
|
1344
1387
|
message: "Telegram leadership changed during polling startup.",
|
|
1345
1388
|
};
|
|
1346
1389
|
}
|
|
1390
|
+
if (!isCurrent()) return cancelled;
|
|
1347
1391
|
deps.onTransportAvailabilityChanged?.();
|
|
1348
1392
|
deps.updateStatus(ctx);
|
|
1349
1393
|
const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
|
|
@@ -1362,6 +1406,42 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1362
1406
|
return "Telegram bridge disconnected.";
|
|
1363
1407
|
},
|
|
1364
1408
|
suspend: suspendPolling,
|
|
1409
|
+
onPersistentConflict: async (ctx, count) => {
|
|
1410
|
+
if (activeContext === undefined || ownershipStop) return;
|
|
1411
|
+
if (!(deps.isContextCurrent?.(ctx) ?? activeContext === ctx)) return;
|
|
1412
|
+
activeContext = undefined;
|
|
1413
|
+
pollingGeneration += 1;
|
|
1414
|
+
stopOwnershipWatcher();
|
|
1415
|
+
deps.transportMonitor?.stop();
|
|
1416
|
+
let ownership = "unverifiable";
|
|
1417
|
+
const cleanupErrors: string[] = [];
|
|
1418
|
+
try {
|
|
1419
|
+
ownership = deps.lock.owns(snapshotLockContext(ctx)) ? "owned" : "lost";
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
cleanupErrors.push(String(error));
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
deps.lock.release();
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
ownership = "unverifiable";
|
|
1427
|
+
cleanupErrors.push(String(error));
|
|
1428
|
+
}
|
|
1429
|
+
ownershipStop = Promise.resolve()
|
|
1430
|
+
.then(() => deps.stopPolling())
|
|
1431
|
+
.catch((error) => { cleanupErrors.push(String(error)); })
|
|
1432
|
+
.finally(() => {
|
|
1433
|
+
ownershipStop = undefined;
|
|
1434
|
+
deps.recordRuntimeEvent?.("polling", ownership === "lost"
|
|
1435
|
+
? "Telegram transport stopped: local ownership lost; check for another Pi instance."
|
|
1436
|
+
: "Telegram transport stopped: competing getUpdates client or ownership mismatch.", {
|
|
1437
|
+
phase: "persistent-conflict", count, ownership,
|
|
1438
|
+
...(cleanupErrors.length ? { cleanupErrors } : {}),
|
|
1439
|
+
});
|
|
1440
|
+
deps.updateStatus(ctx);
|
|
1441
|
+
});
|
|
1442
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1443
|
+
await ownershipStop;
|
|
1444
|
+
},
|
|
1365
1445
|
onSessionStart: async (_event, ctx) => {
|
|
1366
1446
|
if (!deps.hasBotToken()) return;
|
|
1367
1447
|
if (!canStartPolling(ctx)) return;
|
|
@@ -1379,15 +1459,18 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1379
1459
|
) {
|
|
1380
1460
|
return;
|
|
1381
1461
|
}
|
|
1382
|
-
|
|
1383
|
-
const generation =
|
|
1462
|
+
if (deps.isContextCurrent?.(ctx) === false) return;
|
|
1463
|
+
const generation = ++pollingGeneration;
|
|
1464
|
+
const isCurrent = () => generation === pollingGeneration &&
|
|
1465
|
+
(deps.isContextCurrent?.(ctx) ?? true);
|
|
1384
1466
|
const startedAtMs = Date.now();
|
|
1385
1467
|
deps.recordRuntimeEvent?.("lock", "Telegram auto-start scheduled", {
|
|
1386
1468
|
phase: "auto-start-scheduled",
|
|
1387
1469
|
});
|
|
1388
1470
|
const run = (async () => {
|
|
1389
1471
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1390
|
-
if (
|
|
1472
|
+
if (ownershipStop) await ownershipStop;
|
|
1473
|
+
if (!isCurrent()) return;
|
|
1391
1474
|
if (canResumeStaleSameCwd || canHandoffSameProcess) {
|
|
1392
1475
|
const acquired = deps.lock.acquire(
|
|
1393
1476
|
ctx,
|
|
@@ -1397,9 +1480,9 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1397
1480
|
);
|
|
1398
1481
|
if (!acquired.ok) return;
|
|
1399
1482
|
}
|
|
1400
|
-
if (
|
|
1401
|
-
if (!(await runOwnedPollingStart(ctx, {}))) return;
|
|
1402
|
-
if (
|
|
1483
|
+
if (!isCurrent()) return;
|
|
1484
|
+
if (!(await runOwnedPollingStart(ctx, {}, isCurrent))) return;
|
|
1485
|
+
if (!isCurrent()) return;
|
|
1403
1486
|
deps.onTransportAvailabilityChanged?.();
|
|
1404
1487
|
deps.updateStatus(ctx);
|
|
1405
1488
|
deps.recordRuntimeEvent?.("lock", "Telegram auto-start completed", {
|
|
@@ -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.",
|