@llblab/pi-kit 0.4.0 → 0.5.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/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 +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -2
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +9 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +39 -11
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -4
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/index.ts +12 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +12 -3
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +5 -3
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +99 -16
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +72 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +81 -9
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +142 -30
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +8 -6
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +6 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +4 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +19 -8
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +2 -0
- package/package.json +2 -2
|
@@ -21,6 +21,7 @@ const TELEGRAM_LONG_POLL_LIMIT = 10;
|
|
|
21
21
|
const TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30;
|
|
22
22
|
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS = 2_500;
|
|
23
23
|
const TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES = 2;
|
|
24
|
+
export const TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT = 10;
|
|
24
25
|
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
|
|
25
26
|
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
26
27
|
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
@@ -69,6 +70,15 @@ export function getLatestTelegramUpdateId(
|
|
|
69
70
|
return updates.at(-1)?.update_id;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export class TelegramPersistentGetUpdatesConflictError extends Error {
|
|
74
|
+
readonly count: number;
|
|
75
|
+
constructor(count: number) {
|
|
76
|
+
super(`Telegram polling stopped after ${count} consecutive getUpdates conflicts.`);
|
|
77
|
+
this.name = "TelegramPersistentGetUpdatesConflictError";
|
|
78
|
+
this.count = count;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
export class TelegramGetUpdatesTimeoutError extends Error {
|
|
73
83
|
readonly timeoutMs: number;
|
|
74
84
|
|
|
@@ -126,7 +136,8 @@ export type TelegramPollingStopReason =
|
|
|
126
136
|
| "not-started"
|
|
127
137
|
| "requested"
|
|
128
138
|
| "completed"
|
|
129
|
-
| "failed"
|
|
139
|
+
| "failed"
|
|
140
|
+
| "persistent-conflict";
|
|
130
141
|
|
|
131
142
|
export interface TelegramPollingStateSnapshot {
|
|
132
143
|
phase: TelegramPollingPhase;
|
|
@@ -200,6 +211,7 @@ export interface TelegramPollingRuntimeDeps<
|
|
|
200
211
|
createAbortController?: () => AbortController;
|
|
201
212
|
getNowMs?: () => number;
|
|
202
213
|
onPollingStateChange?: () => void;
|
|
214
|
+
onPersistentConflict?: (ctx: TContext, count: number) => MaybePromise<void>;
|
|
203
215
|
onPollingStarted?: () => void;
|
|
204
216
|
onPollingStopped?: (reason: TelegramPollingStopReason) => void;
|
|
205
217
|
}
|
|
@@ -227,20 +239,31 @@ export interface TelegramPollingAdmissionRuntime<TContext> {
|
|
|
227
239
|
export function createTelegramPollingAdmissionRuntime<TContext>(deps: {
|
|
228
240
|
polling: TelegramPollingController<TContext>;
|
|
229
241
|
prepareStart?: () => MaybePromise<void>;
|
|
242
|
+
canStart?: (ctx: TContext) => boolean;
|
|
230
243
|
validateStart?: () => void;
|
|
231
244
|
worker: {
|
|
232
245
|
onSessionStart: (ctx: TContext) => Promise<void>;
|
|
233
246
|
};
|
|
234
247
|
}): TelegramPollingAdmissionRuntime<TContext> {
|
|
248
|
+
let generation = 0;
|
|
235
249
|
return {
|
|
236
250
|
isActive: deps.polling.isActive,
|
|
237
251
|
async start(ctx) {
|
|
252
|
+
if (!(deps.canStart?.(ctx) ?? true)) return;
|
|
253
|
+
const expectedGeneration = ++generation;
|
|
254
|
+
const isCurrent = () => expectedGeneration === generation && (deps.canStart?.(ctx) ?? true);
|
|
255
|
+
if (!isCurrent()) return;
|
|
238
256
|
await deps.prepareStart?.();
|
|
257
|
+
if (!isCurrent()) return;
|
|
239
258
|
deps.validateStart?.();
|
|
240
259
|
await deps.worker.onSessionStart(ctx);
|
|
241
|
-
|
|
260
|
+
if (!isCurrent()) return;
|
|
261
|
+
deps.polling.start(ctx);
|
|
262
|
+
},
|
|
263
|
+
async stop() {
|
|
264
|
+
generation += 1;
|
|
265
|
+
await deps.polling.stop();
|
|
242
266
|
},
|
|
243
|
-
stop: deps.polling.stop,
|
|
244
267
|
};
|
|
245
268
|
}
|
|
246
269
|
|
|
@@ -256,6 +279,7 @@ export type TelegramDurablePollingRuntimeAssemblyDeps<
|
|
|
256
279
|
TelegramPollingControllerRuntimeDeps<TUpdate, TContext>,
|
|
257
280
|
"appendUpdateBatch" | "getJournalEntryCount" | "signalUpdateWorker"
|
|
258
281
|
> & {
|
|
282
|
+
canStart?: (ctx: TContext) => boolean;
|
|
259
283
|
journal: {
|
|
260
284
|
appendBatch: (
|
|
261
285
|
updates: readonly TUpdate[],
|
|
@@ -287,6 +311,7 @@ export function createTelegramDurablePollingRuntimeAssembly<
|
|
|
287
311
|
const admission = createTelegramPollingAdmissionRuntime({
|
|
288
312
|
polling: controller,
|
|
289
313
|
prepareStart: deps.journal.prepareCursorCutover,
|
|
314
|
+
canStart: deps.canStart,
|
|
290
315
|
validateStart() {
|
|
291
316
|
if (deps.journal.getAcceptedThroughUpdateId() !== undefined) return;
|
|
292
317
|
if (deps.journal.getBootstrapEntryCount() === 0) return;
|
|
@@ -312,6 +337,7 @@ export type TelegramPollingControllerRuntimeDeps<
|
|
|
312
337
|
createAbortController?: () => AbortController;
|
|
313
338
|
getNowMs?: () => number;
|
|
314
339
|
onPollingStateChange?: () => void;
|
|
340
|
+
onPersistentConflict?: (ctx: TContext, count: number) => MaybePromise<void>;
|
|
315
341
|
};
|
|
316
342
|
|
|
317
343
|
function notifyTelegramPollingStateChange(
|
|
@@ -390,6 +416,7 @@ export function createTelegramPollingControllerRuntime<
|
|
|
390
416
|
createAbortController: deps.createAbortController,
|
|
391
417
|
getNowMs,
|
|
392
418
|
onPollingStateChange: deps.onPollingStateChange,
|
|
419
|
+
onPersistentConflict: deps.onPersistentConflict,
|
|
393
420
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
394
421
|
});
|
|
395
422
|
}
|
|
@@ -503,6 +530,7 @@ export function startTelegramPollingRuntime<TContext>(
|
|
|
503
530
|
deps.setPollingController(controller);
|
|
504
531
|
deps.onPollingStarted?.();
|
|
505
532
|
let failed = false;
|
|
533
|
+
let persistentConflict: TelegramPersistentGetUpdatesConflictError | undefined;
|
|
506
534
|
let runPromise: Promise<void>;
|
|
507
535
|
try {
|
|
508
536
|
runPromise = deps.runPollLoop(ctx, controller.signal);
|
|
@@ -513,24 +541,40 @@ export function startTelegramPollingRuntime<TContext>(
|
|
|
513
541
|
promise = runPromise
|
|
514
542
|
.catch((error) => {
|
|
515
543
|
if (shouldStopTelegramPolling(controller.signal.aborted, error)) return;
|
|
544
|
+
if (error instanceof TelegramPersistentGetUpdatesConflictError) {
|
|
545
|
+
persistentConflict = error;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
516
548
|
failed = true;
|
|
517
549
|
deps.recordRuntimeEvent?.("polling", error, {
|
|
518
550
|
phase: "controller",
|
|
519
551
|
});
|
|
520
552
|
})
|
|
521
|
-
.finally(() => {
|
|
553
|
+
.finally(async () => {
|
|
522
554
|
const ownsPromise = deps.getPollingPromise() === promise;
|
|
523
555
|
const ownsController = deps.getPollingController() === controller;
|
|
524
556
|
if (ownsPromise) deps.setPollingPromise(undefined);
|
|
525
557
|
if (ownsController) deps.setPollingController(undefined);
|
|
526
|
-
if (ownsPromise
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
558
|
+
if (!ownsPromise && !ownsController) return;
|
|
559
|
+
deps.onPollingStopped?.(
|
|
560
|
+
controller.signal.aborted ? "requested" : persistentConflict
|
|
561
|
+
? "persistent-conflict" : failed ? "failed" : "completed",
|
|
562
|
+
);
|
|
563
|
+
// Detach the inner promise before outer teardown calls polling.stop().
|
|
564
|
+
if (persistentConflict && !controller.signal.aborted) {
|
|
565
|
+
try {
|
|
566
|
+
if (deps.onPersistentConflict) {
|
|
567
|
+
await deps.onPersistentConflict(ctx, persistentConflict.count);
|
|
568
|
+
} else {
|
|
569
|
+
deps.stopTypingLoop();
|
|
570
|
+
deps.recordRuntimeEvent?.("polling", persistentConflict, {
|
|
571
|
+
phase: "persistent-conflict", count: persistentConflict.count,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "conflict-stand-down" });
|
|
576
|
+
}
|
|
577
|
+
if (deps.getPollingController() || deps.getPollingPromise()) return;
|
|
534
578
|
}
|
|
535
579
|
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
536
580
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -581,6 +625,11 @@ export interface TelegramThreadCapabilityReaderDeps {
|
|
|
581
625
|
) => Promise<TResponse>;
|
|
582
626
|
}
|
|
583
627
|
|
|
628
|
+
interface TelegramThreadCapabilityLifecycle {
|
|
629
|
+
capture: () => () => boolean;
|
|
630
|
+
invalidate: () => void;
|
|
631
|
+
}
|
|
632
|
+
|
|
584
633
|
export interface TelegramStartupThreadCapabilityProbeDeps extends TelegramThreadCapabilityReaderDeps {
|
|
585
634
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
586
635
|
recordEvent: (
|
|
@@ -595,6 +644,7 @@ export interface TelegramStartupThreadCapabilityProbeDeps extends TelegramThread
|
|
|
595
644
|
export interface TelegramThreadCapabilityRuntimeDeps<
|
|
596
645
|
TContext,
|
|
597
646
|
> extends TelegramThreadCapabilityReaderDeps {
|
|
647
|
+
lifecycle?: TelegramThreadCapabilityLifecycle;
|
|
598
648
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
599
649
|
ownsLock: (ctx: TContext) => boolean;
|
|
600
650
|
isFollowerRegistered?: () => boolean;
|
|
@@ -672,6 +722,7 @@ export interface TelegramThreadAwarePollingDeps<
|
|
|
672
722
|
TContext,
|
|
673
723
|
TOwner,
|
|
674
724
|
> extends TelegramStartupThreadCapabilityProbeDeps {
|
|
725
|
+
lifecycle?: TelegramThreadCapabilityLifecycle;
|
|
675
726
|
isBusRuntimeEnabled: () => boolean;
|
|
676
727
|
isTopicModeUnavailableError: (error: unknown) => boolean;
|
|
677
728
|
getPollingStartedWithTelegramBus: () => boolean;
|
|
@@ -749,7 +800,16 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
|
|
|
749
800
|
export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
750
801
|
deps: TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner>,
|
|
751
802
|
): TelegramThreadCapabilityOrchestration<TContext, TOwner> {
|
|
803
|
+
let generation = 0;
|
|
804
|
+
const lifecycle: TelegramThreadCapabilityLifecycle = {
|
|
805
|
+
capture() {
|
|
806
|
+
const expected = generation;
|
|
807
|
+
return () => expected === generation;
|
|
808
|
+
},
|
|
809
|
+
invalidate() { generation++; },
|
|
810
|
+
};
|
|
752
811
|
const capabilityDeps: TelegramThreadCapabilityRuntimeDeps<TContext> = {
|
|
812
|
+
lifecycle,
|
|
753
813
|
getAllowedUserId: deps.getAllowedUserId,
|
|
754
814
|
callApi: deps.callApi,
|
|
755
815
|
topicTargetStore: deps.topicTargetStore,
|
|
@@ -773,6 +833,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
|
773
833
|
monitor: createTelegramThreadCapabilityMonitor(capabilityDeps),
|
|
774
834
|
observeTarget: createTelegramThreadTargetObservationHandler(capabilityDeps),
|
|
775
835
|
pollingPorts: createTelegramThreadAwarePollingPorts({
|
|
836
|
+
lifecycle,
|
|
776
837
|
getAllowedUserId: deps.getAllowedUserId,
|
|
777
838
|
callApi: deps.callApi,
|
|
778
839
|
topicTargetStore: deps.topicTargetStore,
|
|
@@ -807,8 +868,11 @@ export async function readTelegramThreadCapability(
|
|
|
807
868
|
|
|
808
869
|
export async function probeTelegramStartupThreadCapability(
|
|
809
870
|
deps: TelegramStartupThreadCapabilityProbeDeps,
|
|
871
|
+
isCurrent: () => boolean = () => true,
|
|
810
872
|
): Promise<boolean | undefined> {
|
|
873
|
+
if (!isCurrent()) return;
|
|
811
874
|
const threadModeEnabled = await readTelegramThreadCapability(deps);
|
|
875
|
+
if (!isCurrent()) return;
|
|
812
876
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
813
877
|
if (threadModeEnabled === false) {
|
|
814
878
|
deps.topicTargetStore.setBotState({
|
|
@@ -817,6 +881,7 @@ export async function probeTelegramStartupThreadCapability(
|
|
|
817
881
|
lastReconcileAction: "startup-bot-topics-disabled",
|
|
818
882
|
});
|
|
819
883
|
await deps.topicTargetStore.persist();
|
|
884
|
+
if (!isCurrent()) return;
|
|
820
885
|
deps.recordEvent("bus", "Telegram Threaded Mode unavailable on startup", {
|
|
821
886
|
phase: "startup-bot-topics-disabled",
|
|
822
887
|
});
|
|
@@ -831,6 +896,7 @@ export async function probeTelegramStartupThreadCapability(
|
|
|
831
896
|
lastReconcileAction: "startup-bot-topics-enabled",
|
|
832
897
|
});
|
|
833
898
|
await deps.topicTargetStore.persist();
|
|
899
|
+
if (!isCurrent()) return;
|
|
834
900
|
deps.setTopicModeUnavailable(false);
|
|
835
901
|
}
|
|
836
902
|
return threadModeEnabled;
|
|
@@ -865,8 +931,11 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
865
931
|
threadModeEnabled: boolean,
|
|
866
932
|
phase: string,
|
|
867
933
|
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
934
|
+
isCurrent: () => boolean = deps.lifecycle?.capture() ?? (() => true),
|
|
868
935
|
): Promise<void> {
|
|
936
|
+
if (!isCurrent()) return;
|
|
869
937
|
await deps.topicTargetStore.load();
|
|
938
|
+
if (!isCurrent()) return;
|
|
870
939
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
871
940
|
const previousBotState = deps.topicTargetStore.getBotState();
|
|
872
941
|
if (!threadModeEnabled) {
|
|
@@ -886,6 +955,7 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
886
955
|
lastReconcileAction: phase,
|
|
887
956
|
});
|
|
888
957
|
await deps.topicTargetStore.persist();
|
|
958
|
+
if (!isCurrent()) return;
|
|
889
959
|
deps.setTopicModeUnavailable(true);
|
|
890
960
|
deps.stopFollowerRegistration();
|
|
891
961
|
if (
|
|
@@ -894,16 +964,20 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
894
964
|
) {
|
|
895
965
|
deps.stopLeaderHealth();
|
|
896
966
|
await deps.stopBusPolling();
|
|
967
|
+
if (!isCurrent()) return;
|
|
897
968
|
deps.setPollingStartedWithTelegramBus(false);
|
|
898
969
|
try {
|
|
899
970
|
await deps.startClassicPolling(ctx);
|
|
971
|
+
if (!isCurrent()) return;
|
|
900
972
|
} catch (classicError) {
|
|
973
|
+
if (!isCurrent()) return;
|
|
901
974
|
deps.topicTargetStore.setBotState({
|
|
902
975
|
threadMode: "disabled",
|
|
903
976
|
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
904
977
|
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
905
978
|
});
|
|
906
979
|
await deps.topicTargetStore.persist();
|
|
980
|
+
if (!isCurrent()) return;
|
|
907
981
|
deps.recordEvent("bus", classicError, {
|
|
908
982
|
phase: `${phase}-classic-restore`,
|
|
909
983
|
});
|
|
@@ -919,14 +993,18 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
919
993
|
lastReconcileAction: phase,
|
|
920
994
|
});
|
|
921
995
|
await deps.topicTargetStore.persist();
|
|
996
|
+
if (!isCurrent()) return;
|
|
922
997
|
deps.setTopicModeUnavailable(false);
|
|
923
998
|
if (!deps.getPollingStartedWithTelegramBus() && deps.ownsLock(ctx)) {
|
|
924
999
|
await deps.stopClassicPolling();
|
|
1000
|
+
if (!isCurrent()) return;
|
|
925
1001
|
deps.setPollingStartedWithTelegramBus(true);
|
|
926
1002
|
try {
|
|
927
1003
|
await deps.startBusPolling(ctx);
|
|
1004
|
+
if (!isCurrent()) return;
|
|
928
1005
|
deps.startLeaderHealth();
|
|
929
1006
|
} catch (error) {
|
|
1007
|
+
if (!isCurrent()) return;
|
|
930
1008
|
deps.setPollingStartedWithTelegramBus(false);
|
|
931
1009
|
const threadModeUnavailable =
|
|
932
1010
|
deps.isTopicModeUnavailableError?.(error) === true;
|
|
@@ -937,17 +1015,21 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
937
1015
|
lastReconcileAction: `${phase}-unavailable`,
|
|
938
1016
|
});
|
|
939
1017
|
await deps.topicTargetStore.persist();
|
|
1018
|
+
if (!isCurrent()) return;
|
|
940
1019
|
deps.setTopicModeUnavailable(true);
|
|
941
1020
|
}
|
|
942
1021
|
try {
|
|
943
1022
|
await deps.startClassicPolling(ctx);
|
|
1023
|
+
if (!isCurrent()) return;
|
|
944
1024
|
} catch (classicError) {
|
|
1025
|
+
if (!isCurrent()) return;
|
|
945
1026
|
deps.topicTargetStore.setBotState({
|
|
946
1027
|
threadMode: "disabled",
|
|
947
1028
|
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
948
1029
|
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
949
1030
|
});
|
|
950
1031
|
await deps.topicTargetStore.persist();
|
|
1032
|
+
if (!isCurrent()) return;
|
|
951
1033
|
deps.recordEvent("bus", classicError, {
|
|
952
1034
|
phase: `${phase}-classic-restore`,
|
|
953
1035
|
});
|
|
@@ -963,17 +1045,25 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
963
1045
|
export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
964
1046
|
deps: TelegramThreadAwarePollingDeps<TContext, TOwner>,
|
|
965
1047
|
): TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
1048
|
+
let generation = 0;
|
|
966
1049
|
const startPolling = async (
|
|
967
1050
|
ctx: TContext,
|
|
968
1051
|
options?: { forceFreshLeaderThread?: boolean },
|
|
969
1052
|
): Promise<void> => {
|
|
1053
|
+
const expectedGeneration = ++generation;
|
|
1054
|
+
deps.lifecycle?.invalidate();
|
|
1055
|
+
const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
1056
|
+
const isCurrent = () => expectedGeneration === generation && isLifecycleCurrent();
|
|
970
1057
|
await deps.topicTargetStore.load();
|
|
1058
|
+
if (!isCurrent()) return;
|
|
971
1059
|
let startupThreadCapability: boolean | undefined;
|
|
972
1060
|
try {
|
|
973
|
-
startupThreadCapability = await probeTelegramStartupThreadCapability(deps);
|
|
1061
|
+
startupThreadCapability = await probeTelegramStartupThreadCapability(deps, isCurrent);
|
|
974
1062
|
} catch (error) {
|
|
1063
|
+
if (!isCurrent()) return;
|
|
975
1064
|
deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
|
|
976
1065
|
}
|
|
1066
|
+
if (!isCurrent()) return;
|
|
977
1067
|
deps.setTopicModeUnavailable(startupThreadCapability !== true);
|
|
978
1068
|
if (deps.isBusRuntimeEnabled()) {
|
|
979
1069
|
deps.setTopicModeUnavailable(false);
|
|
@@ -983,32 +1073,39 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
983
1073
|
!!options?.forceFreshLeaderThread,
|
|
984
1074
|
);
|
|
985
1075
|
await deps.startBusLeaderPolling(ctx);
|
|
1076
|
+
if (!isCurrent()) return;
|
|
986
1077
|
deps.startLeaderHealth();
|
|
987
1078
|
return;
|
|
988
1079
|
} catch (error) {
|
|
1080
|
+
if (!isCurrent()) return;
|
|
989
1081
|
deps.setPollingStartedWithTelegramBus(false);
|
|
990
1082
|
if (!deps.isTopicModeUnavailableError(error)) throw error;
|
|
991
1083
|
deps.setTopicModeUnavailable(true);
|
|
992
1084
|
await deps.topicTargetStore.load();
|
|
1085
|
+
if (!isCurrent()) return;
|
|
993
1086
|
deps.topicTargetStore.setBotState({
|
|
994
1087
|
threadMode: "disabled",
|
|
995
1088
|
updatedAtMs: Date.now(),
|
|
996
1089
|
lastReconcileAction: "thread-mode-unavailable",
|
|
997
1090
|
});
|
|
998
1091
|
await deps.topicTargetStore.persist();
|
|
1092
|
+
if (!isCurrent()) return;
|
|
999
1093
|
deps.recordEvent("bus", error, { phase: "thread-mode-unavailable" });
|
|
1000
1094
|
} finally {
|
|
1001
|
-
deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
1095
|
+
if (isCurrent()) deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
1002
1096
|
}
|
|
1003
1097
|
}
|
|
1004
1098
|
deps.setPollingStartedWithTelegramBus(false);
|
|
1005
1099
|
await deps.startClassicPolling(ctx);
|
|
1006
1100
|
};
|
|
1007
1101
|
const stopPolling = async (): Promise<void> => {
|
|
1102
|
+
const expectedGeneration = ++generation;
|
|
1103
|
+
deps.lifecycle?.invalidate();
|
|
1104
|
+
deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
1105
|
+
deps.stopLeaderHealth();
|
|
1008
1106
|
if (deps.getPollingStartedWithTelegramBus()) {
|
|
1009
|
-
deps.stopLeaderHealth();
|
|
1010
1107
|
await deps.stopBusLeaderPolling();
|
|
1011
|
-
deps.setPollingStartedWithTelegramBus(false);
|
|
1108
|
+
if (expectedGeneration === generation) deps.setPollingStartedWithTelegramBus(false);
|
|
1012
1109
|
return;
|
|
1013
1110
|
}
|
|
1014
1111
|
await deps.stopClassicPolling();
|
|
@@ -1043,14 +1140,17 @@ export function createTelegramThreadTargetObservationHandler<TContext>(
|
|
|
1043
1140
|
if (transitionPending) return;
|
|
1044
1141
|
if (deps.topicTargetStore.getBotState().threadMode === "enabled") return;
|
|
1045
1142
|
transitionPending = true;
|
|
1143
|
+
const isCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
1046
1144
|
try {
|
|
1047
1145
|
await applyTelegramThreadCapability(
|
|
1048
1146
|
ctx,
|
|
1049
1147
|
true,
|
|
1050
1148
|
"thread-target-observed",
|
|
1051
1149
|
deps,
|
|
1150
|
+
isCurrent,
|
|
1052
1151
|
);
|
|
1053
1152
|
} catch (error) {
|
|
1153
|
+
if (!isCurrent()) return;
|
|
1054
1154
|
deps.recordEvent("bus", error, { phase: "thread-target-observed" });
|
|
1055
1155
|
} finally {
|
|
1056
1156
|
transitionPending = false;
|
|
@@ -1079,6 +1179,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1079
1179
|
let consecutiveDisabledProbes = 0;
|
|
1080
1180
|
const stop = (): void => {
|
|
1081
1181
|
generation += 1;
|
|
1182
|
+
deps.lifecycle?.invalidate();
|
|
1082
1183
|
if (interval) clearInterval(interval);
|
|
1083
1184
|
interval = undefined;
|
|
1084
1185
|
};
|
|
@@ -1087,7 +1188,8 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1087
1188
|
return;
|
|
1088
1189
|
}
|
|
1089
1190
|
const expectedGeneration = generation;
|
|
1090
|
-
const
|
|
1191
|
+
const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
1192
|
+
const isCurrent = (): boolean => generation === expectedGeneration && isLifecycleCurrent();
|
|
1091
1193
|
let tracked: Promise<void>;
|
|
1092
1194
|
tracked = readTelegramThreadCapability(deps)
|
|
1093
1195
|
.then(async (threadModeEnabled) => {
|
|
@@ -1103,6 +1205,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1103
1205
|
true,
|
|
1104
1206
|
"capability-monitor-retry",
|
|
1105
1207
|
deps,
|
|
1208
|
+
isCurrent,
|
|
1106
1209
|
);
|
|
1107
1210
|
}
|
|
1108
1211
|
return;
|
|
@@ -1123,6 +1226,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1123
1226
|
false,
|
|
1124
1227
|
"capability-monitor-disabled-confirmed",
|
|
1125
1228
|
deps,
|
|
1229
|
+
isCurrent,
|
|
1126
1230
|
);
|
|
1127
1231
|
return;
|
|
1128
1232
|
}
|
|
@@ -1153,6 +1257,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1153
1257
|
? "capability-monitor-disabled-confirmed"
|
|
1154
1258
|
: "capability-monitor-disabled",
|
|
1155
1259
|
deps,
|
|
1260
|
+
isCurrent,
|
|
1156
1261
|
);
|
|
1157
1262
|
})
|
|
1158
1263
|
.catch((error) => {
|
|
@@ -1526,6 +1631,19 @@ export async function runTelegramPollLoop<
|
|
|
1526
1631
|
TContext = unknown,
|
|
1527
1632
|
>(deps: TelegramPollLoopDeps<TUpdate, TContext>): Promise<void> {
|
|
1528
1633
|
if (!deps.config.botToken) return;
|
|
1634
|
+
let consecutiveGetUpdatesConflicts = 0;
|
|
1635
|
+
const retryConflict = async () => {
|
|
1636
|
+
consecutiveGetUpdatesConflicts += 1;
|
|
1637
|
+
if (consecutiveGetUpdatesConflicts >= TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT) {
|
|
1638
|
+
throw new TelegramPersistentGetUpdatesConflictError(consecutiveGetUpdatesConflicts);
|
|
1639
|
+
}
|
|
1640
|
+
await deps.sleep(
|
|
1641
|
+
consecutiveGetUpdatesConflicts < TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
|
|
1642
|
+
? TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS
|
|
1643
|
+
: TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS,
|
|
1644
|
+
deps.signal,
|
|
1645
|
+
);
|
|
1646
|
+
};
|
|
1529
1647
|
try {
|
|
1530
1648
|
await deps.deleteWebhook(deps.signal);
|
|
1531
1649
|
} catch {
|
|
@@ -1564,7 +1682,9 @@ export async function runTelegramPollLoop<
|
|
|
1564
1682
|
} catch (error) {
|
|
1565
1683
|
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
1566
1684
|
reportTelegramPollingPhase(deps, "retrying");
|
|
1567
|
-
|
|
1685
|
+
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1686
|
+
await retryConflict();
|
|
1687
|
+
} else deps.recordRuntimeEvent?.("polling", error, {
|
|
1568
1688
|
phase: "initial-sync",
|
|
1569
1689
|
...(error instanceof TelegramGetUpdatesTimeoutError
|
|
1570
1690
|
? { timeoutMs: error.timeoutMs }
|
|
@@ -1572,7 +1692,6 @@ export async function runTelegramPollLoop<
|
|
|
1572
1692
|
});
|
|
1573
1693
|
}
|
|
1574
1694
|
}
|
|
1575
|
-
let consecutiveGetUpdatesConflicts = 0;
|
|
1576
1695
|
let currentUpdateId: number | undefined;
|
|
1577
1696
|
while (!deps.signal.aborted) {
|
|
1578
1697
|
try {
|
|
@@ -1600,6 +1719,10 @@ export async function runTelegramPollLoop<
|
|
|
1600
1719
|
} catch (error) {
|
|
1601
1720
|
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
1602
1721
|
reportTelegramPollingPhase(deps, "retrying", currentUpdateId);
|
|
1722
|
+
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1723
|
+
await retryConflict();
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1603
1726
|
deps.recordRuntimeEvent?.("polling", error, {
|
|
1604
1727
|
phase:
|
|
1605
1728
|
error instanceof TelegramGetUpdatesTimeoutError
|
|
@@ -1609,17 +1732,6 @@ export async function runTelegramPollLoop<
|
|
|
1609
1732
|
? { timeoutMs: error.timeoutMs }
|
|
1610
1733
|
: {}),
|
|
1611
1734
|
});
|
|
1612
|
-
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1613
|
-
consecutiveGetUpdatesConflicts += 1;
|
|
1614
|
-
await deps.sleep(
|
|
1615
|
-
consecutiveGetUpdatesConflicts <
|
|
1616
|
-
TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
|
|
1617
|
-
? TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS
|
|
1618
|
-
: TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS,
|
|
1619
|
-
deps.signal,
|
|
1620
|
-
);
|
|
1621
|
-
continue;
|
|
1622
|
-
}
|
|
1623
1735
|
consecutiveGetUpdatesConflicts = 0;
|
|
1624
1736
|
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
|
|
1625
1737
|
await deps.sleep(TELEGRAM_POLLING_RETRY_MS, deps.signal);
|
|
@@ -152,7 +152,7 @@ type TelegramBeforeAgentStartEvent = Omit<
|
|
|
152
152
|
BeforeAgentStartEvent,
|
|
153
153
|
"systemPrompt"
|
|
154
154
|
> & {
|
|
155
|
-
systemPrompt
|
|
155
|
+
systemPrompt?: TelegramSystemPrompt | null;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
type TelegramBeforeAgentStartResult = {
|
|
@@ -165,11 +165,12 @@ type TelegramBeforeAgentStartHook = (
|
|
|
165
165
|
|
|
166
166
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
167
167
|
prompt: string;
|
|
168
|
-
systemPrompt
|
|
168
|
+
systemPrompt?: TelegramSystemPrompt | null;
|
|
169
169
|
telegramPrefix?: string;
|
|
170
170
|
localSystemPromptSuffix: string;
|
|
171
171
|
telegramTurnSystemPromptSuffix: string;
|
|
172
172
|
}): TelegramBeforeAgentStartResult {
|
|
173
|
+
const basePrompt = options.systemPrompt ?? "";
|
|
173
174
|
const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
|
|
174
175
|
const telegramHead = telegramPrefix.endsWith("]")
|
|
175
176
|
? telegramPrefix.slice(0, -1)
|
|
@@ -182,12 +183,12 @@ export function buildTelegramBridgeSystemPrompt(options: {
|
|
|
182
183
|
? `${options.telegramTurnSystemPromptSuffix}\n- The current user message came from Telegram.`
|
|
183
184
|
: "";
|
|
184
185
|
return {
|
|
185
|
-
systemPrompt: Array.isArray(
|
|
186
|
+
systemPrompt: Array.isArray(basePrompt)
|
|
186
187
|
? [
|
|
187
|
-
...
|
|
188
|
+
...basePrompt,
|
|
188
189
|
options.localSystemPromptSuffix + telegramSuffix,
|
|
189
190
|
]
|
|
190
|
-
:
|
|
191
|
+
: basePrompt +
|
|
191
192
|
options.localSystemPromptSuffix +
|
|
192
193
|
telegramSuffix,
|
|
193
194
|
};
|
|
@@ -221,8 +222,9 @@ function stripTelegramToolMetadataFromString(systemPrompt: string): string {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
function stripTelegramToolMetadataFromSystemPrompt(
|
|
224
|
-
systemPrompt: TelegramSystemPrompt,
|
|
225
|
+
systemPrompt: TelegramSystemPrompt | null | undefined,
|
|
225
226
|
): TelegramSystemPrompt {
|
|
227
|
+
if (!systemPrompt) return "";
|
|
226
228
|
return Array.isArray(systemPrompt)
|
|
227
229
|
? systemPrompt.map(stripTelegramToolMetadataFromString)
|
|
228
230
|
: stripTelegramToolMetadataFromString(systemPrompt);
|
|
@@ -1908,6 +1908,8 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1908
1908
|
if (!isDeliveryActive()) return;
|
|
1909
1909
|
if (richAttachmentDelivered) {
|
|
1910
1910
|
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1911
|
+
if (!isDeliveryActive()) return;
|
|
1912
|
+
deps.setPreviewPendingText("");
|
|
1911
1913
|
}
|
|
1912
1914
|
} catch (error) {
|
|
1913
1915
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
@@ -1938,6 +1940,8 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1938
1940
|
{ replyMarkup, target: turn.target },
|
|
1939
1941
|
);
|
|
1940
1942
|
}
|
|
1943
|
+
if (!isDeliveryActive()) return;
|
|
1944
|
+
deps.setPreviewPendingText("");
|
|
1941
1945
|
} catch (error) {
|
|
1942
1946
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1943
1947
|
phase: "final-text",
|
|
@@ -532,7 +532,7 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
|
532
532
|
function countTelegramNativeMarkdownBlocks(block: string): number {
|
|
533
533
|
if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
|
|
534
534
|
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
535
|
-
if (lines.some((line) => /^\s*([-*+] |\d+\.
|
|
535
|
+
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))) {
|
|
536
536
|
return Math.max(1, lines.length);
|
|
537
537
|
}
|
|
538
538
|
return 1;
|
|
@@ -291,6 +291,7 @@ export interface TelegramStatusBarTheme {
|
|
|
291
291
|
export interface TelegramStatusBarState {
|
|
292
292
|
hasBotToken: boolean;
|
|
293
293
|
pollingActive: boolean;
|
|
294
|
+
pollingStopReason?: string;
|
|
294
295
|
paired: boolean;
|
|
295
296
|
busRole?: TelegramBridgeBusRole;
|
|
296
297
|
busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
|
|
@@ -699,6 +700,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
699
700
|
queuedItems: queuedItemCount,
|
|
700
701
|
}),
|
|
701
702
|
queuedStatus: deps.formatQueuedStatus(queuedItems),
|
|
703
|
+
pollingStopReason: deps.getPollingState?.().stopReason,
|
|
702
704
|
error,
|
|
703
705
|
};
|
|
704
706
|
},
|
|
@@ -889,6 +891,8 @@ export function buildTelegramStatusBarText(
|
|
|
889
891
|
: "";
|
|
890
892
|
if (!state.hasBotToken)
|
|
891
893
|
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
|
|
894
|
+
if (state.pollingStopReason === "persistent-conflict" && state.busRole !== "follower")
|
|
895
|
+
return `${label} ${theme.fg("error", "error")}`;
|
|
892
896
|
if (!state.paired)
|
|
893
897
|
return `${label} ${theme.fg("warning", "awaiting pairing")}${queued}`;
|
|
894
898
|
if (state.busLifecyclePhase === "electing")
|
|
@@ -2160,6 +2160,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2160
2160
|
failurePhase: string,
|
|
2161
2161
|
error: unknown,
|
|
2162
2162
|
currentUpdateId?: number,
|
|
2163
|
+
extraDetails?: Record<string, unknown>,
|
|
2163
2164
|
): "blocked" => {
|
|
2164
2165
|
blocked = true;
|
|
2165
2166
|
state.lastFailureAtMs = getNowMs();
|
|
@@ -2168,6 +2169,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2168
2169
|
phase: failurePhase,
|
|
2169
2170
|
generation: owner?.generation,
|
|
2170
2171
|
...(currentUpdateId !== undefined ? { updateId: currentUpdateId } : {}),
|
|
2172
|
+
...extraDetails,
|
|
2171
2173
|
});
|
|
2172
2174
|
transition("blocked", currentUpdateId, blockedReason);
|
|
2173
2175
|
return "blocked";
|
|
@@ -2553,6 +2555,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2553
2555
|
`Telegram queue receipt ${normalized.receiptId} conflicts with committed authority.`,
|
|
2554
2556
|
),
|
|
2555
2557
|
currentUpdateId,
|
|
2558
|
+
{
|
|
2559
|
+
receiptId: normalized.receiptId,
|
|
2560
|
+
sourceUpdateIds: normalized.sourceUpdateIds,
|
|
2561
|
+
},
|
|
2556
2562
|
);
|
|
2557
2563
|
}
|
|
2558
2564
|
return "duplicate";
|
|
@@ -40,7 +40,9 @@ A surface is an ordered ragged sequence of rows. Each button carries:
|
|
|
40
40
|
|
|
41
41
|
- A short, distinct label.
|
|
42
42
|
- The smallest self-contained next-request prompt.
|
|
43
|
-
- Optional presentation state supported by the transport.
|
|
43
|
+
- Optional presentation state supported by the transport, including disabled controls when their visible unavailability helps explain current state.
|
|
44
|
+
|
|
45
|
+
A disabled control is not an action: it needs no prompt or selected style and must not enqueue a prompt or invoke a bound method. Prefer a meaningful label; omit it only for an intentional blank cell in a spatial layout, never as decorative padding. Preserve its label and position when that makes a changing surface easier to understand; otherwise omit irrelevant controls. Explain non-obvious unavailability without relying on color alone. Retain at least one useful enabled action, such as refresh or navigation. Derive disabled state from the same evidence as the view; an old enabled control still requires current domain validation. Use the transport owner's disabled encoding rather than a dummy prompt or no-op callback.
|
|
44
46
|
|
|
45
47
|
Prompts must name any target, operation, constraint, or freshness identity whose omission could change the action. Reuse visible context only when it remains unambiguous under delayed or reordered clicks. Never encode volatile output that should be freshly inspected.
|
|
46
48
|
|
|
@@ -57,7 +59,7 @@ Every generated human-readable action label must use `emoji + space + text`; emo
|
|
|
57
59
|
|
|
58
60
|
For complex grids, navigation collections, or stateful repeated clicks, read [`references/layout-and-state.md`](./references/layout-and-state.md).
|
|
59
61
|
|
|
60
|
-
Serialize the resulting rows with the active transport contract. This Skill owns admission and composition, not transport syntax.
|
|
62
|
+
Place a control group beside the section it governs when the transport supports in-body blocks; use a footer for whole-answer actions. Placement must not change the matrix or action semantics. Serialize the resulting rows with the active transport contract. This Skill owns admission and composition, not transport syntax.
|
|
61
63
|
|
|
62
64
|
## Safety
|
|
63
65
|
|