@llblab/pi-telegram 0.17.5 → 0.18.0
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 +67 -32
- package/BACKLOG.md +59 -19
- package/CHANGELOG.md +36 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/status.ts
CHANGED
|
@@ -103,10 +103,80 @@ export interface TelegramRuntimeEventRecorderOptions {
|
|
|
103
103
|
now?: () => number;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export interface TelegramBridgeStatusBusFollower {
|
|
107
|
+
instanceId: string;
|
|
108
|
+
cwd?: string;
|
|
109
|
+
lastHeartbeatMs: number;
|
|
110
|
+
target?: { chatId: number; threadId?: number };
|
|
111
|
+
slot?: string;
|
|
112
|
+
threadName?: string;
|
|
113
|
+
status?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface TelegramBridgeStatusTopicTarget {
|
|
117
|
+
instanceId?: string;
|
|
118
|
+
status?: string;
|
|
119
|
+
target?: { chatId: number; threadId?: number };
|
|
120
|
+
slot?: string;
|
|
121
|
+
threadName?: string;
|
|
122
|
+
syncStatus?: string;
|
|
123
|
+
lastSyncObservedAtMs?: number;
|
|
124
|
+
lastSyncProbeAtMs?: number;
|
|
125
|
+
lastSyncError?: string;
|
|
126
|
+
lastReconcileAction?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TelegramBridgeStatusThreadReservation {
|
|
130
|
+
target?: { chatId: number; threadId?: number };
|
|
131
|
+
slot?: string;
|
|
132
|
+
reason?: string;
|
|
133
|
+
instanceId?: string;
|
|
134
|
+
expiresAtMs?: number;
|
|
135
|
+
lastReconcileAction?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TelegramBridgeStatusSyncObservation {
|
|
139
|
+
target?: { chatId: number; threadId?: number };
|
|
140
|
+
syncStatus: string;
|
|
141
|
+
observedAtMs: number;
|
|
142
|
+
instanceId?: string;
|
|
143
|
+
slot?: string;
|
|
144
|
+
lastSyncError?: string;
|
|
145
|
+
lastReconcileAction?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface TelegramBridgeStatusSyncSlice {
|
|
149
|
+
status: string;
|
|
150
|
+
updatedAtMs?: number;
|
|
151
|
+
suspectAtMs?: number;
|
|
152
|
+
reason?: string;
|
|
153
|
+
lastReconcileAction?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface TelegramBridgeThreadReconciliationState {
|
|
157
|
+
phase: string;
|
|
158
|
+
event: string;
|
|
159
|
+
atMs: number;
|
|
160
|
+
leaderEpoch?: number | string;
|
|
161
|
+
pendingProvisionCount: number;
|
|
162
|
+
syncActionCount: number;
|
|
163
|
+
cleanupActionCount: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type TelegramBridgeBusRole = "leader" | "follower";
|
|
167
|
+
export type TelegramBridgeBusLifecyclePhase = "electing";
|
|
168
|
+
|
|
106
169
|
export interface TelegramBridgeStatusLineState {
|
|
107
170
|
hasBotToken?: boolean;
|
|
108
171
|
botUsername?: string;
|
|
109
172
|
allowedUserId?: number;
|
|
173
|
+
botThreadMode?: "unknown" | "enabled" | "disabled";
|
|
174
|
+
botThreadModeUpdatedAtMs?: number;
|
|
175
|
+
botThreadModeAction?: string;
|
|
176
|
+
busRole?: TelegramBridgeBusRole;
|
|
177
|
+
busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
|
|
178
|
+
instanceSlot?: string;
|
|
179
|
+
instanceThreadName?: string;
|
|
110
180
|
lockState?: string;
|
|
111
181
|
pollingActive: boolean;
|
|
112
182
|
lastUpdateId?: number;
|
|
@@ -116,6 +186,13 @@ export interface TelegramBridgeStatusLineState {
|
|
|
116
186
|
activeToolExecutions: number;
|
|
117
187
|
pendingModelSwitch: boolean;
|
|
118
188
|
queuedItems: Array<{ queueLane: TelegramStatusQueueLane }>;
|
|
189
|
+
busFollowers?: TelegramBridgeStatusBusFollower[];
|
|
190
|
+
topicTargets?: TelegramBridgeStatusTopicTarget[];
|
|
191
|
+
threadReservations?: TelegramBridgeStatusThreadReservation[];
|
|
192
|
+
topicSyncObservations?: TelegramBridgeStatusSyncObservation[];
|
|
193
|
+
syncState?: Record<string, TelegramBridgeStatusSyncSlice | undefined>;
|
|
194
|
+
threadReconciliation?: TelegramBridgeThreadReconciliationState;
|
|
195
|
+
busNowMs?: number;
|
|
119
196
|
recentRuntimeEvents: TelegramRuntimeEvent[];
|
|
120
197
|
}
|
|
121
198
|
|
|
@@ -130,6 +207,10 @@ export interface TelegramStatusBarState {
|
|
|
130
207
|
hasBotToken: boolean;
|
|
131
208
|
pollingActive: boolean;
|
|
132
209
|
paired: boolean;
|
|
210
|
+
busRole?: TelegramBridgeBusRole;
|
|
211
|
+
busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
|
|
212
|
+
instanceSlot?: string;
|
|
213
|
+
instanceThreadName?: string;
|
|
133
214
|
compactionInProgress: boolean;
|
|
134
215
|
processing: boolean;
|
|
135
216
|
processingStatus?: string;
|
|
@@ -175,13 +256,41 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
175
256
|
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
176
257
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
177
258
|
getRuntimeLockState?: () => string;
|
|
259
|
+
getBusRole?: () => TelegramBridgeBusRole | undefined;
|
|
260
|
+
getBusLifecyclePhase?: () => TelegramBridgeBusLifecyclePhase | undefined;
|
|
261
|
+
getBotThreadMode?: () =>
|
|
262
|
+
| {
|
|
263
|
+
threadMode: "unknown" | "enabled" | "disabled";
|
|
264
|
+
updatedAtMs?: number;
|
|
265
|
+
lastReconcileAction?: string;
|
|
266
|
+
}
|
|
267
|
+
| undefined;
|
|
268
|
+
getBusFollowers?: () => TelegramBridgeStatusBusFollower[];
|
|
269
|
+
getTopicTargets?: () => TelegramBridgeStatusTopicTarget[];
|
|
270
|
+
getThreadReservations?: () => TelegramBridgeStatusThreadReservation[];
|
|
271
|
+
getTopicSyncObservations?: () => TelegramBridgeStatusSyncObservation[];
|
|
272
|
+
getSyncState?: () => Record<
|
|
273
|
+
string,
|
|
274
|
+
TelegramBridgeStatusSyncSlice | undefined
|
|
275
|
+
>;
|
|
276
|
+
getThreadReconciliationState?: () =>
|
|
277
|
+
| TelegramBridgeThreadReconciliationState
|
|
278
|
+
| undefined;
|
|
279
|
+
getInstanceSlot?: () => string | undefined;
|
|
280
|
+
getInstanceThreadName?: () => string | undefined;
|
|
281
|
+
getNowMs?: () => number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface TelegramBridgeStatusLineOptions {
|
|
285
|
+
verbose?: boolean;
|
|
178
286
|
}
|
|
179
287
|
|
|
180
288
|
export interface TelegramStatusRuntime<
|
|
181
289
|
TContext extends TelegramStatusRuntimeContext,
|
|
182
290
|
> {
|
|
183
291
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
184
|
-
getStatusLines: () => string[];
|
|
292
|
+
getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
|
|
293
|
+
getStatusState: () => TelegramBridgeStatusLineState;
|
|
185
294
|
}
|
|
186
295
|
|
|
187
296
|
function truncateTelegramRuntimeEventText(
|
|
@@ -421,11 +530,13 @@ export function buildTelegramRuntimeEventLines(
|
|
|
421
530
|
export function createTelegramStatusHtmlBuilder<TContext>(deps: {
|
|
422
531
|
getActiveModel: (ctx: TContext) => TelegramStatusActiveModel | undefined;
|
|
423
532
|
isCompactionInProgress?: () => boolean;
|
|
533
|
+
getBridgeStatusLineState?: () => TelegramBridgeStatusLineState;
|
|
424
534
|
}): (ctx: TContext & TelegramStatusContext) => string {
|
|
425
535
|
return (ctx) =>
|
|
426
536
|
buildStatusHtml(
|
|
427
537
|
{ ...ctx, isCompactionInProgress: deps.isCompactionInProgress },
|
|
428
538
|
deps.getActiveModel(ctx),
|
|
539
|
+
deps.getBridgeStatusLineState?.(),
|
|
429
540
|
);
|
|
430
541
|
}
|
|
431
542
|
|
|
@@ -443,8 +554,9 @@ export function createTelegramStatusRuntime<
|
|
|
443
554
|
),
|
|
444
555
|
);
|
|
445
556
|
},
|
|
446
|
-
getStatusLines: () =>
|
|
447
|
-
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState()),
|
|
557
|
+
getStatusLines: (options) =>
|
|
558
|
+
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options),
|
|
559
|
+
getStatusState: deps.getBridgeStatusLineState,
|
|
448
560
|
};
|
|
449
561
|
}
|
|
450
562
|
|
|
@@ -468,6 +580,10 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
468
580
|
hasBotToken: !!config.botToken,
|
|
469
581
|
pollingActive: deps.isPollingActive(),
|
|
470
582
|
paired: !!config.allowedUserId,
|
|
583
|
+
busRole: deps.getBusRole?.(),
|
|
584
|
+
busLifecyclePhase: deps.getBusLifecyclePhase?.(),
|
|
585
|
+
instanceSlot: deps.getInstanceSlot?.(),
|
|
586
|
+
instanceThreadName: deps.getInstanceThreadName?.(),
|
|
471
587
|
compactionInProgress,
|
|
472
588
|
processing:
|
|
473
589
|
hasActiveTurn ||
|
|
@@ -488,10 +604,18 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
488
604
|
},
|
|
489
605
|
getBridgeStatusLineState: () => {
|
|
490
606
|
const config = deps.getConfig();
|
|
607
|
+
const botThreadMode = deps.getBotThreadMode?.();
|
|
491
608
|
return {
|
|
492
609
|
hasBotToken: Boolean(config.botToken),
|
|
493
610
|
botUsername: config.botUsername,
|
|
494
611
|
allowedUserId: config.allowedUserId,
|
|
612
|
+
botThreadMode: botThreadMode?.threadMode,
|
|
613
|
+
botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
|
|
614
|
+
botThreadModeAction: botThreadMode?.lastReconcileAction,
|
|
615
|
+
busRole: deps.getBusRole?.(),
|
|
616
|
+
busLifecyclePhase: deps.getBusLifecyclePhase?.(),
|
|
617
|
+
instanceSlot: deps.getInstanceSlot?.(),
|
|
618
|
+
instanceThreadName: deps.getInstanceThreadName?.(),
|
|
495
619
|
lockState: deps.getRuntimeLockState?.(),
|
|
496
620
|
pollingActive: deps.isPollingActive(),
|
|
497
621
|
lastUpdateId: config.lastUpdateId,
|
|
@@ -501,12 +625,96 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
501
625
|
activeToolExecutions: deps.getActiveToolExecutions(),
|
|
502
626
|
pendingModelSwitch: deps.hasPendingModelSwitch(),
|
|
503
627
|
queuedItems: deps.getQueuedItems(),
|
|
628
|
+
busFollowers: deps.getBusFollowers?.(),
|
|
629
|
+
topicTargets: deps.getTopicTargets?.(),
|
|
630
|
+
threadReservations: deps.getThreadReservations?.(),
|
|
631
|
+
topicSyncObservations: deps.getTopicSyncObservations?.(),
|
|
632
|
+
syncState: deps.getSyncState?.(),
|
|
633
|
+
threadReconciliation: deps.getThreadReconciliationState?.(),
|
|
634
|
+
busNowMs: deps.getNowMs?.(),
|
|
504
635
|
recentRuntimeEvents: deps.getRecentRuntimeEvents(),
|
|
505
636
|
};
|
|
506
637
|
},
|
|
507
638
|
});
|
|
508
639
|
}
|
|
509
640
|
|
|
641
|
+
export interface TelegramRuntimeLogScope extends Record<string, unknown> {
|
|
642
|
+
instanceId: string;
|
|
643
|
+
role: string;
|
|
644
|
+
slot?: string;
|
|
645
|
+
threadName?: string;
|
|
646
|
+
lockState?: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function createTelegramRuntimeLogScope(input: {
|
|
650
|
+
state: TelegramBridgeStatusLineState;
|
|
651
|
+
instanceId: string;
|
|
652
|
+
}): TelegramRuntimeLogScope {
|
|
653
|
+
return {
|
|
654
|
+
instanceId: input.instanceId,
|
|
655
|
+
role: input.state.busRole ?? "classic-or-disconnected",
|
|
656
|
+
slot: input.state.instanceSlot,
|
|
657
|
+
threadName: input.state.instanceThreadName,
|
|
658
|
+
lockState: input.state.lockState,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function createTelegramStatusSnapshot(
|
|
663
|
+
state: TelegramBridgeStatusLineState,
|
|
664
|
+
): {
|
|
665
|
+
runtime: Record<string, unknown>;
|
|
666
|
+
liveRoster: Record<string, unknown>;
|
|
667
|
+
diagnostics: Record<string, unknown>;
|
|
668
|
+
} {
|
|
669
|
+
return {
|
|
670
|
+
runtime: {
|
|
671
|
+
busRole: state.busRole,
|
|
672
|
+
...(state.busLifecyclePhase
|
|
673
|
+
? { busLifecyclePhase: state.busLifecyclePhase }
|
|
674
|
+
: {}),
|
|
675
|
+
botThreadMode: state.botThreadMode,
|
|
676
|
+
botThreadModeUpdatedAtMs: state.botThreadModeUpdatedAtMs,
|
|
677
|
+
botThreadModeAction: state.botThreadModeAction,
|
|
678
|
+
instanceSlot: state.instanceSlot,
|
|
679
|
+
instanceThreadName: state.instanceThreadName,
|
|
680
|
+
pollingActive: state.pollingActive,
|
|
681
|
+
lockState: state.lockState,
|
|
682
|
+
},
|
|
683
|
+
liveRoster: {
|
|
684
|
+
busFollowers: state.busFollowers ?? [],
|
|
685
|
+
topicTargets: state.topicTargets ?? [],
|
|
686
|
+
reservations: state.threadReservations ?? [],
|
|
687
|
+
syncObservations: state.topicSyncObservations ?? [],
|
|
688
|
+
},
|
|
689
|
+
diagnostics: {
|
|
690
|
+
pendingDispatch: state.pendingDispatch,
|
|
691
|
+
compactionInProgress: state.compactionInProgress,
|
|
692
|
+
activeToolExecutions: state.activeToolExecutions,
|
|
693
|
+
pendingModelSwitch: state.pendingModelSwitch,
|
|
694
|
+
syncState: state.syncState,
|
|
695
|
+
threadReconciliation: state.threadReconciliation,
|
|
696
|
+
recentRuntimeEvents: state.recentRuntimeEvents,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
|
|
702
|
+
persistSnapshot: () => Promise<void>;
|
|
703
|
+
recordError: (error: unknown) => void;
|
|
704
|
+
setTimer?: (callback: () => void, ms: number) => { unref?: () => void };
|
|
705
|
+
}): () => void {
|
|
706
|
+
const setTimer = deps.setTimer ?? setTimeout;
|
|
707
|
+
let timer: { unref?: () => void } | number | undefined;
|
|
708
|
+
return () => {
|
|
709
|
+
if (timer) return;
|
|
710
|
+
timer = setTimer(() => {
|
|
711
|
+
timer = undefined;
|
|
712
|
+
void deps.persistSnapshot().catch(deps.recordError);
|
|
713
|
+
}, 0);
|
|
714
|
+
if (typeof timer !== "number") timer?.unref?.();
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
510
718
|
export function getTelegramStatusBarProcessingStatus(state: {
|
|
511
719
|
hasActiveTurn: boolean;
|
|
512
720
|
hasPendingDispatch: boolean;
|
|
@@ -515,19 +723,25 @@ export function getTelegramStatusBarProcessingStatus(state: {
|
|
|
515
723
|
queuedItems: number;
|
|
516
724
|
}): string | undefined {
|
|
517
725
|
if (state.hasPendingModelSwitch) return "model";
|
|
518
|
-
if (state.hasActiveTurn && state.activeToolExecutions > 0)
|
|
519
|
-
return "tool running";
|
|
520
726
|
if (state.hasActiveTurn || state.activeToolExecutions > 0) return "active";
|
|
521
727
|
if (state.hasPendingDispatch) return "dispatching";
|
|
522
728
|
if (state.queuedItems > 0) return "queued";
|
|
523
729
|
return undefined;
|
|
524
730
|
}
|
|
525
731
|
|
|
732
|
+
function getTelegramStatusBarLabel(state: TelegramStatusBarState): string {
|
|
733
|
+
const threadName = state.instanceThreadName?.trim();
|
|
734
|
+
if (!threadName) return "telegram";
|
|
735
|
+
const genericLabels = new Set(["telegram", "leader", "follower"]);
|
|
736
|
+
if (genericLabels.has(threadName.toLowerCase())) return "telegram";
|
|
737
|
+
return threadName;
|
|
738
|
+
}
|
|
739
|
+
|
|
526
740
|
export function buildTelegramStatusBarText(
|
|
527
741
|
theme: TelegramStatusBarTheme,
|
|
528
742
|
state: TelegramStatusBarState,
|
|
529
743
|
): string {
|
|
530
|
-
const label = theme.fg("accent",
|
|
744
|
+
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
531
745
|
if (state.error) {
|
|
532
746
|
return `${label} ${theme.fg("error", "error")} ${theme.fg("muted", state.error)}`;
|
|
533
747
|
}
|
|
@@ -536,10 +750,14 @@ export function buildTelegramStatusBarText(
|
|
|
536
750
|
: "";
|
|
537
751
|
if (!state.hasBotToken)
|
|
538
752
|
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
|
|
539
|
-
if (!state.pollingActive)
|
|
540
|
-
return `${label} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
541
753
|
if (!state.paired)
|
|
542
754
|
return `${label} ${theme.fg("warning", "awaiting pairing")}${queued}`;
|
|
755
|
+
if (state.busLifecyclePhase === "electing")
|
|
756
|
+
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
757
|
+
if (!state.pollingActive && state.busRole !== "follower")
|
|
758
|
+
return `${label} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
759
|
+
if (state.busRole === "follower")
|
|
760
|
+
return `${label} ${theme.fg("success", "follower")}${queued}`;
|
|
543
761
|
if (state.compactionInProgress) {
|
|
544
762
|
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
545
763
|
}
|
|
@@ -551,6 +769,8 @@ export function buildTelegramStatusBarText(
|
|
|
551
769
|
processingStatus === "active" ? "warning" : "accent";
|
|
552
770
|
return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
|
|
553
771
|
}
|
|
772
|
+
if (state.busRole === "leader")
|
|
773
|
+
return `${label} ${theme.fg("success", "leader")}`;
|
|
554
774
|
return `${label} ${theme.fg("success", "connected")}`;
|
|
555
775
|
}
|
|
556
776
|
|
|
@@ -561,8 +781,282 @@ function formatTelegramBridgeBotStatus(
|
|
|
561
781
|
return state.hasBotToken ? "unknown" : "not configured";
|
|
562
782
|
}
|
|
563
783
|
|
|
784
|
+
function formatTelegramStatusTarget(
|
|
785
|
+
target: { chatId: number; threadId?: number } | undefined,
|
|
786
|
+
): string {
|
|
787
|
+
if (!target) return "";
|
|
788
|
+
return target.threadId === undefined
|
|
789
|
+
? ` target ${target.chatId}`
|
|
790
|
+
: ` target ${target.chatId}:${target.threadId}`;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function formatTelegramThreadStatusLabel(input: {
|
|
794
|
+
threadName?: string;
|
|
795
|
+
slot?: string;
|
|
796
|
+
}): string {
|
|
797
|
+
const threadName = input.threadName?.trim();
|
|
798
|
+
if (threadName) return threadName;
|
|
799
|
+
return input.slot ? `[${input.slot}]` : "";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function buildTelegramBusFollowerLines(
|
|
803
|
+
state: Pick<TelegramBridgeStatusLineState, "busFollowers" | "busNowMs">,
|
|
804
|
+
): string[] {
|
|
805
|
+
const followers = state.busFollowers ?? [];
|
|
806
|
+
if (followers.length === 0) return [];
|
|
807
|
+
const nowMs = state.busNowMs ?? Date.now();
|
|
808
|
+
return [
|
|
809
|
+
"",
|
|
810
|
+
"bus:",
|
|
811
|
+
`- followers: ${followers.length}`,
|
|
812
|
+
...followers.map((follower) => {
|
|
813
|
+
const ageSeconds = Math.max(
|
|
814
|
+
0,
|
|
815
|
+
Math.round((nowMs - follower.lastHeartbeatMs) / 1000),
|
|
816
|
+
);
|
|
817
|
+
const label = formatTelegramThreadStatusLabel(follower);
|
|
818
|
+
const labelSuffix = label ? ` ${label}` : "";
|
|
819
|
+
const statusLabel = follower.status ? ` (${follower.status})` : "";
|
|
820
|
+
const cwd = follower.cwd ? ` ${follower.cwd}` : "";
|
|
821
|
+
const target = formatTelegramStatusTarget(follower.target);
|
|
822
|
+
return `- ${follower.instanceId}:${labelSuffix} heartbeat ${ageSeconds}s ago${statusLabel}${target}${cwd}`;
|
|
823
|
+
}),
|
|
824
|
+
];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function buildTelegramSyncSliceLines(
|
|
828
|
+
state: Pick<TelegramBridgeStatusLineState, "syncState">,
|
|
829
|
+
): string[] {
|
|
830
|
+
const syncState = state.syncState;
|
|
831
|
+
if (!syncState || Object.keys(syncState).length === 0) return [];
|
|
832
|
+
return [
|
|
833
|
+
"sync:",
|
|
834
|
+
...Object.entries(syncState).map(([slice, value]) => {
|
|
835
|
+
const status = value?.status ?? "unknown";
|
|
836
|
+
const action = value?.lastReconcileAction
|
|
837
|
+
? ` reconcile=${value.lastReconcileAction}`
|
|
838
|
+
: "";
|
|
839
|
+
const reason = value?.reason ? ` reason=${value.reason}` : "";
|
|
840
|
+
return `- ${slice}: ${status}${action}${reason}`;
|
|
841
|
+
}),
|
|
842
|
+
];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function buildTelegramThreadReconciliationLines(
|
|
846
|
+
state: Pick<TelegramBridgeStatusLineState, "threadReconciliation">,
|
|
847
|
+
): string[] {
|
|
848
|
+
const reconciliation = state.threadReconciliation;
|
|
849
|
+
if (!reconciliation) return [];
|
|
850
|
+
const epoch =
|
|
851
|
+
reconciliation.leaderEpoch !== undefined
|
|
852
|
+
? ` epoch=${reconciliation.leaderEpoch}`
|
|
853
|
+
: "";
|
|
854
|
+
return [
|
|
855
|
+
"reconciliation:",
|
|
856
|
+
`- phase: ${reconciliation.phase} event=${reconciliation.event}${epoch}`,
|
|
857
|
+
`- counts: pending=${reconciliation.pendingProvisionCount}, sync=${reconciliation.syncActionCount}, cleanup=${reconciliation.cleanupActionCount}`,
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function buildTelegramTopicTargetDiagnosticLines(
|
|
862
|
+
state: Pick<
|
|
863
|
+
TelegramBridgeStatusLineState,
|
|
864
|
+
"topicTargets" | "threadReservations" | "topicSyncObservations"
|
|
865
|
+
>,
|
|
866
|
+
): string[] {
|
|
867
|
+
const activeTargets = (state.topicTargets ?? []).filter(
|
|
868
|
+
(record) =>
|
|
869
|
+
!!record.instanceId &&
|
|
870
|
+
(record.status === "active" || record.status === "starting"),
|
|
871
|
+
);
|
|
872
|
+
const reservations = state.threadReservations ?? [];
|
|
873
|
+
const observations = state.topicSyncObservations ?? [];
|
|
874
|
+
if (
|
|
875
|
+
activeTargets.length === 0 &&
|
|
876
|
+
reservations.length === 0 &&
|
|
877
|
+
observations.length === 0
|
|
878
|
+
)
|
|
879
|
+
return [];
|
|
880
|
+
const byInstance = new Map<string, TelegramBridgeStatusTopicTarget[]>();
|
|
881
|
+
for (const record of activeTargets) {
|
|
882
|
+
const key = record.instanceId;
|
|
883
|
+
if (!key) continue;
|
|
884
|
+
const records = byInstance.get(key) ?? [];
|
|
885
|
+
records.push(record);
|
|
886
|
+
byInstance.set(key, records);
|
|
887
|
+
}
|
|
888
|
+
const duplicateLines = Array.from(byInstance.entries())
|
|
889
|
+
.filter(([, records]) => records.length > 1)
|
|
890
|
+
.map(([instanceId, records]) => {
|
|
891
|
+
const targets = records
|
|
892
|
+
.map((record) => {
|
|
893
|
+
const label = formatTelegramThreadStatusLabel(record);
|
|
894
|
+
return `${label}${formatTelegramStatusTarget(record.target) || " unknown"}`.trim();
|
|
895
|
+
})
|
|
896
|
+
.join(", ");
|
|
897
|
+
return `- duplicate ${instanceId}: ${records.length} active threads ${targets}`;
|
|
898
|
+
});
|
|
899
|
+
const twinLines = activeTargets.map((record) => {
|
|
900
|
+
const label = formatTelegramThreadStatusLabel(record);
|
|
901
|
+
const target = formatTelegramStatusTarget(record.target) || " unknown";
|
|
902
|
+
const sync = record.syncStatus ? ` sync=${record.syncStatus}` : "";
|
|
903
|
+
const observed = record.lastSyncObservedAtMs
|
|
904
|
+
? ` observed=${new Date(record.lastSyncObservedAtMs).toISOString()}`
|
|
905
|
+
: "";
|
|
906
|
+
const probe = record.lastSyncProbeAtMs
|
|
907
|
+
? ` probed=${new Date(record.lastSyncProbeAtMs).toISOString()}`
|
|
908
|
+
: "";
|
|
909
|
+
const error = record.lastSyncError
|
|
910
|
+
? ` syncError=${record.lastSyncError}`
|
|
911
|
+
: "";
|
|
912
|
+
const action = record.lastReconcileAction
|
|
913
|
+
? ` reconcile=${record.lastReconcileAction}`
|
|
914
|
+
: "";
|
|
915
|
+
return `- ${label}${target}${sync}${observed}${probe}${error}${action}`.trim();
|
|
916
|
+
});
|
|
917
|
+
const reservationLines = reservations.map((reservation) => {
|
|
918
|
+
const slot = reservation.slot ? `[${reservation.slot}]` : "";
|
|
919
|
+
const target = formatTelegramStatusTarget(reservation.target) || " unknown";
|
|
920
|
+
const reason = reservation.reason ? ` reason=${reservation.reason}` : "";
|
|
921
|
+
const instance = reservation.instanceId
|
|
922
|
+
? ` instance=${reservation.instanceId}`
|
|
923
|
+
: "";
|
|
924
|
+
const action = reservation.lastReconcileAction
|
|
925
|
+
? ` reconcile=${reservation.lastReconcileAction}`
|
|
926
|
+
: "";
|
|
927
|
+
return `- reservation ${slot}${target}${reason}${instance}${action}`.trim();
|
|
928
|
+
});
|
|
929
|
+
const observationLines = observations.map((observation) => {
|
|
930
|
+
const slot = observation.slot ? `[${observation.slot}]` : "";
|
|
931
|
+
const target = formatTelegramStatusTarget(observation.target) || " unknown";
|
|
932
|
+
const observed = ` observed=${new Date(observation.observedAtMs).toISOString()}`;
|
|
933
|
+
const instance = observation.instanceId
|
|
934
|
+
? ` instance=${observation.instanceId}`
|
|
935
|
+
: "";
|
|
936
|
+
const error = observation.lastSyncError
|
|
937
|
+
? ` syncError=${observation.lastSyncError}`
|
|
938
|
+
: "";
|
|
939
|
+
const action = observation.lastReconcileAction
|
|
940
|
+
? ` reconcile=${observation.lastReconcileAction}`
|
|
941
|
+
: "";
|
|
942
|
+
return `- sync ${slot}${target} sync=${observation.syncStatus}${observed}${instance}${error}${action}`.trim();
|
|
943
|
+
});
|
|
944
|
+
return [
|
|
945
|
+
"topics:",
|
|
946
|
+
`- active bindings: instances=${byInstance.size}, targets=${activeTargets.length}`,
|
|
947
|
+
...duplicateLines,
|
|
948
|
+
...twinLines,
|
|
949
|
+
...reservationLines,
|
|
950
|
+
...observationLines,
|
|
951
|
+
];
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function buildTelegramBridgeCompactThreadLines(
|
|
955
|
+
state: Pick<
|
|
956
|
+
TelegramBridgeStatusLineState,
|
|
957
|
+
| "busFollowers"
|
|
958
|
+
| "topicTargets"
|
|
959
|
+
| "threadReservations"
|
|
960
|
+
| "topicSyncObservations"
|
|
961
|
+
>,
|
|
962
|
+
): string[] {
|
|
963
|
+
const activeTargets = (state.topicTargets ?? []).filter(
|
|
964
|
+
(record) =>
|
|
965
|
+
!!record.instanceId &&
|
|
966
|
+
(record.status === "active" || record.status === "starting"),
|
|
967
|
+
);
|
|
968
|
+
const activeLabels = activeTargets
|
|
969
|
+
.map(formatTelegramThreadStatusLabel)
|
|
970
|
+
.filter((label) => label.length > 0);
|
|
971
|
+
const followers = state.busFollowers ?? [];
|
|
972
|
+
const reservations = state.threadReservations ?? [];
|
|
973
|
+
const observations = state.topicSyncObservations ?? [];
|
|
974
|
+
if (
|
|
975
|
+
activeLabels.length === 0 &&
|
|
976
|
+
followers.length === 0 &&
|
|
977
|
+
reservations.length === 0 &&
|
|
978
|
+
observations.length === 0
|
|
979
|
+
) {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
const lines = ["threads:"];
|
|
983
|
+
if (activeLabels.length > 0)
|
|
984
|
+
lines.push(`- active: ${activeLabels.join(", ")}`);
|
|
985
|
+
if (followers.length > 0) lines.push(`- followers: ${followers.length}`);
|
|
986
|
+
if (reservations.length > 0) lines.push(`- reserved: ${reservations.length}`);
|
|
987
|
+
const syncIssueCount = observations.filter(
|
|
988
|
+
(observation) => observation.syncStatus !== "open",
|
|
989
|
+
).length;
|
|
990
|
+
if (syncIssueCount > 0) lines.push(`- sync issues: ${syncIssueCount}`);
|
|
991
|
+
return lines;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function buildTelegramBridgeCompactStatusLines(
|
|
995
|
+
state: TelegramBridgeStatusLineState,
|
|
996
|
+
): string[] {
|
|
997
|
+
const controlQueueCount = state.queuedItems.filter(
|
|
998
|
+
(item) => item.queueLane === "control",
|
|
999
|
+
).length;
|
|
1000
|
+
const priorityQueueCount = state.queuedItems.filter(
|
|
1001
|
+
(item) => item.queueLane === "priority",
|
|
1002
|
+
).length;
|
|
1003
|
+
const defaultQueueCount = state.queuedItems.filter(
|
|
1004
|
+
(item) => item.queueLane === "default",
|
|
1005
|
+
).length;
|
|
1006
|
+
const queueLine = `- queued turns: ${state.queuedItems.length}${
|
|
1007
|
+
state.queuedItems.length > 0
|
|
1008
|
+
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
1009
|
+
: ""
|
|
1010
|
+
}`;
|
|
1011
|
+
const executionState = state.compactionInProgress
|
|
1012
|
+
? "compacting"
|
|
1013
|
+
: state.pendingDispatch
|
|
1014
|
+
? "pending dispatch"
|
|
1015
|
+
: state.activeSourceMessageIds?.length
|
|
1016
|
+
? "active"
|
|
1017
|
+
: "idle";
|
|
1018
|
+
return [
|
|
1019
|
+
"connection:",
|
|
1020
|
+
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
1021
|
+
`- user: ${state.allowedUserId ?? "not paired"}`,
|
|
1022
|
+
...(state.botThreadMode ? [`- thread mode: ${state.botThreadMode}`] : []),
|
|
1023
|
+
...(state.busRole ? [`- role: ${state.busRole}`] : []),
|
|
1024
|
+
...(state.busLifecyclePhase
|
|
1025
|
+
? [`- lifecycle: ${state.busLifecyclePhase}`]
|
|
1026
|
+
: []),
|
|
1027
|
+
...(state.instanceThreadName || state.instanceSlot
|
|
1028
|
+
? [`- instance: ${state.instanceThreadName ?? state.instanceSlot}`]
|
|
1029
|
+
: []),
|
|
1030
|
+
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
|
|
1031
|
+
"",
|
|
1032
|
+
"health:",
|
|
1033
|
+
`- polling: ${state.pollingActive ? "running" : "stopped"}`,
|
|
1034
|
+
`- state: ${executionState}`,
|
|
1035
|
+
queueLine,
|
|
1036
|
+
...(state.activeToolExecutions > 0
|
|
1037
|
+
? [`- active tools: ${state.activeToolExecutions}`]
|
|
1038
|
+
: []),
|
|
1039
|
+
...(state.pendingModelSwitch ? ["- pending model switch: yes"] : []),
|
|
1040
|
+
...buildTelegramBridgeCompactThreadLines(state),
|
|
1041
|
+
...buildTelegramThreadReconciliationLines(state),
|
|
1042
|
+
"",
|
|
1043
|
+
"diagnostics:",
|
|
1044
|
+
"- state: ~/.pi/agent/tmp/telegram/state.json",
|
|
1045
|
+
"- logs: ~/.pi/agent/tmp/telegram/logs.jsonl",
|
|
1046
|
+
"- full dump: /telegram-status --debug",
|
|
1047
|
+
];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
564
1050
|
export function buildTelegramBridgeStatusLines(
|
|
565
1051
|
state: TelegramBridgeStatusLineState,
|
|
1052
|
+
options: TelegramBridgeStatusLineOptions = {},
|
|
1053
|
+
): string[] {
|
|
1054
|
+
if (options.verbose) return buildTelegramBridgeDiagnosticStatusLines(state);
|
|
1055
|
+
return buildTelegramBridgeCompactStatusLines(state);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export function buildTelegramBridgeDiagnosticStatusLines(
|
|
1059
|
+
state: TelegramBridgeStatusLineState,
|
|
566
1060
|
): string[] {
|
|
567
1061
|
const controlQueueCount = state.queuedItems.filter(
|
|
568
1062
|
(item) => item.queueLane === "control",
|
|
@@ -577,6 +1071,18 @@ export function buildTelegramBridgeStatusLines(
|
|
|
577
1071
|
"connection:",
|
|
578
1072
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
579
1073
|
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
|
|
1074
|
+
...(state.botThreadMode
|
|
1075
|
+
? [
|
|
1076
|
+
`- thread mode: ${state.botThreadMode}${state.botThreadModeAction ? ` reconcile=${state.botThreadModeAction}` : ""}`,
|
|
1077
|
+
]
|
|
1078
|
+
: []),
|
|
1079
|
+
...(state.busRole ? [`- bus role: ${state.busRole}`] : []),
|
|
1080
|
+
...(state.busLifecyclePhase
|
|
1081
|
+
? [`- bus lifecycle: ${state.busLifecyclePhase}`]
|
|
1082
|
+
: []),
|
|
1083
|
+
...(state.instanceThreadName || state.instanceSlot
|
|
1084
|
+
? [`- instance: ${state.instanceThreadName ?? state.instanceSlot}`]
|
|
1085
|
+
: []),
|
|
580
1086
|
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
|
|
581
1087
|
"",
|
|
582
1088
|
"polling:",
|
|
@@ -593,6 +1099,10 @@ export function buildTelegramBridgeStatusLines(
|
|
|
593
1099
|
"queue:",
|
|
594
1100
|
`- queued turns: ${state.queuedItems.length}`,
|
|
595
1101
|
`- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1102
|
+
...buildTelegramBusFollowerLines(state),
|
|
1103
|
+
...buildTelegramTopicTargetDiagnosticLines(state),
|
|
1104
|
+
...buildTelegramThreadReconciliationLines(state),
|
|
1105
|
+
...buildTelegramSyncSliceLines(state),
|
|
596
1106
|
"",
|
|
597
1107
|
...buildTelegramRuntimeEventLines(state.recentRuntimeEvents),
|
|
598
1108
|
];
|
|
@@ -686,15 +1196,28 @@ function buildStatusSummary(ctx: TelegramStatusContext): string {
|
|
|
686
1196
|
return "unknown";
|
|
687
1197
|
}
|
|
688
1198
|
|
|
1199
|
+
function buildTelegramStatusRoleSuffix(
|
|
1200
|
+
state: TelegramBridgeStatusLineState | undefined,
|
|
1201
|
+
): string {
|
|
1202
|
+
if (state?.botThreadMode !== "enabled" || !state.busRole) return "";
|
|
1203
|
+
return ` @${state.busRole}`;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
689
1206
|
export function buildStatusHtml(
|
|
690
1207
|
ctx: TelegramStatusContext,
|
|
691
1208
|
activeModel: TelegramStatusActiveModel | undefined,
|
|
1209
|
+
bridgeStatus?: TelegramBridgeStatusLineState,
|
|
692
1210
|
): string {
|
|
693
1211
|
const stats = collectUsageStats(ctx);
|
|
694
1212
|
const usesSubscription = activeModel
|
|
695
1213
|
? ctx.modelRegistry.isUsingOAuth(activeModel)
|
|
696
1214
|
: false;
|
|
697
|
-
const lines: string[] = [
|
|
1215
|
+
const lines: string[] = [
|
|
1216
|
+
buildStatusRow(
|
|
1217
|
+
"Status",
|
|
1218
|
+
`${buildStatusSummary(ctx)}${buildTelegramStatusRoleSuffix(bridgeStatus)}`,
|
|
1219
|
+
),
|
|
1220
|
+
];
|
|
698
1221
|
const usageSummary = buildUsageSummary(stats);
|
|
699
1222
|
const costSummary = buildCostSummary(stats, usesSubscription);
|
|
700
1223
|
if (usageSummary) {
|