@llblab/pi-telegram 0.20.3 → 0.20.5
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 +3 -3
- package/BACKLOG.md +64 -0
- package/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/docs/architecture.md +8 -8
- package/docs/multi-instance-bus.md +4 -4
- package/docs/outbound.md +6 -0
- package/index.ts +53 -21
- package/lib/bindings.ts +127 -34
- package/lib/bus-api.ts +5 -3
- package/lib/bus-follower.ts +11 -0
- package/lib/bus-leader.ts +81 -1
- package/lib/bus.ts +47 -1
- package/lib/config.ts +2 -2
- package/lib/lifecycle.ts +4 -4
- package/lib/locks.ts +12 -1
- package/lib/logs.ts +2 -2
- package/lib/outbound-attachments.ts +170 -0
- package/lib/paths.ts +2 -1
- package/lib/queue.ts +46 -1
- package/lib/setup.ts +7 -1
- package/lib/status.ts +15 -14
- package/lib/telegram-api.ts +44 -6
- package/lib/threads.ts +113 -2
- package/package.json +1 -1
package/lib/bindings.ts
CHANGED
|
@@ -127,22 +127,29 @@ export function registerTelegramCommandsAndTools({
|
|
|
127
127
|
});
|
|
128
128
|
setupConfigStore.activateProfile(profileName);
|
|
129
129
|
persistSetupConfig = async () => {
|
|
130
|
+
try {
|
|
131
|
+
configStore.activateProfile(undefined);
|
|
132
|
+
configStore.set({
|
|
133
|
+
...storedConfig,
|
|
134
|
+
profiles: {
|
|
135
|
+
...(storedConfig.profiles ?? {}),
|
|
136
|
+
[profileName]: storedConfig.profiles?.[profileName] ?? {
|
|
137
|
+
botToken: "",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
configStore.activateProfile(profileName);
|
|
142
|
+
configStore.set(setupConfigStore.get());
|
|
143
|
+
await persistConfig();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
configStore.activateProfile(undefined);
|
|
146
|
+
configStore.set(storedConfig);
|
|
147
|
+
configStore.activateProfile(previousProfileName);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
130
150
|
if (previousProfileName !== profileName) {
|
|
131
151
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
132
152
|
}
|
|
133
|
-
configStore.activateProfile(undefined);
|
|
134
|
-
configStore.set({
|
|
135
|
-
...storedConfig,
|
|
136
|
-
profiles: {
|
|
137
|
-
...(storedConfig.profiles ?? {}),
|
|
138
|
-
[profileName]: storedConfig.profiles?.[profileName] ?? {
|
|
139
|
-
botToken: "",
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
configStore.activateProfile(profileName);
|
|
144
|
-
configStore.set(setupConfigStore.get());
|
|
145
|
-
await persistConfig();
|
|
146
153
|
};
|
|
147
154
|
}
|
|
148
155
|
const runSetup = Setup.createTelegramSetupPromptRuntime({
|
|
@@ -242,14 +249,8 @@ interface TelegramLifecycleBindingDeps {
|
|
|
242
249
|
>["sendTextReply"] &
|
|
243
250
|
NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
|
|
244
251
|
dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
|
|
245
|
-
answerGuestQuery:
|
|
246
|
-
|
|
247
|
-
Queue.PendingTelegramTurn,
|
|
248
|
-
Pi.ExtensionContext,
|
|
249
|
-
Pi.AgentEndEvent["messages"][number],
|
|
250
|
-
Keyboard.TelegramInlineKeyboardMarkup
|
|
251
|
-
>["answerGuestQuery"]
|
|
252
|
-
>;
|
|
252
|
+
answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
|
|
253
|
+
deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
|
|
253
254
|
sendGuestReply: NonNullable<
|
|
254
255
|
Queue.TelegramAgentEndHookRuntimeDeps<
|
|
255
256
|
Queue.PendingTelegramTurn,
|
|
@@ -295,6 +296,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
295
296
|
sendTextReply,
|
|
296
297
|
dispatchNextQueuedTelegramTurn,
|
|
297
298
|
answerGuestQuery,
|
|
299
|
+
deleteMessage,
|
|
298
300
|
sendGuestReply,
|
|
299
301
|
finalizeMarkdownPreview,
|
|
300
302
|
proactivePushChatIdGetter,
|
|
@@ -319,18 +321,107 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
319
321
|
sendTextReply,
|
|
320
322
|
recordRuntimeEvent,
|
|
321
323
|
});
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
const sendGuestAttachment = async (
|
|
325
|
+
turn: Queue.PendingTelegramTurn,
|
|
326
|
+
attachment: Queue.QueuedAttachment,
|
|
327
|
+
caption?: string,
|
|
328
|
+
): Promise<void> => {
|
|
329
|
+
const stagingTarget = proactivePushTargetGetter();
|
|
330
|
+
const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
|
|
331
|
+
if (stagingChatId === undefined) {
|
|
332
|
+
throw new Error("Guest attachment staging requires a paired Telegram chat");
|
|
333
|
+
}
|
|
334
|
+
await OutboundAttachments.deliverTelegramGuestCachedAttachment({
|
|
335
|
+
guestQueryId: turn.guestQueryId!,
|
|
336
|
+
stagingChatId,
|
|
337
|
+
stagingTarget,
|
|
338
|
+
attachment,
|
|
339
|
+
caption,
|
|
327
340
|
sendMultipart: callMultipart,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
341
|
+
answerGuestQuery: (guestQueryId, result) =>
|
|
342
|
+
answerGuestQuery(guestQueryId, undefined, { result }),
|
|
343
|
+
answerGuestText: (guestQueryId, text) =>
|
|
344
|
+
answerGuestQuery(guestQueryId, text),
|
|
345
|
+
fallbackText:
|
|
346
|
+
caption || "Telegram bridge could not deliver the requested attachment.",
|
|
347
|
+
deleteMessage,
|
|
332
348
|
recordRuntimeEvent,
|
|
333
349
|
});
|
|
350
|
+
};
|
|
351
|
+
const outboundReplyPlanner =
|
|
352
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
|
|
353
|
+
const voiceReplySenderDeps = {
|
|
354
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
355
|
+
sendMultipart: callMultipart,
|
|
356
|
+
sendTextReply,
|
|
357
|
+
sendChatAction,
|
|
358
|
+
sendRecordVoiceAction,
|
|
359
|
+
getHandlers: configStore.getOutboundHandlers,
|
|
360
|
+
recordRuntimeEvent,
|
|
361
|
+
};
|
|
362
|
+
const outboundReplyArtifactSender =
|
|
363
|
+
OutboundHandlers.createTelegramOutboundReplyArtifactSender(
|
|
364
|
+
voiceReplySenderDeps,
|
|
365
|
+
);
|
|
366
|
+
const sendGuestVoiceReply = async (
|
|
367
|
+
turn: Queue.PendingTelegramTurn,
|
|
368
|
+
plan: OutboundHandlers.TelegramOutboundReplyPlan,
|
|
369
|
+
caption?: string,
|
|
370
|
+
): Promise<void> => {
|
|
371
|
+
const stagingTarget = proactivePushTargetGetter();
|
|
372
|
+
const stagingChatId = stagingTarget?.chatId ?? proactivePushChatIdGetter();
|
|
373
|
+
if (stagingChatId === undefined) {
|
|
374
|
+
throw new Error("Guest voice staging requires a paired Telegram chat");
|
|
375
|
+
}
|
|
376
|
+
const guestVoiceSender =
|
|
377
|
+
OutboundHandlers.createTelegramOutboundReplyArtifactSender({
|
|
378
|
+
...voiceReplySenderDeps,
|
|
379
|
+
sendChatAction: undefined,
|
|
380
|
+
sendRecordVoiceAction: undefined,
|
|
381
|
+
sendMultipart: async (
|
|
382
|
+
_method,
|
|
383
|
+
_fields,
|
|
384
|
+
_fileField,
|
|
385
|
+
filePath,
|
|
386
|
+
fileName,
|
|
387
|
+
) => {
|
|
388
|
+
try {
|
|
389
|
+
await OutboundAttachments.deliverTelegramGuestCachedAttachment({
|
|
390
|
+
guestQueryId: turn.guestQueryId!,
|
|
391
|
+
stagingChatId,
|
|
392
|
+
stagingTarget,
|
|
393
|
+
attachment: { path: filePath, fileName },
|
|
394
|
+
caption,
|
|
395
|
+
sendMultipart: callMultipart,
|
|
396
|
+
answerGuestQuery: (guestQueryId, result) =>
|
|
397
|
+
answerGuestQuery(guestQueryId, undefined, { result }),
|
|
398
|
+
answerGuestText: (guestQueryId, text) =>
|
|
399
|
+
answerGuestQuery(guestQueryId, text),
|
|
400
|
+
fallbackText:
|
|
401
|
+
caption || "Telegram bridge could not deliver the voice reply.",
|
|
402
|
+
deleteMessage,
|
|
403
|
+
recordRuntimeEvent,
|
|
404
|
+
});
|
|
405
|
+
} catch (error) {
|
|
406
|
+
recordRuntimeEvent("delivery", error, {
|
|
407
|
+
phase: "guest-voice-answer",
|
|
408
|
+
guestQueryId: turn.guestQueryId,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return {};
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
await guestVoiceSender(
|
|
415
|
+
turn,
|
|
416
|
+
{
|
|
417
|
+
...plan,
|
|
418
|
+
...(plan.voiceReplies?.length
|
|
419
|
+
? { voiceReplies: [plan.voiceReplies[0]!] }
|
|
420
|
+
: {}),
|
|
421
|
+
},
|
|
422
|
+
{ replyToPrompt: false },
|
|
423
|
+
);
|
|
424
|
+
};
|
|
334
425
|
const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
|
|
335
426
|
Queue.PendingTelegramTurn,
|
|
336
427
|
Pi.ExtensionContext,
|
|
@@ -383,6 +474,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
383
474
|
sendQueuedAttachments: queuedAttachmentSender,
|
|
384
475
|
answerGuestQuery,
|
|
385
476
|
sendGuestReply,
|
|
477
|
+
sendGuestAttachment,
|
|
478
|
+
sendGuestVoiceReply,
|
|
386
479
|
planOutboundReply: outboundReplyPlanner,
|
|
387
480
|
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
388
481
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
@@ -398,8 +491,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
398
491
|
const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
|
|
399
492
|
agentLifecycleHooks.onAgentStart,
|
|
400
493
|
);
|
|
401
|
-
const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext):
|
|
402
|
-
if (!canSendAgentActivity(ctx)) return;
|
|
494
|
+
const startAgentActivityTypingLoop = (ctx: Pi.ExtensionContext): boolean => {
|
|
495
|
+
if (!canSendAgentActivity(ctx)) return false;
|
|
403
496
|
const turn = activeTurnRuntime.get();
|
|
404
497
|
const target = turn?.target ?? proactivePushTargetGetter();
|
|
405
498
|
promptDispatchRuntime.startTypingLoop(
|
|
@@ -407,6 +500,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
407
500
|
turn?.chatId ?? target?.chatId ?? proactivePushChatIdGetter(),
|
|
408
501
|
{ target },
|
|
409
502
|
);
|
|
503
|
+
return true;
|
|
410
504
|
};
|
|
411
505
|
const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
|
|
412
506
|
const turn = activeTurnRuntime.get();
|
|
@@ -417,9 +511,8 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
417
511
|
const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
|
|
418
512
|
setCompactionInProgress: lifecycle.setCompactionInProgress,
|
|
419
513
|
updateStatus,
|
|
420
|
-
startTypingLoop:
|
|
514
|
+
startTypingLoop: startAgentActivityTypingLoop,
|
|
421
515
|
stopTypingLoop: typing.stop,
|
|
422
|
-
shouldStartTypingLoop: activeTurnRuntime.has,
|
|
423
516
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
424
517
|
deferredQueueDispatchRuntime.request,
|
|
425
518
|
dispatchNextQueuedTelegramTurn,
|
package/lib/bus-api.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
TelegramAnswerGuestQueryOptions,
|
|
8
9
|
TelegramApiCallOptions,
|
|
9
10
|
TelegramBridgeApiRuntime,
|
|
10
11
|
TelegramEditMessageTextBody,
|
|
11
|
-
TelegramInputRichMessage,
|
|
12
12
|
TelegramSendMessageBody,
|
|
13
13
|
TelegramSendMessageDraftBody,
|
|
14
14
|
TelegramSendRichMessageBody,
|
|
@@ -273,14 +273,16 @@ export function createTelegramBusAwareApiRuntime(
|
|
|
273
273
|
async answerGuestQuery(
|
|
274
274
|
guestQueryId: string,
|
|
275
275
|
text?: string,
|
|
276
|
-
options?:
|
|
276
|
+
options?: TelegramAnswerGuestQueryOptions,
|
|
277
277
|
): Promise<void> {
|
|
278
278
|
if (deps.ownsDirect()) {
|
|
279
279
|
await deps.directRuntime.answerGuestQuery(guestQueryId, text, options);
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
282
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
283
|
-
if (
|
|
283
|
+
if (options?.result) {
|
|
284
|
+
body.result = options.result;
|
|
285
|
+
} else if (text !== undefined || options?.richMessage) {
|
|
284
286
|
const inputContent: Record<string, unknown> = options?.richMessage
|
|
285
287
|
? { rich_message: options.richMessage }
|
|
286
288
|
: { message_text: text };
|
package/lib/bus-follower.ts
CHANGED
|
@@ -777,6 +777,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
777
777
|
let activeAuthSecret: string | undefined;
|
|
778
778
|
let activeContext: TContext | undefined;
|
|
779
779
|
let lastKnownTarget: TelegramTarget | undefined;
|
|
780
|
+
let lastKnownSlot: string | undefined;
|
|
781
|
+
let lastKnownThreadName: string | undefined;
|
|
780
782
|
const stopHeartbeat = () => {
|
|
781
783
|
if (!heartbeatInterval) return;
|
|
782
784
|
clearInterval(heartbeatInterval);
|
|
@@ -788,6 +790,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
788
790
|
deps.setActiveAuthSecret?.(undefined);
|
|
789
791
|
deps.registrationState?.setRegistered(false);
|
|
790
792
|
lastKnownTarget = undefined;
|
|
793
|
+
lastKnownSlot = undefined;
|
|
794
|
+
lastKnownThreadName = undefined;
|
|
791
795
|
activeContext = undefined;
|
|
792
796
|
void deps.stopReceiving?.();
|
|
793
797
|
};
|
|
@@ -849,8 +853,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
849
853
|
deps.getProfileKey?.(ctx) ??
|
|
850
854
|
(ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
|
|
851
855
|
threadName:
|
|
856
|
+
deps.registrationState?.getThreadName() ??
|
|
857
|
+
lastKnownThreadName ??
|
|
852
858
|
deps.getThreadName?.(ctx) ??
|
|
853
859
|
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
860
|
+
...(deps.registrationState?.getSlot() ?? lastKnownSlot
|
|
861
|
+
? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
|
|
862
|
+
: {}),
|
|
854
863
|
cwd: ctx.cwd,
|
|
855
864
|
pid: getPid(),
|
|
856
865
|
target:
|
|
@@ -912,6 +921,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
912
921
|
registrationResult,
|
|
913
922
|
);
|
|
914
923
|
lastKnownTarget = registrationResult.target;
|
|
924
|
+
lastKnownSlot = registrationResult.slot;
|
|
925
|
+
lastKnownThreadName = registrationResult.threadName;
|
|
915
926
|
activeLeaderSocketPath = leaderSocketPath;
|
|
916
927
|
activeContext = ctx;
|
|
917
928
|
await sendHeartbeat();
|
package/lib/bus-leader.ts
CHANGED
|
@@ -477,6 +477,22 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
477
477
|
: undefined;
|
|
478
478
|
const followerOwner =
|
|
479
479
|
Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
|
|
480
|
+
const recoverableTarget =
|
|
481
|
+
!reconnectRecord &&
|
|
482
|
+
registration.target?.chatId === chatId &&
|
|
483
|
+
registration.target.threadId !== undefined &&
|
|
484
|
+
!recordsBeforeProvision.some(
|
|
485
|
+
(record) =>
|
|
486
|
+
record.target.chatId === registration.target?.chatId &&
|
|
487
|
+
record.target.threadId === registration.target.threadId,
|
|
488
|
+
)
|
|
489
|
+
? registration.target
|
|
490
|
+
: undefined;
|
|
491
|
+
const recoveryHint = recoverableTarget
|
|
492
|
+
? deps.topicTargetStore.getFollowerRecoveryHintByTarget?.(
|
|
493
|
+
recoverableTarget,
|
|
494
|
+
)
|
|
495
|
+
: undefined;
|
|
480
496
|
const registrationKey = followerProfileKey || registration.instanceId;
|
|
481
497
|
const pendingRegistration = pendingRegistrations.get(registrationKey);
|
|
482
498
|
if (pendingRegistration) return pendingRegistration;
|
|
@@ -499,12 +515,69 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
499
515
|
deps.onProvisioningEnd?.();
|
|
500
516
|
}
|
|
501
517
|
};
|
|
518
|
+
const recoverRequestedTarget = async () => {
|
|
519
|
+
const nowMs = getNowMs();
|
|
520
|
+
const requestedThreadName =
|
|
521
|
+
registration.threadName &&
|
|
522
|
+
Threads.isTelegramTopicThreadNameValidForSlot(
|
|
523
|
+
registration.threadName,
|
|
524
|
+
registration.slot,
|
|
525
|
+
)
|
|
526
|
+
? registration.threadName
|
|
527
|
+
: recoveryHint?.threadName &&
|
|
528
|
+
Threads.isTelegramTopicThreadNameValidForSlot(
|
|
529
|
+
recoveryHint.threadName,
|
|
530
|
+
recoveryHint.slot,
|
|
531
|
+
)
|
|
532
|
+
? recoveryHint.threadName
|
|
533
|
+
: undefined;
|
|
534
|
+
let recoveredRecord = deps.topicTargetStore.upsert({
|
|
535
|
+
profileKey: followerProfileKey,
|
|
536
|
+
owner:
|
|
537
|
+
followerOwner.kind === "manual-follower"
|
|
538
|
+
? followerOwner
|
|
539
|
+
: {
|
|
540
|
+
kind: "manual-follower",
|
|
541
|
+
instanceId: registration.instanceId,
|
|
542
|
+
},
|
|
543
|
+
target: {
|
|
544
|
+
chatId: recoverableTarget!.chatId,
|
|
545
|
+
threadId: recoverableTarget!.threadId!,
|
|
546
|
+
},
|
|
547
|
+
status: "active",
|
|
548
|
+
createdAtMs: registration.connectedAtMs || nowMs,
|
|
549
|
+
updatedAtMs: nowMs,
|
|
550
|
+
instanceId: registration.instanceId,
|
|
551
|
+
...(requestedThreadName ? { threadName: requestedThreadName } : {}),
|
|
552
|
+
lastSyncObservedAtMs: nowMs,
|
|
553
|
+
lastReconcileAction: "follower-live-target-recovery",
|
|
554
|
+
});
|
|
555
|
+
const requestedSlot = registration.slot ?? recoveryHint?.slot;
|
|
556
|
+
const requestedSlotAvailable =
|
|
557
|
+
!!requestedSlot &&
|
|
558
|
+
/^[A-Z]$/.test(requestedSlot) &&
|
|
559
|
+
!recordsBeforeProvision.some((record) => record.slot === requestedSlot);
|
|
560
|
+
if (requestedSlotAvailable) {
|
|
561
|
+
recoveredRecord = deps.topicTargetStore.upsert({
|
|
562
|
+
...recoveredRecord,
|
|
563
|
+
slot: requestedSlot,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
await deps.topicTargetStore.persist();
|
|
567
|
+
return {
|
|
568
|
+
target: recoveredRecord.target,
|
|
569
|
+
reused: true,
|
|
570
|
+
record: recoveredRecord,
|
|
571
|
+
};
|
|
572
|
+
};
|
|
502
573
|
const runRegistration = async (): Promise<
|
|
503
574
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
504
575
|
> => {
|
|
505
576
|
let result = reconnectRecord
|
|
506
577
|
? { target: reconnectRecord.target, reused: true, record: reconnectRecord }
|
|
507
|
-
:
|
|
578
|
+
: recoverableTarget
|
|
579
|
+
? await recoverRequestedTarget()
|
|
580
|
+
: await provisionTarget();
|
|
508
581
|
if (reconnectRecord) {
|
|
509
582
|
const nowMs = getNowMs();
|
|
510
583
|
const transferredRecord = deps.topicTargetStore.upsert({
|
|
@@ -1126,6 +1199,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1126
1199
|
followerRealityTimer.unref?.();
|
|
1127
1200
|
};
|
|
1128
1201
|
const pruneFollowers = async () => {
|
|
1202
|
+
try {
|
|
1203
|
+
await localServer.ensureEndpoint();
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1206
|
+
phase: "leader-endpoint-recovery",
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1129
1209
|
const removed = deps.followerRegistry.pruneStale(
|
|
1130
1210
|
getNowMs(),
|
|
1131
1211
|
followerStaleAfterMs,
|
package/lib/bus.ts
CHANGED
|
@@ -98,6 +98,7 @@ export interface TelegramBusInstanceRegistration {
|
|
|
98
98
|
instanceId: string;
|
|
99
99
|
profileKey?: string;
|
|
100
100
|
threadName?: string;
|
|
101
|
+
slot?: string;
|
|
101
102
|
cwd?: string;
|
|
102
103
|
pid?: number;
|
|
103
104
|
target?: TelegramTarget;
|
|
@@ -164,6 +165,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
164
165
|
"sendRichMessageDraft",
|
|
165
166
|
]);
|
|
166
167
|
const allowedMultipartMethods = new Set([
|
|
168
|
+
"sendAudio",
|
|
167
169
|
"sendDocument",
|
|
168
170
|
"sendMediaGroup",
|
|
169
171
|
"sendPhoto",
|
|
@@ -383,6 +385,7 @@ export function parseTelegramBusEnvelope(
|
|
|
383
385
|
export interface TelegramBusLocalServer {
|
|
384
386
|
start: () => Promise<void>;
|
|
385
387
|
stop: () => Promise<void>;
|
|
388
|
+
ensureEndpoint: () => Promise<boolean>;
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
export type TelegramBusSocketPathSource = string | (() => string);
|
|
@@ -609,12 +612,14 @@ export function createTelegramBusLocalServer(
|
|
|
609
612
|
): TelegramBusLocalServer {
|
|
610
613
|
let server: Server | undefined;
|
|
611
614
|
let activeSocketPath: string | undefined;
|
|
615
|
+
let endpointRecovery: Promise<boolean> | undefined;
|
|
616
|
+
let stopGeneration = 0;
|
|
612
617
|
const sockets = new Set<Socket>();
|
|
613
618
|
const closeSocket = (socket: Socket) => {
|
|
614
619
|
sockets.delete(socket);
|
|
615
620
|
socket.destroy();
|
|
616
621
|
};
|
|
617
|
-
|
|
622
|
+
const runtime: TelegramBusLocalServer = {
|
|
618
623
|
start: async () => {
|
|
619
624
|
if (server) return;
|
|
620
625
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
@@ -678,6 +683,7 @@ export function createTelegramBusLocalServer(
|
|
|
678
683
|
if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
|
|
679
684
|
},
|
|
680
685
|
stop: async () => {
|
|
686
|
+
stopGeneration += 1;
|
|
681
687
|
const activeServer = server;
|
|
682
688
|
const socketPath = activeSocketPath;
|
|
683
689
|
server = undefined;
|
|
@@ -702,7 +708,44 @@ export function createTelegramBusLocalServer(
|
|
|
702
708
|
);
|
|
703
709
|
}
|
|
704
710
|
},
|
|
711
|
+
ensureEndpoint: async () => {
|
|
712
|
+
const socketPath = activeSocketPath;
|
|
713
|
+
if (
|
|
714
|
+
!server ||
|
|
715
|
+
!socketPath ||
|
|
716
|
+
isTelegramBusPipePath(socketPath) ||
|
|
717
|
+
existsSync(socketPath)
|
|
718
|
+
) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
if (endpointRecovery) return endpointRecovery;
|
|
722
|
+
endpointRecovery = (async () => {
|
|
723
|
+
deps.recordTransportEvent?.(
|
|
724
|
+
"server-endpoint-missing",
|
|
725
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
726
|
+
);
|
|
727
|
+
const recoveryStopGeneration = stopGeneration + 1;
|
|
728
|
+
await runtime.stop();
|
|
729
|
+
if (stopGeneration !== recoveryStopGeneration) return false;
|
|
730
|
+
await runtime.start();
|
|
731
|
+
if (stopGeneration !== recoveryStopGeneration) {
|
|
732
|
+
await runtime.stop();
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
deps.recordTransportEvent?.(
|
|
736
|
+
"server-endpoint-recovered",
|
|
737
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
738
|
+
);
|
|
739
|
+
return true;
|
|
740
|
+
})();
|
|
741
|
+
try {
|
|
742
|
+
return await endpointRecovery;
|
|
743
|
+
} finally {
|
|
744
|
+
endpointRecovery = undefined;
|
|
745
|
+
}
|
|
746
|
+
},
|
|
705
747
|
};
|
|
748
|
+
return runtime;
|
|
706
749
|
}
|
|
707
750
|
|
|
708
751
|
function getTelegramBusEnvelopeDiagnostics(
|
|
@@ -1068,6 +1111,9 @@ function parseRegistration(
|
|
|
1068
1111
|
registration.profileKey = value.profileKey;
|
|
1069
1112
|
if (typeof value.threadName === "string")
|
|
1070
1113
|
registration.threadName = value.threadName;
|
|
1114
|
+
if (typeof value.slot === "string" && /^[A-Z]$/.test(value.slot)) {
|
|
1115
|
+
registration.slot = value.slot;
|
|
1116
|
+
}
|
|
1071
1117
|
if (typeof value.cwd === "string") registration.cwd = value.cwd;
|
|
1072
1118
|
if (typeof value.pid === "number") registration.pid = value.pid;
|
|
1073
1119
|
if (typeof value.busSocketPath === "string") {
|
package/lib/config.ts
CHANGED
|
@@ -85,8 +85,8 @@ export interface TelegramBotProfile {
|
|
|
85
85
|
lastUpdateId?: number;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/** Profile names must
|
|
89
|
-
const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9]
|
|
88
|
+
/** Profile names must contain only lowercase ASCII letters and digits; max 32 chars. */
|
|
89
|
+
const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9]{1,32}$/;
|
|
90
90
|
const TELEGRAM_RESERVED_PROFILE_NAMES: ReadonlySet<string> = new Set([
|
|
91
91
|
"default",
|
|
92
92
|
"main",
|
package/lib/lifecycle.ts
CHANGED
|
@@ -153,9 +153,8 @@ function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
|
|
|
153
153
|
export interface TelegramCompactionObserverRuntimeDeps<TContext> {
|
|
154
154
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
155
155
|
updateStatus: (ctx: TContext) => void;
|
|
156
|
-
startTypingLoop?: (ctx: TContext) => void;
|
|
156
|
+
startTypingLoop?: (ctx: TContext) => boolean | void;
|
|
157
157
|
stopTypingLoop?: () => void;
|
|
158
|
-
shouldStartTypingLoop?: () => boolean;
|
|
159
158
|
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
160
159
|
dispatch: (ctx: TContext) => void,
|
|
161
160
|
) => void;
|
|
@@ -196,8 +195,9 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
196
195
|
return {
|
|
197
196
|
onSessionBeforeCompact: (_event, ctx) => {
|
|
198
197
|
deps.setCompactionInProgress(true);
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const typingStartResult = deps.startTypingLoop?.(ctx);
|
|
199
|
+
typingStartedByObserver =
|
|
200
|
+
!!deps.startTypingLoop && typingStartResult !== false;
|
|
201
201
|
deps.updateStatus(ctx);
|
|
202
202
|
clearFallbackTimer();
|
|
203
203
|
fallbackTimer = setTimer(() => {
|
package/lib/locks.ts
CHANGED
|
@@ -203,6 +203,17 @@ export function formatTelegramLockEntry(lock: TelegramLockEntry): string {
|
|
|
203
203
|
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
function formatTelegramFollowerRegistrationFailure(message: string): string {
|
|
207
|
+
if (/\b(?:ENOENT|ECONNREFUSED|ETIMEDOUT)\b/u.test(message)) {
|
|
208
|
+
return (
|
|
209
|
+
`live owner / unreachable bus endpoint after bounded retries (${message}); ` +
|
|
210
|
+
"wait briefly for owner recovery, then retry /telegram-connect. " +
|
|
211
|
+
"Do not force takeover while the owner remains live"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return message;
|
|
215
|
+
}
|
|
216
|
+
|
|
206
217
|
function getLockState(
|
|
207
218
|
lock: TelegramLockEntry | undefined,
|
|
208
219
|
pid: number,
|
|
@@ -509,7 +520,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
509
520
|
ok: false,
|
|
510
521
|
canTakeover: false,
|
|
511
522
|
owner,
|
|
512
|
-
message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${failureMessage}.`,
|
|
523
|
+
message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${formatTelegramFollowerRegistrationFailure(failureMessage)}.`,
|
|
513
524
|
};
|
|
514
525
|
}
|
|
515
526
|
}
|
package/lib/logs.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function getTelegramPreviousRuntimeLogPath(
|
|
|
65
65
|
): string {
|
|
66
66
|
return resolveTelegramProfileTempFilePath(
|
|
67
67
|
"logs",
|
|
68
|
-
"
|
|
68
|
+
"_prev.jsonl",
|
|
69
69
|
agentDir,
|
|
70
70
|
profileName,
|
|
71
71
|
);
|
|
@@ -91,7 +91,7 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
91
91
|
const resolvePreviousPath = () => {
|
|
92
92
|
if (typeof options.previousPath === "function") return options.previousPath();
|
|
93
93
|
if (options.previousPath) return options.previousPath;
|
|
94
|
-
return resolvePath().replace(/\.jsonl$/u, ".
|
|
94
|
+
return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
|
|
95
95
|
};
|
|
96
96
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
97
97
|
const getNowMs = options.getNowMs ?? Date.now;
|