@llblab/pi-telegram 0.19.3 → 0.20.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/AGENTS.md +6 -3
- package/BACKLOG.md +7 -6
- package/CHANGELOG.md +14 -2
- package/README.md +8 -5
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -1
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +2 -0
- package/docs/outbound.md +5 -5
- package/docs/public-api.md +1 -1
- package/index.ts +95 -26
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +37 -13
- package/lib/bus-leader.ts +28 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +66 -43
- package/lib/commands.ts +41 -8
- package/lib/config.ts +165 -26
- package/lib/locks.ts +78 -25
- package/lib/{runtime-log.ts → logs.ts} +58 -27
- package/lib/media.ts +97 -4
- package/lib/outbound-buttons.ts +1 -1
- package/lib/outbound.ts +5 -8
- package/lib/paths.ts +77 -0
- package/lib/prompt-templates.ts +3 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +32 -1
- package/lib/routing.ts +106 -41
- package/lib/status.ts +6 -1
- package/lib/sync.ts +22 -17
- package/lib/telegram-api.ts +13 -14
- package/lib/threads.ts +142 -55
- package/lib/turns.ts +47 -13
- package/package.json +3 -3
- /package/{banner.png → screenshot.png} +0 -0
package/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import * as Queue from "./lib/queue.ts";
|
|
|
32
32
|
import * as Replies from "./lib/replies.ts";
|
|
33
33
|
import * as Routing from "./lib/routing.ts";
|
|
34
34
|
import * as Runtime from "./lib/runtime.ts";
|
|
35
|
-
import * as
|
|
35
|
+
import * as Logs from "./lib/logs.ts";
|
|
36
36
|
import * as Sections from "./lib/sections.ts";
|
|
37
37
|
import * as Status from "./lib/status.ts";
|
|
38
38
|
import * as Sync from "./lib/sync.ts";
|
|
@@ -58,16 +58,40 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
58
58
|
} = piRuntime;
|
|
59
59
|
const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
|
|
60
60
|
const telegramInstanceId = `${process.pid}:${Date.now()}`;
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Manual follower identity must survive a Pi process reload in the same
|
|
62
|
+
// terminal. The process id changes on reload, but the parent shell/terminal id
|
|
63
|
+
// is stable enough to keep the follower bound to its existing Telegram thread.
|
|
64
|
+
const telegramManualFollowerOwnerId = String(process.ppid || process.pid);
|
|
65
|
+
const getActiveTelegramThreadProfile = function (): string | undefined {
|
|
66
|
+
return configStore.getActiveProfileName();
|
|
67
|
+
};
|
|
68
|
+
const getTelegramManualFollowerProfileKey = function (): string {
|
|
69
|
+
return Threads.getTelegramThreadOwnerKey({
|
|
70
|
+
kind: "manual-follower",
|
|
71
|
+
instanceId: telegramManualFollowerOwnerId,
|
|
72
|
+
telegramProfile: getActiveTelegramThreadProfile(),
|
|
73
|
+
});
|
|
74
|
+
};
|
|
63
75
|
const telegramBusAuthSecret = Bus.createTelegramBusAuthSecret();
|
|
64
76
|
let telegramActiveBusAuthSecret: string | undefined;
|
|
65
77
|
let telegramBusLifecycleOverridePhase:
|
|
66
78
|
| Status.TelegramBridgeBusLifecyclePhase
|
|
67
79
|
| undefined;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const getTelegramBusSocketPath = function (): string {
|
|
81
|
+
return Bus.getTelegramBusSocketPath(
|
|
82
|
+
undefined,
|
|
83
|
+
undefined,
|
|
84
|
+
getActiveTelegramThreadProfile(),
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
const getTelegramBusFollowerSocketPath = function (): string {
|
|
88
|
+
return Bus.getTelegramBusFollowerSocketPath(
|
|
89
|
+
telegramInstanceId,
|
|
90
|
+
undefined,
|
|
91
|
+
undefined,
|
|
92
|
+
getActiveTelegramThreadProfile(),
|
|
93
|
+
);
|
|
94
|
+
};
|
|
71
95
|
let telegramBusRequestSequence = 0;
|
|
72
96
|
const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
|
|
73
97
|
const telegramBusFollowerRegistrationState =
|
|
@@ -85,7 +109,23 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
85
109
|
return configStoreForRedaction?.getBotToken();
|
|
86
110
|
},
|
|
87
111
|
});
|
|
88
|
-
|
|
112
|
+
let getRuntimeLogProfileName = function (): string | undefined {
|
|
113
|
+
return undefined;
|
|
114
|
+
};
|
|
115
|
+
const runtimeJsonlLog = Logs.createTelegramRuntimeJsonlLog({
|
|
116
|
+
path: function () {
|
|
117
|
+
return Logs.getTelegramRuntimeLogPath(
|
|
118
|
+
undefined,
|
|
119
|
+
getRuntimeLogProfileName(),
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
previousPath: function () {
|
|
123
|
+
return Logs.getTelegramPreviousRuntimeLogPath(
|
|
124
|
+
undefined,
|
|
125
|
+
getRuntimeLogProfileName(),
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
89
129
|
runtimeJsonlLog.reset("extension-start", {
|
|
90
130
|
instanceId: telegramInstanceId,
|
|
91
131
|
pid: process.pid,
|
|
@@ -103,6 +143,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
103
143
|
};
|
|
104
144
|
const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
|
|
105
145
|
configStoreForRedaction = configStore;
|
|
146
|
+
getRuntimeLogProfileName = configStore.getActiveProfileName;
|
|
106
147
|
const isTelegramBusConfigured = function (): boolean {
|
|
107
148
|
return true;
|
|
108
149
|
};
|
|
@@ -112,9 +153,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
112
153
|
Config.bindGlobalTelegramConfigRuntime(configStore);
|
|
113
154
|
const configControls = Config.createTelegramConfigControls(configStore);
|
|
114
155
|
const threadStore = Threads.createTelegramTopicTargetStore({
|
|
115
|
-
path:
|
|
156
|
+
path: function () {
|
|
157
|
+
return Threads.getTelegramTopicTargetsPath(
|
|
158
|
+
undefined,
|
|
159
|
+
configStore.getActiveProfileName(),
|
|
160
|
+
);
|
|
161
|
+
},
|
|
116
162
|
});
|
|
117
163
|
const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>({
|
|
164
|
+
key: Locks.createTelegramLockKeyResolver(configStore),
|
|
118
165
|
instanceId: telegramInstanceId,
|
|
119
166
|
busSecret: telegramBusAuthSecret,
|
|
120
167
|
staleHeartbeatMs: Locks.TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
@@ -201,7 +248,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
201
248
|
await configStore.persist();
|
|
202
249
|
markTelegramConfigSyncChange("config-persist");
|
|
203
250
|
};
|
|
204
|
-
const
|
|
251
|
+
const findCurrentThreadRecord = function ():
|
|
205
252
|
| Threads.TelegramTopicTargetRecord
|
|
206
253
|
| undefined {
|
|
207
254
|
return Threads.findCurrentTelegramInstanceThreadRecord({
|
|
@@ -213,11 +260,24 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
213
260
|
telegramBusLeaderTarget,
|
|
214
261
|
});
|
|
215
262
|
};
|
|
263
|
+
const getCurrentThreadRecord = function ():
|
|
264
|
+
| Threads.TelegramTopicTargetRecord
|
|
265
|
+
| undefined {
|
|
266
|
+
const record = findCurrentThreadRecord();
|
|
267
|
+
if (
|
|
268
|
+
record?.owner?.kind === "manual-follower" &&
|
|
269
|
+
!telegramBusFollowerRegistrationState.isRegistered()
|
|
270
|
+
) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return record;
|
|
274
|
+
};
|
|
216
275
|
const statusRuntime = Status.createTelegramBridgeStatusRuntime<
|
|
217
276
|
Pi.ExtensionContext,
|
|
218
277
|
Queue.TelegramQueueItem<Pi.ExtensionContext>
|
|
219
278
|
>({
|
|
220
279
|
getConfig: configStore.get,
|
|
280
|
+
getActiveProfileName: configStore.getActiveProfileName,
|
|
221
281
|
isPollingActive: Polling.createTelegramPollingActivityReader(
|
|
222
282
|
pollingControllerState,
|
|
223
283
|
),
|
|
@@ -252,12 +312,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
252
312
|
},
|
|
253
313
|
getLocalBus() {
|
|
254
314
|
return {
|
|
255
|
-
leaderSocketPath:
|
|
256
|
-
leaderTransport:
|
|
257
|
-
|
|
258
|
-
|
|
315
|
+
leaderSocketPath: getTelegramBusSocketPath(),
|
|
316
|
+
leaderTransport: BusTransport.getTelegramBusTransportKind(
|
|
317
|
+
getTelegramBusSocketPath(),
|
|
318
|
+
),
|
|
319
|
+
followerSocketPath: getTelegramBusFollowerSocketPath(),
|
|
259
320
|
followerTransport: BusTransport.getTelegramBusTransportKind(
|
|
260
|
-
|
|
321
|
+
getTelegramBusFollowerSocketPath(),
|
|
261
322
|
),
|
|
262
323
|
followerRegistered: telegramBusFollowerRegistrationState.isRegistered(),
|
|
263
324
|
followerTarget: telegramBusFollowerRegistrationState.getTarget(),
|
|
@@ -302,15 +363,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
302
363
|
},
|
|
303
364
|
});
|
|
304
365
|
const { updateStatus: updateStatusLine } = statusRuntime;
|
|
305
|
-
let lastRuntimeLogScopeKey: string | undefined;
|
|
306
366
|
const updateRuntimeLogScope = function (reason: string) {
|
|
307
367
|
const scope = Status.createTelegramRuntimeLogScope({
|
|
308
368
|
state: statusRuntime.getStatusState(),
|
|
309
369
|
instanceId: telegramInstanceId,
|
|
310
370
|
});
|
|
311
371
|
const scopeKey = JSON.stringify(scope);
|
|
312
|
-
if (scopeKey === lastRuntimeLogScopeKey) return;
|
|
313
|
-
lastRuntimeLogScopeKey = scopeKey;
|
|
314
372
|
runtimeJsonlLog.resetIfScopeChanged(scopeKey, reason, scope);
|
|
315
373
|
};
|
|
316
374
|
const updateStatus = function (ctx: Pi.ExtensionContext) {
|
|
@@ -367,7 +425,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
367
425
|
},
|
|
368
426
|
getDefaultTarget: proactivePushTargetGetter,
|
|
369
427
|
callFollowerApi: BusFollower.createTelegramBusFollowerApiCaller({
|
|
370
|
-
socketPath:
|
|
428
|
+
socketPath: getTelegramBusSocketPath,
|
|
371
429
|
instanceId: telegramInstanceId,
|
|
372
430
|
createRequestId() {
|
|
373
431
|
telegramBusRequestSequence += 1;
|
|
@@ -596,7 +654,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
596
654
|
Routing.TelegramRoutedCallbackQuery,
|
|
597
655
|
Routing.TelegramRoutedMessage
|
|
598
656
|
>({
|
|
599
|
-
socketPath:
|
|
657
|
+
socketPath: getTelegramBusSocketPath,
|
|
600
658
|
createRequestId() {
|
|
601
659
|
telegramBusRequestSequence += 1;
|
|
602
660
|
return Bus.createTelegramBusRequestId({
|
|
@@ -611,7 +669,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
611
669
|
});
|
|
612
670
|
const followerTargetController =
|
|
613
671
|
Bus.createTelegramBusFollowerTargetController({
|
|
614
|
-
socketPath:
|
|
672
|
+
socketPath: getTelegramBusSocketPath,
|
|
615
673
|
createRequestId() {
|
|
616
674
|
telegramBusRequestSequence += 1;
|
|
617
675
|
return Bus.createTelegramBusRequestId({
|
|
@@ -766,7 +824,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
766
824
|
Routing.TelegramRoutedCallbackQuery,
|
|
767
825
|
Routing.TelegramRoutedMessage
|
|
768
826
|
>({
|
|
769
|
-
socketPath:
|
|
827
|
+
socketPath: getTelegramBusFollowerSocketPath,
|
|
770
828
|
instanceId: telegramInstanceId,
|
|
771
829
|
getContext() {
|
|
772
830
|
return telegramSessionContextStore.get();
|
|
@@ -800,7 +858,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
800
858
|
topicTargetStore: threadStore,
|
|
801
859
|
registrationState: telegramBusFollowerRegistrationState,
|
|
802
860
|
instanceId: telegramInstanceId,
|
|
803
|
-
manualFollowerProfileKey:
|
|
861
|
+
manualFollowerProfileKey: getTelegramManualFollowerProfileKey(),
|
|
804
862
|
manualFollowerOwnerId: telegramManualFollowerOwnerId,
|
|
805
863
|
getSyncState() {
|
|
806
864
|
return telegramSyncState;
|
|
@@ -834,6 +892,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
834
892
|
store: threadStore,
|
|
835
893
|
instanceId: telegramInstanceId,
|
|
836
894
|
cwd: ctx.cwd,
|
|
895
|
+
telegramProfile: getActiveTelegramThreadProfile(),
|
|
837
896
|
target: binding.target,
|
|
838
897
|
slot: binding.slot,
|
|
839
898
|
threadName: binding.threadName,
|
|
@@ -864,7 +923,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
864
923
|
BusFollower.createTelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>(
|
|
865
924
|
{
|
|
866
925
|
instanceId: telegramInstanceId,
|
|
867
|
-
|
|
926
|
+
getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
|
|
927
|
+
getLeaderSocketPath: getTelegramBusSocketPath,
|
|
868
928
|
startReceiving: telegramBusForwardedUpdateReceiver.start,
|
|
869
929
|
stopReceiving: telegramBusForwardedUpdateReceiver.stop,
|
|
870
930
|
registrationState: telegramBusFollowerRegistrationState,
|
|
@@ -885,7 +945,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
885
945
|
return undefined;
|
|
886
946
|
},
|
|
887
947
|
getProfileKey() {
|
|
888
|
-
return
|
|
948
|
+
return getTelegramManualFollowerProfileKey();
|
|
889
949
|
},
|
|
890
950
|
onHeartbeatFailure: telegramFollowerHeartbeatRecovery,
|
|
891
951
|
recordRuntimeEvent,
|
|
@@ -921,7 +981,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
921
981
|
});
|
|
922
982
|
};
|
|
923
983
|
const telegramBusLeaderRuntime = BusLeader.createTelegramBusLeaderRuntime({
|
|
924
|
-
socketPath:
|
|
984
|
+
socketPath: getTelegramBusSocketPath,
|
|
925
985
|
followerRegistry: telegramBusFollowerRegistry,
|
|
926
986
|
authSecret: telegramBusAuthSecret,
|
|
927
987
|
startPolling: pollingRuntime.start,
|
|
@@ -942,6 +1002,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
942
1002
|
getCwd(ctx) {
|
|
943
1003
|
return typeof ctx.cwd === "string" ? ctx.cwd : undefined;
|
|
944
1004
|
},
|
|
1005
|
+
getTelegramProfile: getActiveTelegramThreadProfile,
|
|
945
1006
|
shouldForceFreshUnnamed() {
|
|
946
1007
|
return forceFreshLeaderThreadOnNextStart;
|
|
947
1008
|
},
|
|
@@ -1174,7 +1235,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1174
1235
|
const disconnectTelegramAndDeleteCurrentThread =
|
|
1175
1236
|
Sync.createTelegramManualThreadDisconnectHandler({
|
|
1176
1237
|
instanceId: telegramInstanceId,
|
|
1177
|
-
getCurrentThreadRecord,
|
|
1238
|
+
getCurrentThreadRecord: findCurrentThreadRecord,
|
|
1178
1239
|
topicTargetStore: threadStore,
|
|
1179
1240
|
callApi: callTelegramApi,
|
|
1180
1241
|
getLeaderTarget() {
|
|
@@ -1199,6 +1260,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1199
1260
|
registrationState: telegramBusFollowerRegistrationState,
|
|
1200
1261
|
instanceId: telegramInstanceId,
|
|
1201
1262
|
suspendPolling: lockedPollingRuntime.suspend,
|
|
1263
|
+
onFollowerSessionDisconnect() {
|
|
1264
|
+
const target = telegramBusFollowerRegistrationState.getTarget();
|
|
1265
|
+
if (!target) return undefined;
|
|
1266
|
+
return BusFollower.markTelegramFollowerThreadStaleForSessionDisconnect({
|
|
1267
|
+
topicTargetStore: threadStore,
|
|
1268
|
+
target,
|
|
1269
|
+
});
|
|
1270
|
+
},
|
|
1202
1271
|
recordRuntimeEvent,
|
|
1203
1272
|
});
|
|
1204
1273
|
const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime({
|
package/lib/bindings.ts
CHANGED
|
@@ -73,8 +73,8 @@ export function registerTelegramCommandsAndTools({
|
|
|
73
73
|
getDefaultChatId,
|
|
74
74
|
getDefaultTarget,
|
|
75
75
|
canSendDirect,
|
|
76
|
-
updateStatus,
|
|
77
76
|
recordRuntimeEvent,
|
|
77
|
+
updateStatus,
|
|
78
78
|
}: TelegramCommandsAndToolsBindingDeps): void {
|
|
79
79
|
OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
|
|
80
80
|
getActiveTurn: activeTurnRuntime.get,
|
|
@@ -96,22 +96,72 @@ export function registerTelegramCommandsAndTools({
|
|
|
96
96
|
});
|
|
97
97
|
Prompts.registerTelegramHelpTool(pi);
|
|
98
98
|
Commands.registerTelegramBridgeCommands(pi, {
|
|
99
|
-
promptForConfig:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
promptForConfig: async (ctx, profileName) => {
|
|
100
|
+
const nextProfileName = profileName ?? undefined;
|
|
101
|
+
if (profileName && !Config.isValidTelegramProfileName(profileName)) {
|
|
102
|
+
ctx.ui.notify(`Invalid Telegram profile name: ${profileName}`, "error");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const previousProfileName = configStore.getActiveProfileName();
|
|
106
|
+
if (previousProfileName !== nextProfileName) {
|
|
107
|
+
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
108
|
+
}
|
|
109
|
+
if (!profileName) {
|
|
110
|
+
configStore.activateProfile(undefined);
|
|
111
|
+
} else {
|
|
112
|
+
const storedConfig = configStore.getStoredConfig();
|
|
113
|
+
if (!storedConfig.profiles?.[profileName]) {
|
|
114
|
+
configStore.set({
|
|
115
|
+
...storedConfig,
|
|
116
|
+
profiles: {
|
|
117
|
+
...(storedConfig.profiles ?? {}),
|
|
118
|
+
[profileName]: { botToken: "" },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
configStore.activateProfile(profileName);
|
|
123
|
+
}
|
|
124
|
+
const runSetup = Setup.createTelegramSetupPromptRuntime({
|
|
125
|
+
getConfig: configStore.get,
|
|
126
|
+
setConfig: configStore.set,
|
|
127
|
+
setupGuard: setup,
|
|
128
|
+
getMe: TelegramApi.fetchTelegramBotIdentity,
|
|
129
|
+
persistConfig,
|
|
130
|
+
startPolling: lockedPollingRuntime.start,
|
|
131
|
+
updateStatus,
|
|
132
|
+
recordRuntimeEvent,
|
|
133
|
+
});
|
|
134
|
+
await runSetup(ctx);
|
|
135
|
+
if (profileName) {
|
|
136
|
+
ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
|
|
137
|
+
}
|
|
138
|
+
},
|
|
109
139
|
getStatusLines,
|
|
110
140
|
reloadConfig: configStore.load,
|
|
111
141
|
hasBotToken: configStore.hasBotToken,
|
|
112
142
|
startPolling: lockedPollingRuntime.start,
|
|
113
143
|
stopPolling: stopPolling ?? lockedPollingRuntime.stop,
|
|
114
144
|
updateStatus,
|
|
145
|
+
getProfileNames: () =>
|
|
146
|
+
Config.getTelegramProfileNames(configStore.getStoredConfig()),
|
|
147
|
+
activateDefaultProfileConfig: async () => {
|
|
148
|
+
await configStore.load();
|
|
149
|
+
const previousProfileName = configStore.getActiveProfileName();
|
|
150
|
+
if (previousProfileName) {
|
|
151
|
+
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
152
|
+
}
|
|
153
|
+
configStore.activateProfile(undefined);
|
|
154
|
+
},
|
|
155
|
+
activateProfileConfig: async (_ctx, profileName) => {
|
|
156
|
+
await configStore.load();
|
|
157
|
+
if (!Config.isValidTelegramProfileName(profileName)) return false;
|
|
158
|
+
const previousProfileName = configStore.getActiveProfileName();
|
|
159
|
+
if (previousProfileName !== profileName) {
|
|
160
|
+
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
161
|
+
}
|
|
162
|
+
if (!configStore.activateProfile(profileName)) return false;
|
|
163
|
+
return true;
|
|
164
|
+
},
|
|
115
165
|
});
|
|
116
166
|
}
|
|
117
167
|
|
package/lib/bus-follower.ts
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
createTelegramBusLocalServer,
|
|
17
17
|
createUnauthorizedBusAck,
|
|
18
18
|
getTelegramBusSocketPath,
|
|
19
|
+
resolveTelegramBusSocketPath,
|
|
19
20
|
sendTelegramBusLocalEnvelope,
|
|
20
21
|
type TelegramBusEnvelope,
|
|
22
|
+
type TelegramBusSocketPathSource,
|
|
21
23
|
} from "./bus.ts";
|
|
22
24
|
import {
|
|
23
25
|
getTelegramBusTransportRetryPolicy,
|
|
@@ -90,6 +92,7 @@ export interface TelegramBusFollowerSessionReplacementSuspenderDeps {
|
|
|
90
92
|
registrationState: Pick<TelegramBusFollowerRegistrationState, "isRegistered">;
|
|
91
93
|
instanceId: string;
|
|
92
94
|
suspendPolling: () => Promise<void>;
|
|
95
|
+
onFollowerSessionDisconnect?: () => Promise<void> | void;
|
|
93
96
|
recordRuntimeEvent: (
|
|
94
97
|
category: string,
|
|
95
98
|
error: unknown,
|
|
@@ -132,7 +135,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntime {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
export interface TelegramBusFollowerApiCallerDeps {
|
|
135
|
-
socketPath:
|
|
138
|
+
socketPath: TelegramBusSocketPathSource;
|
|
136
139
|
instanceId: string;
|
|
137
140
|
createRequestId: () => string;
|
|
138
141
|
getAuthSecret?: () => string | undefined;
|
|
@@ -148,6 +151,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
148
151
|
getLeaderAuthSecret?: (leader: { busSecret?: string }) => string | undefined;
|
|
149
152
|
setActiveAuthSecret?: (secret: string | undefined) => void;
|
|
150
153
|
followerBusSocketPath?: string;
|
|
154
|
+
getFollowerBusSocketPath?: () => string;
|
|
151
155
|
getLeaderSocketPath?: () => string;
|
|
152
156
|
startReceiving?: () => Promise<void>;
|
|
153
157
|
stopReceiving?: () => Promise<void> | void;
|
|
@@ -237,7 +241,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
|
|
|
237
241
|
TCallbackQuery,
|
|
238
242
|
TMessage = unknown,
|
|
239
243
|
> {
|
|
240
|
-
socketPath:
|
|
244
|
+
socketPath: TelegramBusSocketPathSource;
|
|
241
245
|
instanceId: string;
|
|
242
246
|
getAuthSecret?: () => string | undefined;
|
|
243
247
|
getContext: () => TContext | undefined;
|
|
@@ -353,11 +357,12 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
353
357
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
354
358
|
const timeoutMs = deps.timeoutMs ?? 30000;
|
|
355
359
|
return async (method, args) => {
|
|
360
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
356
361
|
const response = await sendTelegramBusLocalEnvelope({
|
|
357
|
-
socketPath
|
|
362
|
+
socketPath,
|
|
358
363
|
timeoutMs,
|
|
359
364
|
retry: getTelegramBusTransportRetryPolicy({
|
|
360
|
-
endpoint:
|
|
365
|
+
endpoint: socketPath,
|
|
361
366
|
operation: "operation",
|
|
362
367
|
}),
|
|
363
368
|
envelope: {
|
|
@@ -387,24 +392,42 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
387
392
|
);
|
|
388
393
|
}
|
|
389
394
|
|
|
395
|
+
export async function markTelegramFollowerThreadStaleForSessionDisconnect(input: {
|
|
396
|
+
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
397
|
+
target: TelegramTarget;
|
|
398
|
+
}): Promise<void> {
|
|
399
|
+
await input.topicTargetStore.load();
|
|
400
|
+
const record = input.topicTargetStore.list().find(
|
|
401
|
+
(candidate) =>
|
|
402
|
+
candidate.target.chatId === input.target.chatId &&
|
|
403
|
+
candidate.target.threadId === input.target.threadId,
|
|
404
|
+
);
|
|
405
|
+
const marked = input.topicTargetStore.markStaleByTarget(
|
|
406
|
+
input.target,
|
|
407
|
+
"unknown",
|
|
408
|
+
"Follower session reloaded before reconnect.",
|
|
409
|
+
);
|
|
410
|
+
const forgotIdentity = record?.profileKey
|
|
411
|
+
? input.topicTargetStore.forgetIdentityByProfileKey(record.profileKey)
|
|
412
|
+
: false;
|
|
413
|
+
if (marked || forgotIdentity) await input.topicTargetStore.persist();
|
|
414
|
+
}
|
|
415
|
+
|
|
390
416
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
391
417
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
392
418
|
): () => Promise<void> {
|
|
393
419
|
return async () => {
|
|
394
420
|
if (deps.registrationState.isRegistered()) {
|
|
395
|
-
setTelegramFollowerSessionHandoff(
|
|
396
|
-
|
|
397
|
-
instanceId: deps.instanceId,
|
|
398
|
-
createdAtMs: (deps.getNowMs ?? Date.now)(),
|
|
399
|
-
});
|
|
421
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
422
|
+
await deps.onFollowerSessionDisconnect?.();
|
|
400
423
|
deps.recordRuntimeEvent(
|
|
401
424
|
"bus",
|
|
402
|
-
"Telegram follower registration
|
|
425
|
+
"Telegram follower registration stopped for session replacement",
|
|
403
426
|
{
|
|
404
|
-
phase: "follower-session-
|
|
427
|
+
phase: "follower-session-disconnect",
|
|
428
|
+
instanceId: deps.instanceId,
|
|
405
429
|
},
|
|
406
430
|
);
|
|
407
|
-
return;
|
|
408
431
|
}
|
|
409
432
|
await deps.suspendPolling();
|
|
410
433
|
};
|
|
@@ -706,7 +729,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
706
729
|
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
707
730
|
cwd: ctx.cwd,
|
|
708
731
|
pid: getPid(),
|
|
709
|
-
busSocketPath:
|
|
732
|
+
busSocketPath:
|
|
733
|
+
deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
|
|
710
734
|
connectedAtMs: getNowMs(),
|
|
711
735
|
},
|
|
712
736
|
});
|
package/lib/bus-leader.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TelegramBusFollowerRegistry,
|
|
21
21
|
type TelegramBusFollowerView,
|
|
22
22
|
type TelegramBusInstanceRegistration,
|
|
23
|
+
type TelegramBusSocketPathSource,
|
|
23
24
|
} from "./bus.ts";
|
|
24
25
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
25
26
|
|
|
@@ -54,6 +55,7 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
|
|
|
54
55
|
getAllowedUserId: () => number | undefined;
|
|
55
56
|
instanceId: string;
|
|
56
57
|
getCwd?: (ctx: TContext) => string | undefined;
|
|
58
|
+
getTelegramProfile?: () => string | undefined;
|
|
57
59
|
shouldForceFreshUnnamed?: () => boolean;
|
|
58
60
|
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
59
61
|
callApi: <TResponse>(
|
|
@@ -175,7 +177,7 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
|
|
|
175
177
|
) => void;
|
|
176
178
|
|
|
177
179
|
export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
178
|
-
socketPath:
|
|
180
|
+
socketPath: TelegramBusSocketPathSource;
|
|
179
181
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
180
182
|
authSecret?: string;
|
|
181
183
|
startPolling: (ctx: TContext) => void | Promise<void>;
|
|
@@ -315,6 +317,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
315
317
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
316
318
|
> {
|
|
317
319
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
320
|
+
const pendingRegistrations = new Map<
|
|
321
|
+
string,
|
|
322
|
+
Promise<(TelegramTarget & { slot?: string; threadName?: string }) | undefined>
|
|
323
|
+
>();
|
|
318
324
|
return async (registration) => {
|
|
319
325
|
const registrationStartedAtMs = Date.now();
|
|
320
326
|
const chatId = deps.getAllowedUserId();
|
|
@@ -337,6 +343,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
337
343
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
338
344
|
const followerOwner =
|
|
339
345
|
Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
|
|
346
|
+
const registrationKey = followerProfileKey || registration.instanceId;
|
|
347
|
+
const pendingRegistration = pendingRegistrations.get(registrationKey);
|
|
348
|
+
if (pendingRegistration) return pendingRegistration;
|
|
340
349
|
const provisionTarget = async () => {
|
|
341
350
|
deps.onProvisioningStart?.();
|
|
342
351
|
try {
|
|
@@ -356,6 +365,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
356
365
|
deps.onProvisioningEnd?.();
|
|
357
366
|
}
|
|
358
367
|
};
|
|
368
|
+
const runRegistration = async (): Promise<
|
|
369
|
+
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
370
|
+
> => {
|
|
359
371
|
let result = await provisionTarget();
|
|
360
372
|
deps.setSyncState(
|
|
361
373
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
@@ -363,48 +375,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
363
375
|
action: "follower-register",
|
|
364
376
|
}),
|
|
365
377
|
);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let connectedAnnouncementSent = false;
|
|
374
|
-
if (result.reused && connectedAnnouncement) {
|
|
375
|
-
try {
|
|
376
|
-
await deps.callApi("sendMessage", {
|
|
377
|
-
chat_id: connectedAnnouncement.target.chatId,
|
|
378
|
-
message_thread_id: connectedAnnouncement.target.threadId,
|
|
379
|
-
text: connectedAnnouncement.text,
|
|
380
|
-
parse_mode: connectedAnnouncement.parseMode,
|
|
381
|
-
});
|
|
382
|
-
connectedAnnouncementSent = true;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
deps.recordRuntimeEvent("telegram", error, {
|
|
385
|
-
phase: "follower-topic-reuse-probe",
|
|
386
|
-
instanceId: registration.instanceId,
|
|
387
|
-
chatId: result.target.chatId,
|
|
388
|
-
threadId: result.target.threadId,
|
|
378
|
+
const connectedAnnouncement = result.reused
|
|
379
|
+
? undefined
|
|
380
|
+
: createTelegramBusInstanceLifecycleAnnouncement({
|
|
381
|
+
target: result.target,
|
|
382
|
+
threadName: result.record.threadName,
|
|
383
|
+
slot: result.record.slot,
|
|
384
|
+
state: "connected",
|
|
389
385
|
});
|
|
390
|
-
if (Threads.isTelegramTopicTargetStaleError(error)) {
|
|
391
|
-
deps.topicTargetStore.markStaleByTarget(
|
|
392
|
-
result.target,
|
|
393
|
-
"deleted",
|
|
394
|
-
"Follower registration found the reusable thread target stale.",
|
|
395
|
-
);
|
|
396
|
-
await deps.topicTargetStore.persist();
|
|
397
|
-
result = await provisionTarget();
|
|
398
|
-
connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
399
|
-
target: result.target,
|
|
400
|
-
threadName: result.record.threadName,
|
|
401
|
-
slot: result.record.slot,
|
|
402
|
-
state: "connected",
|
|
403
|
-
});
|
|
404
|
-
connectedAnnouncementSent = false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
386
|
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
409
387
|
phase: "follower-register-critical",
|
|
410
388
|
elapsedMs: Date.now() - registrationStartedAtMs,
|
|
@@ -414,7 +392,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
414
392
|
});
|
|
415
393
|
scheduleTelegramBusLeaderBackgroundTask(async () => {
|
|
416
394
|
const backgroundStartedAtMs = Date.now();
|
|
417
|
-
if (connectedAnnouncement
|
|
395
|
+
if (connectedAnnouncement) {
|
|
418
396
|
try {
|
|
419
397
|
await deps.callApi("sendMessage", {
|
|
420
398
|
chat_id: connectedAnnouncement.target.chatId,
|
|
@@ -486,6 +464,12 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
486
464
|
slot: result.record.slot,
|
|
487
465
|
threadName: result.record.threadName,
|
|
488
466
|
};
|
|
467
|
+
};
|
|
468
|
+
const registrationPromise = runRegistration().finally(() => {
|
|
469
|
+
pendingRegistrations.delete(registrationKey);
|
|
470
|
+
});
|
|
471
|
+
pendingRegistrations.set(registrationKey, registrationPromise);
|
|
472
|
+
return registrationPromise;
|
|
489
473
|
};
|
|
490
474
|
}
|
|
491
475
|
|
|
@@ -516,6 +500,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
516
500
|
getAllowedUserId: deps.getAllowedUserId,
|
|
517
501
|
instanceId: deps.instanceId,
|
|
518
502
|
cwd: deps.getCwd?.(ctx),
|
|
503
|
+
telegramProfile: deps.getTelegramProfile?.(),
|
|
519
504
|
forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
|
|
520
505
|
getNowMs,
|
|
521
506
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|