@llblab/pi-telegram 0.24.6 → 0.24.7
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/AGENTS.md +5 -5
- package/BACKLOG.md +0 -12
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/multi-instance-bus.md +7 -6
- package/docs/outbound.md +1 -0
- package/docs/public-api.md +1 -1
- package/docs/ui-style.md +4 -0
- package/index.ts +10 -0
- package/lib/bus-leader.ts +204 -59
- package/lib/bus.ts +6 -1
- package/lib/config.ts +1 -1
- package/lib/keyboard.ts +1 -0
- package/lib/menu-settings.ts +8 -12
- package/lib/outbound-buttons.ts +34 -2
- package/lib/outbound.ts +1 -0
- package/lib/routing.ts +25 -2
- package/lib/sync.ts +42 -20
- package/lib/thread-reconciler.ts +100 -2
- package/lib/threads.ts +95 -0
- package/package.json +3 -26
- package/scripts/audit-dependencies.ts +0 -78
- package/scripts/dependency-audit-policy.ts +0 -300
package/lib/bus-leader.ts
CHANGED
|
@@ -112,7 +112,10 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
112
112
|
export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
113
113
|
topicTargetStore: Pick<
|
|
114
114
|
Threads.TelegramTopicTargetStore,
|
|
115
|
-
|
|
115
|
+
| "markStaleByTarget"
|
|
116
|
+
| "persist"
|
|
117
|
+
| "upsertPendingCleanup"
|
|
118
|
+
| "removePendingCleanup"
|
|
116
119
|
>;
|
|
117
120
|
callApi: <TResponse>(
|
|
118
121
|
method: string,
|
|
@@ -155,6 +158,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
155
158
|
TelegramBusLeaderRuntimeDeps<TContext>,
|
|
156
159
|
| "callApi"
|
|
157
160
|
| "onFollowerDisconnected"
|
|
161
|
+
| "onFollowerConfirmedDead"
|
|
158
162
|
| "provisionFollowerTarget"
|
|
159
163
|
| "provisionLeaderTarget"
|
|
160
164
|
| "recordRuntimeEvent"
|
|
@@ -212,6 +216,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
212
216
|
onFollowerDisconnected: createTelegramBusFollowerDisconnectHandler({
|
|
213
217
|
...provisionerPorts,
|
|
214
218
|
}),
|
|
219
|
+
onFollowerConfirmedDead: createTelegramBusFollowerConfirmedDeadHandler({
|
|
220
|
+
...provisionerPorts,
|
|
221
|
+
}),
|
|
215
222
|
provisionFollowerTarget: createTelegramBusFollowerTargetProvisioner({
|
|
216
223
|
...provisionerPorts,
|
|
217
224
|
}),
|
|
@@ -259,9 +266,14 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
259
266
|
getNowMs?: () => number;
|
|
260
267
|
followerPruneIntervalMs?: number;
|
|
261
268
|
followerStaleAfterMs?: number;
|
|
269
|
+
isFollowerProcessAlive?: (pid: number) => boolean;
|
|
270
|
+
shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
|
|
262
271
|
onFollowerDisconnected?: (
|
|
263
272
|
follower: TelegramBusFollowerView,
|
|
264
273
|
) => Promise<void> | void;
|
|
274
|
+
onFollowerConfirmedDead?: (
|
|
275
|
+
follower: TelegramBusFollowerView,
|
|
276
|
+
) => Promise<void> | void;
|
|
265
277
|
recordRuntimeEvent?: (
|
|
266
278
|
category: string,
|
|
267
279
|
error: unknown,
|
|
@@ -678,8 +690,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
678
690
|
};
|
|
679
691
|
}
|
|
680
692
|
|
|
681
|
-
|
|
693
|
+
function createTelegramBusFollowerCleanupHandler(
|
|
682
694
|
deps: TelegramBusFollowerDisconnectHandlerDeps,
|
|
695
|
+
trigger: "graceful-disconnect" | "confirmed-dead",
|
|
683
696
|
): (follower: TelegramBusFollowerView) => Promise<void> {
|
|
684
697
|
return async (follower) => {
|
|
685
698
|
const target = follower.target;
|
|
@@ -688,14 +701,33 @@ export function createTelegramBusFollowerDisconnectHandler(
|
|
|
688
701
|
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
689
702
|
throw new Error("Follower disconnect cleanup requires leader ownership.");
|
|
690
703
|
}
|
|
704
|
+
if (!follower.registrationGeneration) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
"Follower disconnect cleanup requires an exact registration generation.",
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const intent: ThreadReconciler.TelegramThreadCleanupIntent = {
|
|
710
|
+
id: `cleanup:${follower.instanceId}:${follower.registrationGeneration}:${target.chatId}:${target.threadId}`,
|
|
711
|
+
owner: "manual-follower",
|
|
712
|
+
instanceId: follower.instanceId,
|
|
713
|
+
runtimeGeneration: follower.registrationGeneration,
|
|
714
|
+
...(follower.profileKey ? { profileKey: follower.profileKey } : {}),
|
|
715
|
+
target: { chatId: target.chatId, threadId: target.threadId },
|
|
716
|
+
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
717
|
+
};
|
|
718
|
+
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
719
|
+
await deps.topicTargetStore.persist();
|
|
691
720
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
692
|
-
ThreadReconciler.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
721
|
+
ThreadReconciler.planThreadReconciliation({
|
|
722
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
723
|
+
currentLeaderEpoch: leaderEpoch,
|
|
724
|
+
records: [],
|
|
725
|
+
pendingCleanups: [intent],
|
|
696
726
|
}),
|
|
697
727
|
{
|
|
698
728
|
callApi: deps.callApi,
|
|
729
|
+
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
730
|
+
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
699
731
|
persist: deps.topicTargetStore.persist,
|
|
700
732
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
701
733
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -703,7 +735,7 @@ export function createTelegramBusFollowerDisconnectHandler(
|
|
|
703
735
|
);
|
|
704
736
|
if (cleanup.incompleteActions?.length) {
|
|
705
737
|
throw new Error(
|
|
706
|
-
"Telegram follower thread deletion was not confirmed; reconnect the leader
|
|
738
|
+
"Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.",
|
|
707
739
|
);
|
|
708
740
|
}
|
|
709
741
|
if (
|
|
@@ -712,24 +744,45 @@ export function createTelegramBusFollowerDisconnectHandler(
|
|
|
712
744
|
) {
|
|
713
745
|
throw new Error("Follower disconnect cleanup lost leader ownership.");
|
|
714
746
|
}
|
|
715
|
-
const changed =
|
|
716
|
-
deps.topicTargetStore.markOfflineByInstanceId(follower.instanceId) > 0;
|
|
717
|
-
if (changed) await deps.topicTargetStore.persist();
|
|
718
747
|
deps.setSyncState(
|
|
719
748
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
720
749
|
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
721
|
-
action:
|
|
750
|
+
action:
|
|
751
|
+
trigger === "confirmed-dead"
|
|
752
|
+
? "manual-follower-confirmed-dead"
|
|
753
|
+
: "manual-follower-disconnect",
|
|
722
754
|
}),
|
|
723
755
|
);
|
|
724
|
-
deps.recordRuntimeEvent(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
756
|
+
deps.recordRuntimeEvent(
|
|
757
|
+
"bus",
|
|
758
|
+
trigger === "confirmed-dead"
|
|
759
|
+
? "Confirmed-dead Telegram bus follower thread cleaned up"
|
|
760
|
+
: "Telegram bus follower disconnected",
|
|
761
|
+
{
|
|
762
|
+
phase:
|
|
763
|
+
trigger === "confirmed-dead"
|
|
764
|
+
? "follower-confirmed-dead-cleanup"
|
|
765
|
+
: "follower-disconnect",
|
|
766
|
+
instanceId: follower.instanceId,
|
|
767
|
+
chatId: target.chatId,
|
|
768
|
+
threadId: target.threadId,
|
|
769
|
+
},
|
|
770
|
+
);
|
|
730
771
|
};
|
|
731
772
|
}
|
|
732
773
|
|
|
774
|
+
export function createTelegramBusFollowerDisconnectHandler(
|
|
775
|
+
deps: TelegramBusFollowerDisconnectHandlerDeps,
|
|
776
|
+
): (follower: TelegramBusFollowerView) => Promise<void> {
|
|
777
|
+
return createTelegramBusFollowerCleanupHandler(deps, "graceful-disconnect");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function createTelegramBusFollowerConfirmedDeadHandler(
|
|
781
|
+
deps: TelegramBusFollowerDisconnectHandlerDeps,
|
|
782
|
+
): (follower: TelegramBusFollowerView) => Promise<void> {
|
|
783
|
+
return createTelegramBusFollowerCleanupHandler(deps, "confirmed-dead");
|
|
784
|
+
}
|
|
785
|
+
|
|
733
786
|
export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
734
787
|
deps: TelegramBusLeaderTargetProvisionerDeps<TContext>,
|
|
735
788
|
): (ctx: TContext) => Promise<void> {
|
|
@@ -741,6 +794,23 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
741
794
|
"Telegram leader target provisioning requires ownership.",
|
|
742
795
|
);
|
|
743
796
|
}
|
|
797
|
+
await deps.topicTargetStore.load();
|
|
798
|
+
const pendingCleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
799
|
+
nowMs: getNowMs(),
|
|
800
|
+
currentLeaderEpoch: leaderEpoch,
|
|
801
|
+
previousState: deps.getThreadReconciliationMachineState?.(),
|
|
802
|
+
records: deps.topicTargetStore.list(),
|
|
803
|
+
pendingCleanups: deps.topicTargetStore.listPendingCleanups(),
|
|
804
|
+
});
|
|
805
|
+
deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
|
|
806
|
+
await ThreadReconciler.applyThreadReconciliationPlan(pendingCleanupPlan, {
|
|
807
|
+
callApi: deps.callApi,
|
|
808
|
+
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
809
|
+
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
810
|
+
persist: deps.topicTargetStore.persist,
|
|
811
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
812
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
813
|
+
});
|
|
744
814
|
deps.onProvisioningStart?.();
|
|
745
815
|
let ownTarget: Threads.TelegramOwnTopicProvisionResult | undefined;
|
|
746
816
|
try {
|
|
@@ -858,6 +928,33 @@ export function createTelegramBusLeaderApiProxy(
|
|
|
858
928
|
};
|
|
859
929
|
}
|
|
860
930
|
|
|
931
|
+
type TelegramBusFollowerMutationRunner = <T>(
|
|
932
|
+
follower: { instanceId: string; profileKey?: string },
|
|
933
|
+
operation: () => Promise<T>,
|
|
934
|
+
) => Promise<T>;
|
|
935
|
+
|
|
936
|
+
function createTelegramBusFollowerMutationRunner(): TelegramBusFollowerMutationRunner {
|
|
937
|
+
const tails = new Map<string, Promise<void>>();
|
|
938
|
+
return async (follower, operation) => {
|
|
939
|
+
const key = follower.profileKey
|
|
940
|
+
? `profile:${follower.profileKey}`
|
|
941
|
+
: `instance:${follower.instanceId}`;
|
|
942
|
+
const previous = tails.get(key);
|
|
943
|
+
let release!: () => void;
|
|
944
|
+
const current = new Promise<void>((resolve) => {
|
|
945
|
+
release = resolve;
|
|
946
|
+
});
|
|
947
|
+
tails.set(key, current);
|
|
948
|
+
if (previous) await previous;
|
|
949
|
+
try {
|
|
950
|
+
return await operation();
|
|
951
|
+
} finally {
|
|
952
|
+
release();
|
|
953
|
+
if (tails.get(key) === current) tails.delete(key);
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
861
958
|
export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
862
959
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
863
960
|
authSecret?: string;
|
|
@@ -883,38 +980,13 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
883
980
|
follower: TelegramBusFollowerView,
|
|
884
981
|
) => Promise<void> | void;
|
|
885
982
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
983
|
+
runFollowerMutation?: TelegramBusFollowerMutationRunner;
|
|
886
984
|
}): (
|
|
887
985
|
envelope: TelegramBusEnvelope,
|
|
888
986
|
) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
|
|
889
987
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
instanceId: string;
|
|
893
|
-
profileKey?: string;
|
|
894
|
-
}): string =>
|
|
895
|
-
follower.profileKey
|
|
896
|
-
? `profile:${follower.profileKey}`
|
|
897
|
-
: `instance:${follower.instanceId}`;
|
|
898
|
-
const runFollowerMutation = async <T>(
|
|
899
|
-
mutationKey: string,
|
|
900
|
-
operation: () => Promise<T>,
|
|
901
|
-
): Promise<T> => {
|
|
902
|
-
const previous = followerMutationTails.get(mutationKey);
|
|
903
|
-
let release!: () => void;
|
|
904
|
-
const current = new Promise<void>((resolve) => {
|
|
905
|
-
release = resolve;
|
|
906
|
-
});
|
|
907
|
-
followerMutationTails.set(mutationKey, current);
|
|
908
|
-
if (previous) await previous;
|
|
909
|
-
try {
|
|
910
|
-
return await operation();
|
|
911
|
-
} finally {
|
|
912
|
-
release();
|
|
913
|
-
if (followerMutationTails.get(mutationKey) === current) {
|
|
914
|
-
followerMutationTails.delete(mutationKey);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
};
|
|
988
|
+
const runFollowerMutation =
|
|
989
|
+
deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
|
|
918
990
|
const forwardToFollower = async (
|
|
919
991
|
envelope: Extract<
|
|
920
992
|
TelegramBusEnvelope,
|
|
@@ -977,9 +1049,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
977
1049
|
}
|
|
978
1050
|
switch (envelope.kind) {
|
|
979
1051
|
case "follower.register": {
|
|
980
|
-
return runFollowerMutation(
|
|
981
|
-
getFollowerMutationKey(envelope.registration),
|
|
982
|
-
async () => {
|
|
1052
|
+
return runFollowerMutation(envelope.registration, async () => {
|
|
983
1053
|
try {
|
|
984
1054
|
if (!envelope.registration.registrationGeneration) {
|
|
985
1055
|
throw new Error(
|
|
@@ -1041,9 +1111,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1041
1111
|
case "follower.disconnect": {
|
|
1042
1112
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1043
1113
|
return runFollowerMutation(
|
|
1044
|
-
|
|
1045
|
-
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
1046
|
-
),
|
|
1114
|
+
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
1047
1115
|
async () => {
|
|
1048
1116
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1049
1117
|
if (!follower) {
|
|
@@ -1368,6 +1436,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1368
1436
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1369
1437
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
1370
1438
|
const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
|
|
1439
|
+
const runFollowerMutation = createTelegramBusFollowerMutationRunner();
|
|
1371
1440
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
1372
1441
|
const stopPruning = () => {
|
|
1373
1442
|
if (!pruneInterval) return;
|
|
@@ -1387,14 +1456,87 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1387
1456
|
followerStaleAfterMs,
|
|
1388
1457
|
);
|
|
1389
1458
|
for (const follower of removed) {
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1459
|
+
let processConfirmedDead = false;
|
|
1460
|
+
if (follower.pid !== undefined && deps.isFollowerProcessAlive) {
|
|
1461
|
+
try {
|
|
1462
|
+
processConfirmedDead = !deps.isFollowerProcessAlive(follower.pid);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1465
|
+
phase: "follower-process-liveness",
|
|
1466
|
+
instanceId: follower.instanceId,
|
|
1467
|
+
pid: follower.pid,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (!processConfirmedDead) {
|
|
1472
|
+
deps.recordRuntimeEvent?.(
|
|
1473
|
+
"bus",
|
|
1474
|
+
"Telegram bus follower heartbeat stale; preserving thread binding",
|
|
1475
|
+
{
|
|
1476
|
+
phase: "follower-pruned",
|
|
1477
|
+
instanceId: follower.instanceId,
|
|
1478
|
+
processLiveness:
|
|
1479
|
+
follower.pid === undefined || !deps.isFollowerProcessAlive
|
|
1480
|
+
? "unknown"
|
|
1481
|
+
: "alive-or-unknown",
|
|
1482
|
+
},
|
|
1483
|
+
);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
let cleanupEnabled = false;
|
|
1487
|
+
try {
|
|
1488
|
+
cleanupEnabled =
|
|
1489
|
+
(await deps.shouldCleanupConfirmedDeadFollower?.()) ?? false;
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1492
|
+
phase: "follower-confirmed-dead-cleanup-policy",
|
|
1395
1493
|
instanceId: follower.instanceId,
|
|
1396
|
-
|
|
1397
|
-
|
|
1494
|
+
pid: follower.pid,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (!cleanupEnabled || !deps.onFollowerConfirmedDead) {
|
|
1498
|
+
deps.recordRuntimeEvent?.(
|
|
1499
|
+
"bus",
|
|
1500
|
+
"Telegram bus follower process confirmed dead; preserving thread binding",
|
|
1501
|
+
{
|
|
1502
|
+
phase: "follower-confirmed-dead-preserved",
|
|
1503
|
+
instanceId: follower.instanceId,
|
|
1504
|
+
pid: follower.pid,
|
|
1505
|
+
cleanupEnabled,
|
|
1506
|
+
},
|
|
1507
|
+
);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
await runFollowerMutation(follower, async () => {
|
|
1512
|
+
const replacement = deps.followerRegistry.list().find((candidate) =>
|
|
1513
|
+
follower.profileKey
|
|
1514
|
+
? candidate.profileKey === follower.profileKey
|
|
1515
|
+
: candidate.instanceId === follower.instanceId,
|
|
1516
|
+
);
|
|
1517
|
+
if (replacement) {
|
|
1518
|
+
deps.recordRuntimeEvent?.(
|
|
1519
|
+
"bus",
|
|
1520
|
+
"Telegram bus follower replaced before confirmed-dead cleanup; preserving thread binding",
|
|
1521
|
+
{
|
|
1522
|
+
phase: "follower-confirmed-dead-replaced",
|
|
1523
|
+
instanceId: follower.instanceId,
|
|
1524
|
+
replacementInstanceId: replacement.instanceId,
|
|
1525
|
+
},
|
|
1526
|
+
);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
await deps.onFollowerConfirmedDead!(follower);
|
|
1530
|
+
});
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1533
|
+
phase: "follower-confirmed-dead-cleanup",
|
|
1534
|
+
instanceId: follower.instanceId,
|
|
1535
|
+
pid: follower.pid,
|
|
1536
|
+
chatId: follower.target?.chatId,
|
|
1537
|
+
threadId: follower.target?.threadId,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1398
1540
|
}
|
|
1399
1541
|
};
|
|
1400
1542
|
const startPruning = () => {
|
|
@@ -1423,14 +1565,17 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1423
1565
|
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
1424
1566
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
1425
1567
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1568
|
+
runFollowerMutation,
|
|
1426
1569
|
}),
|
|
1427
1570
|
});
|
|
1428
1571
|
return {
|
|
1429
1572
|
startPolling: async (ctx) => {
|
|
1573
|
+
// Replay durable cleanup before publishing the follower endpoint so a
|
|
1574
|
+
// replacement registration cannot reclaim a target while it is deleted.
|
|
1575
|
+
await deps.provisionLeaderTarget?.(ctx);
|
|
1430
1576
|
await localServer.start();
|
|
1431
1577
|
startPruning();
|
|
1432
1578
|
try {
|
|
1433
|
-
await deps.provisionLeaderTarget?.(ctx);
|
|
1434
1579
|
await deps.startPolling(ctx);
|
|
1435
1580
|
} catch (error) {
|
|
1436
1581
|
stopPruning();
|
package/lib/bus.ts
CHANGED
|
@@ -218,6 +218,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
218
218
|
"deleteForumTopic",
|
|
219
219
|
"deleteMessage",
|
|
220
220
|
"editForumTopic",
|
|
221
|
+
"editMessageReplyMarkup",
|
|
221
222
|
"editMessageText",
|
|
222
223
|
"sendChatAction",
|
|
223
224
|
"sendMessage",
|
|
@@ -296,7 +297,11 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
296
297
|
const body = input.args[1] as Record<string, unknown>;
|
|
297
298
|
return body.message_thread_id === undefined && isTargetChatScoped(body);
|
|
298
299
|
}
|
|
299
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
apiMethod === "deleteMessage" ||
|
|
302
|
+
apiMethod === "editMessageReplyMarkup" ||
|
|
303
|
+
apiMethod === "editMessageText"
|
|
304
|
+
) {
|
|
300
305
|
if (!isTargetMessageScoped(input.args[1])) return false;
|
|
301
306
|
const body = input.args[1] as Record<string, unknown>;
|
|
302
307
|
const messageId =
|
package/lib/config.ts
CHANGED
|
@@ -885,7 +885,7 @@ export function createTelegramAutomaticThreadCleanupResolver(
|
|
|
885
885
|
await loadLatestTelegramConfig(configStore);
|
|
886
886
|
if (configStore.didLastLoadRecoverInvalidConfig?.()) {
|
|
887
887
|
throw new Error(
|
|
888
|
-
"
|
|
888
|
+
"Thread cleanup setting is unavailable after invalid Telegram config recovery.",
|
|
889
889
|
);
|
|
890
890
|
}
|
|
891
891
|
return createTelegramAutomaticThreadCleanupChecker(configStore)();
|
package/lib/keyboard.ts
CHANGED
package/lib/menu-settings.ts
CHANGED
|
@@ -133,7 +133,7 @@ export interface TelegramSettingsMenuRuntimeDeps<
|
|
|
133
133
|
|
|
134
134
|
export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
|
|
135
135
|
export const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
|
|
136
|
-
"<b>🧹
|
|
136
|
+
"<b>🧹 Thread cleanup:</b>";
|
|
137
137
|
export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
|
|
138
138
|
export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
|
|
139
139
|
export const ASSISTANT_RENDERING_SETTINGS_TITLE =
|
|
@@ -172,7 +172,7 @@ export function buildAutomaticThreadCleanupSettingsText(
|
|
|
172
172
|
"Delete this Pi instance's Telegram tab when Pi quits normally.",
|
|
173
173
|
"",
|
|
174
174
|
"<code>-</code> <code>on</code> (default): delete the bound thread and release Telegram authority on graceful quit.",
|
|
175
|
-
"<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual
|
|
175
|
+
"<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual <code>/telegram-disconnect</code> still confirms and deletes it.",
|
|
176
176
|
].join("\n");
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -290,7 +290,7 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
290
290
|
rows.push(
|
|
291
291
|
[
|
|
292
292
|
{
|
|
293
|
-
text: `🧹
|
|
293
|
+
text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
|
|
294
294
|
callback_data: "settings:open:automatic-thread-cleanup",
|
|
295
295
|
},
|
|
296
296
|
],
|
|
@@ -669,7 +669,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
669
669
|
await updateAutomaticThreadCleanupSettingsMessage(deps);
|
|
670
670
|
await deps.answerCallbackQuery(
|
|
671
671
|
callbackQueryId,
|
|
672
|
-
`
|
|
672
|
+
`Thread cleanup ${enabled ? "enabled" : "disabled"}`,
|
|
673
673
|
);
|
|
674
674
|
return true;
|
|
675
675
|
}
|
|
@@ -709,8 +709,7 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
709
709
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
710
710
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
711
711
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
712
|
-
isAutomaticThreadCleanupEnabled:
|
|
713
|
-
deps.isAutomaticThreadCleanupEnabled,
|
|
712
|
+
isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
|
|
714
713
|
sendSettingsMenu: (state, text, replyMarkup) =>
|
|
715
714
|
deps.sendInteractiveMessage(
|
|
716
715
|
state.chatId,
|
|
@@ -733,8 +732,7 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
733
732
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
734
733
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
735
734
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
736
|
-
isAutomaticThreadCleanupEnabled:
|
|
737
|
-
deps.isAutomaticThreadCleanupEnabled,
|
|
735
|
+
isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
|
|
738
736
|
updateSettingsMessage: (text, replyMarkup) =>
|
|
739
737
|
deps.editInteractiveMessage(
|
|
740
738
|
state.chatId,
|
|
@@ -777,15 +775,13 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
777
775
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
778
776
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
779
777
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
780
|
-
isAutomaticThreadCleanupEnabled:
|
|
781
|
-
deps.isAutomaticThreadCleanupEnabled,
|
|
778
|
+
isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
|
|
782
779
|
setProactivePushEnabled: deps.setProactivePushEnabled,
|
|
783
780
|
setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
|
|
784
781
|
setAssistantRenderingMode: deps.setAssistantRenderingMode,
|
|
785
782
|
setVoiceReplyMode: deps.setVoiceReplyMode,
|
|
786
783
|
setTimeInjectionMode: deps.setTimeInjectionMode,
|
|
787
|
-
setAutomaticThreadCleanupEnabled:
|
|
788
|
-
deps.setAutomaticThreadCleanupEnabled,
|
|
784
|
+
setAutomaticThreadCleanupEnabled: deps.setAutomaticThreadCleanupEnabled,
|
|
789
785
|
updateSettingsMessage: (text, replyMarkup) =>
|
|
790
786
|
deps.editInteractiveMessage(
|
|
791
787
|
state.chatId,
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface TelegramButtonCallbackQuery {
|
|
|
51
51
|
message_id?: number;
|
|
52
52
|
message_thread_id?: number;
|
|
53
53
|
chat?: { id?: number };
|
|
54
|
+
reply_markup?: TelegramOutboundButtonMarkup;
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -66,7 +67,12 @@ export interface TelegramButtonCallbackHandlerDeps<TContext = unknown> {
|
|
|
66
67
|
query: TelegramButtonCallbackQuery,
|
|
67
68
|
action: TelegramOutboundButtonAction,
|
|
68
69
|
ctx: TContext,
|
|
69
|
-
) => void;
|
|
70
|
+
) => boolean | void;
|
|
71
|
+
editMessageReplyMarkup?: (
|
|
72
|
+
chatId: number,
|
|
73
|
+
messageId: number,
|
|
74
|
+
replyMarkup: TelegramOutboundButtonMarkup,
|
|
75
|
+
) => Promise<void>;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
function nowMs(): number {
|
|
@@ -203,6 +209,21 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
203
209
|
};
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
export function markTelegramButtonSelected(
|
|
213
|
+
replyMarkup: TelegramOutboundButtonMarkup,
|
|
214
|
+
callbackData: string,
|
|
215
|
+
): TelegramOutboundButtonMarkup | undefined {
|
|
216
|
+
let matched = false;
|
|
217
|
+
const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
|
|
218
|
+
row.map((button) => {
|
|
219
|
+
if (button.callback_data !== callbackData) return { ...button };
|
|
220
|
+
matched = true;
|
|
221
|
+
return { ...button, style: "success" as const };
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
return matched ? { inline_keyboard: inlineKeyboard } : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
|
|
207
228
|
query: TelegramButtonCallbackQuery,
|
|
208
229
|
ctx: TContext,
|
|
@@ -225,7 +246,18 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
|
|
|
225
246
|
return true;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
deps.enqueueButtonPrompt(query, action, ctx);
|
|
249
|
+
const enqueued = deps.enqueueButtonPrompt(query, action, ctx);
|
|
250
|
+
if (enqueued === false) {
|
|
251
|
+
await deps.answerCallbackQuery(query.id, "Already queued.");
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
const selectedMarkup =
|
|
255
|
+
query.data && query.message?.reply_markup
|
|
256
|
+
? markTelegramButtonSelected(query.message.reply_markup, query.data)
|
|
257
|
+
: undefined;
|
|
258
|
+
if (selectedMarkup && deps.editMessageReplyMarkup) {
|
|
259
|
+
await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
|
|
260
|
+
}
|
|
229
261
|
await deps.answerCallbackQuery(query.id, "Queued.");
|
|
230
262
|
return true;
|
|
231
263
|
}
|
package/lib/outbound.ts
CHANGED
|
@@ -817,6 +817,7 @@ export {
|
|
|
817
817
|
createTelegramButtonPromptTurn,
|
|
818
818
|
createTelegramButtonReplyPlanner,
|
|
819
819
|
handleTelegramButtonCallbackQuery,
|
|
820
|
+
markTelegramButtonSelected,
|
|
820
821
|
planTelegramButtonReply,
|
|
821
822
|
type TelegramButtonActionStore,
|
|
822
823
|
type TelegramButtonCallbackHandlerDeps,
|
package/lib/routing.ts
CHANGED
|
@@ -608,6 +608,11 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
608
608
|
mode: "markdown" | "html" | "plain",
|
|
609
609
|
replyMarkup: Menu.TelegramReplyMarkup,
|
|
610
610
|
) => Promise<void>;
|
|
611
|
+
editMessageReplyMarkup?: (
|
|
612
|
+
chatId: number,
|
|
613
|
+
messageId: number,
|
|
614
|
+
replyMarkup: OutboundHandlers.TelegramOutboundButtonMarkup,
|
|
615
|
+
) => Promise<void>;
|
|
611
616
|
sendInteractiveMessage?: (
|
|
612
617
|
chatId: number,
|
|
613
618
|
text: string,
|
|
@@ -1448,11 +1453,28 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1448
1453
|
{
|
|
1449
1454
|
resolveAction: deps.buttonActionStore.resolve,
|
|
1450
1455
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
1456
|
+
editMessageReplyMarkup: deps.editMessageReplyMarkup
|
|
1457
|
+
? async (chatId, messageId, replyMarkup) => {
|
|
1458
|
+
try {
|
|
1459
|
+
await deps.editMessageReplyMarkup?.(
|
|
1460
|
+
chatId,
|
|
1461
|
+
messageId,
|
|
1462
|
+
replyMarkup,
|
|
1463
|
+
);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
deps.recordRuntimeEvent?.("telegram", error, {
|
|
1466
|
+
phase: "button-selection-mark",
|
|
1467
|
+
chatId,
|
|
1468
|
+
messageId,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
: undefined,
|
|
1451
1473
|
enqueueButtonPrompt: (buttonQuery, action, context) => {
|
|
1452
1474
|
const chatId = buttonQuery.message?.chat?.id;
|
|
1453
1475
|
const messageId = buttonQuery.message?.message_id;
|
|
1454
1476
|
if (typeof chatId !== "number" || typeof messageId !== "number")
|
|
1455
|
-
return;
|
|
1477
|
+
return false;
|
|
1456
1478
|
const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
1457
1479
|
const turn = OutboundHandlers.createTelegramButtonPromptTurn({
|
|
1458
1480
|
chatId,
|
|
@@ -1471,10 +1493,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1471
1493
|
deps.telegramQueueStore.getQueuedItems(),
|
|
1472
1494
|
turn,
|
|
1473
1495
|
);
|
|
1474
|
-
if (!result.appended) return;
|
|
1496
|
+
if (!result.appended) return false;
|
|
1475
1497
|
deps.telegramQueueStore.setQueuedItems(result.items);
|
|
1476
1498
|
deps.updateStatus(context);
|
|
1477
1499
|
requestDispatchNextQueuedTelegramTurn(context);
|
|
1500
|
+
return true;
|
|
1478
1501
|
},
|
|
1479
1502
|
},
|
|
1480
1503
|
);
|