@llblab/pi-telegram 0.13.2 → 0.14.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 +5 -3
- package/CHANGELOG.md +14 -12
- package/README.md +6 -6
- package/docs/architecture.md +6 -5
- package/docs/locks.md +4 -2
- package/docs/outbound.md +6 -3
- package/docs/public-api.md +7 -6
- package/docs/sections.md +4 -4
- package/index.ts +33 -18
- package/lib/bindings.ts +32 -3
- package/lib/command-templates.ts +155 -10
- package/lib/commands.ts +14 -12
- package/lib/lifecycle.ts +34 -0
- package/lib/locks.ts +16 -2
- package/lib/outbound-attachments.ts +253 -31
- package/lib/prompts.ts +4 -4
- package/lib/queue.ts +33 -32
- package/lib/routing.ts +7 -7
- package/lib/runtime.ts +45 -17
- package/lib/sections.ts +95 -31
- package/lib/status.ts +1 -1
- package/package.json +1 -1
package/lib/runtime.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const TELEGRAM_TYPING_ACTION_INTERVAL_MS =
|
|
7
|
+
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3000;
|
|
8
|
+
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 300;
|
|
8
9
|
|
|
9
10
|
export interface TelegramRuntimeQueueCounters {
|
|
10
11
|
nextQueuedTelegramItemOrder: number;
|
|
@@ -16,7 +17,7 @@ export interface TelegramRuntimeLifecycleFlags {
|
|
|
16
17
|
activeTelegramToolExecutions: number;
|
|
17
18
|
telegramTurnDispatchPending: boolean;
|
|
18
19
|
compactionInProgress: boolean;
|
|
19
|
-
|
|
20
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
20
21
|
setupInProgress: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ export interface TelegramBridgeRuntimeState
|
|
|
24
25
|
extends TelegramRuntimeQueueCounters, TelegramRuntimeLifecycleFlags {
|
|
25
26
|
abortHandler?: () => void;
|
|
26
27
|
typingInterval?: ReturnType<typeof setInterval>;
|
|
28
|
+
typingInFlight?: Promise<void>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface TelegramRuntimeQueuePort {
|
|
@@ -44,8 +46,8 @@ export interface TelegramRuntimeLifecyclePort {
|
|
|
44
46
|
clearDispatchPending: () => void;
|
|
45
47
|
isCompactionInProgress: () => boolean;
|
|
46
48
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
shouldFoldQueuedPromptsIntoHistory: () => boolean;
|
|
50
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface TelegramRuntimeSetupPort {
|
|
@@ -65,6 +67,7 @@ export interface TelegramRuntimeAbortPort {
|
|
|
65
67
|
export interface TelegramRuntimeTypingPort {
|
|
66
68
|
start: (deps: TelegramTypingLoopDeps) => boolean;
|
|
67
69
|
stop: () => boolean;
|
|
70
|
+
waitForIdle: () => Promise<void>;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export interface TelegramBridgeRuntime {
|
|
@@ -84,7 +87,7 @@ export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
|
|
|
84
87
|
activeTelegramToolExecutions: 0,
|
|
85
88
|
telegramTurnDispatchPending: false,
|
|
86
89
|
compactionInProgress: false,
|
|
87
|
-
|
|
90
|
+
foldQueuedPromptsIntoHistory: false,
|
|
88
91
|
setupInProgress: false,
|
|
89
92
|
};
|
|
90
93
|
}
|
|
@@ -117,10 +120,10 @@ export function createTelegramBridgeRuntime(
|
|
|
117
120
|
isCompactionInProgress: () => isTelegramCompactionInProgress(state),
|
|
118
121
|
setCompactionInProgress: (inProgress) =>
|
|
119
122
|
setTelegramCompactionInProgress(state, inProgress),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
shouldFoldQueuedPromptsIntoHistory: () =>
|
|
124
|
+
shouldFoldQueuedPromptsIntoHistory(state),
|
|
125
|
+
setFoldQueuedPromptsIntoHistory: (fold) =>
|
|
126
|
+
setFoldQueuedPromptsIntoHistory(state, fold),
|
|
124
127
|
},
|
|
125
128
|
setup: {
|
|
126
129
|
isInProgress: () => isTelegramSetupInProgress(state),
|
|
@@ -138,6 +141,7 @@ export function createTelegramBridgeRuntime(
|
|
|
138
141
|
typing: {
|
|
139
142
|
start: (deps) => startTelegramTypingLoop(state, deps),
|
|
140
143
|
stop: () => stopTelegramTypingLoop(state),
|
|
144
|
+
waitForIdle: () => waitForTelegramTypingLoopIdle(state),
|
|
141
145
|
},
|
|
142
146
|
};
|
|
143
147
|
}
|
|
@@ -195,8 +199,8 @@ export function syncTelegramLifecycleRuntimeFlags(
|
|
|
195
199
|
if (flags.compactionInProgress !== undefined) {
|
|
196
200
|
state.compactionInProgress = flags.compactionInProgress;
|
|
197
201
|
}
|
|
198
|
-
if (flags.
|
|
199
|
-
state.
|
|
202
|
+
if (flags.foldQueuedPromptsIntoHistory !== undefined) {
|
|
203
|
+
state.foldQueuedPromptsIntoHistory = flags.foldQueuedPromptsIntoHistory;
|
|
200
204
|
}
|
|
201
205
|
if (flags.setupInProgress !== undefined) {
|
|
202
206
|
state.setupInProgress = flags.setupInProgress;
|
|
@@ -254,17 +258,17 @@ export function setTelegramCompactionInProgress(
|
|
|
254
258
|
state.compactionInProgress = inProgress;
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
export function
|
|
261
|
+
export function shouldFoldQueuedPromptsIntoHistory(
|
|
258
262
|
state: TelegramBridgeRuntimeState,
|
|
259
263
|
): boolean {
|
|
260
|
-
return state.
|
|
264
|
+
return state.foldQueuedPromptsIntoHistory;
|
|
261
265
|
}
|
|
262
266
|
|
|
263
|
-
export function
|
|
267
|
+
export function setFoldQueuedPromptsIntoHistory(
|
|
264
268
|
state: TelegramBridgeRuntimeState,
|
|
265
|
-
|
|
269
|
+
fold: boolean,
|
|
266
270
|
): void {
|
|
267
|
-
state.
|
|
271
|
+
state.foldQueuedPromptsIntoHistory = fold;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
export function isTelegramSetupInProgress(
|
|
@@ -392,7 +396,13 @@ export function startTelegramTypingLoop(
|
|
|
392
396
|
if (state.typingInterval || deps.chatId === undefined || deps.chatId === 0)
|
|
393
397
|
return false;
|
|
394
398
|
const sendTyping = (): void => {
|
|
395
|
-
|
|
399
|
+
const typing = Promise.resolve(deps.sendTypingAction(deps.chatId as number))
|
|
400
|
+
.then(() => undefined)
|
|
401
|
+
.catch(() => undefined);
|
|
402
|
+
state.typingInFlight = typing;
|
|
403
|
+
void typing.finally(() => {
|
|
404
|
+
if (state.typingInFlight === typing) state.typingInFlight = undefined;
|
|
405
|
+
});
|
|
396
406
|
};
|
|
397
407
|
sendTyping();
|
|
398
408
|
state.typingInterval = setInterval(sendTyping, deps.intervalMs);
|
|
@@ -408,6 +418,24 @@ export function stopTelegramTypingLoop(
|
|
|
408
418
|
return true;
|
|
409
419
|
}
|
|
410
420
|
|
|
421
|
+
export async function waitForTelegramTypingLoopIdle(
|
|
422
|
+
state: TelegramBridgeRuntimeState,
|
|
423
|
+
timeoutMs = TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
const inFlight = state.typingInFlight;
|
|
426
|
+
if (!inFlight) return;
|
|
427
|
+
if (timeoutMs <= 0) {
|
|
428
|
+
await Promise.race([inFlight, Promise.resolve()]);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
await Promise.race([
|
|
432
|
+
inFlight,
|
|
433
|
+
new Promise<void>((resolve) => {
|
|
434
|
+
setTimeout(resolve, timeoutMs);
|
|
435
|
+
}),
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
|
|
411
439
|
export function createTelegramContextAbortHandlerSetter<
|
|
412
440
|
TContext extends { abort: () => void },
|
|
413
441
|
>(
|
package/lib/sections.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface TelegramSectionDiagnostic {
|
|
|
94
94
|
id: TelegramSectionId;
|
|
95
95
|
token: TelegramSectionToken;
|
|
96
96
|
label: string;
|
|
97
|
-
status: "active" | "
|
|
97
|
+
status: "active" | "error";
|
|
98
98
|
lastError?: string;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -106,6 +106,12 @@ export interface TelegramSectionRegistry {
|
|
|
106
106
|
token: TelegramSectionToken,
|
|
107
107
|
): RegisteredTelegramSection | undefined;
|
|
108
108
|
getDiagnostics(): TelegramSectionDiagnostic[];
|
|
109
|
+
recordError(
|
|
110
|
+
token: TelegramSectionToken,
|
|
111
|
+
message: string,
|
|
112
|
+
source?: string,
|
|
113
|
+
): void;
|
|
114
|
+
clearError(token: TelegramSectionToken, source?: string): void;
|
|
109
115
|
clear(): void;
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -299,6 +305,10 @@ function getUtf8ByteLength(value: string): number {
|
|
|
299
305
|
return new TextEncoder().encode(value).byteLength;
|
|
300
306
|
}
|
|
301
307
|
|
|
308
|
+
function sectionErrorMessage(error: unknown): string {
|
|
309
|
+
return error instanceof Error ? error.message : String(error);
|
|
310
|
+
}
|
|
311
|
+
|
|
302
312
|
function buildTelegramSectionCallbackData(
|
|
303
313
|
token: TelegramSectionToken,
|
|
304
314
|
action: string,
|
|
@@ -340,7 +350,10 @@ function prependBackRow(
|
|
|
340
350
|
/** @internal */
|
|
341
351
|
export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistry {
|
|
342
352
|
const sections = new Map<TelegramSectionToken, RegisteredTelegramSection>();
|
|
343
|
-
const errors = new Map<
|
|
353
|
+
const errors = new Map<
|
|
354
|
+
TelegramSectionToken,
|
|
355
|
+
{ message: string; source: string }
|
|
356
|
+
>();
|
|
344
357
|
let nextToken = 0;
|
|
345
358
|
|
|
346
359
|
function register(section: TelegramSectionRegistration): () => void {
|
|
@@ -382,24 +395,46 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
|
|
|
382
395
|
token: s.token,
|
|
383
396
|
label: s.label,
|
|
384
397
|
status: errors.has(s.token) ? "error" : "active",
|
|
385
|
-
lastError: errors.get(s.token),
|
|
398
|
+
lastError: errors.get(s.token)?.message,
|
|
386
399
|
}));
|
|
387
400
|
}
|
|
388
401
|
|
|
402
|
+
function recordError(
|
|
403
|
+
token: TelegramSectionToken,
|
|
404
|
+
message: string,
|
|
405
|
+
source = "runtime",
|
|
406
|
+
): void {
|
|
407
|
+
if (sections.has(token)) errors.set(token, { message, source });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function clearError(token: TelegramSectionToken, source?: string): void {
|
|
411
|
+
const current = errors.get(token);
|
|
412
|
+
if (!source || !current || current.source === source) errors.delete(token);
|
|
413
|
+
}
|
|
414
|
+
|
|
389
415
|
function clear(): void {
|
|
390
416
|
sections.clear();
|
|
391
417
|
errors.clear();
|
|
392
418
|
nextToken = 0;
|
|
393
419
|
}
|
|
394
420
|
|
|
395
|
-
return {
|
|
421
|
+
return {
|
|
422
|
+
register,
|
|
423
|
+
getSections,
|
|
424
|
+
getByToken,
|
|
425
|
+
getDiagnostics,
|
|
426
|
+
recordError,
|
|
427
|
+
clearError,
|
|
428
|
+
clear,
|
|
429
|
+
};
|
|
396
430
|
}
|
|
397
431
|
|
|
398
432
|
/** @internal */
|
|
399
433
|
export function getTelegramExtensionSettingsRows(
|
|
400
434
|
registry: TelegramSectionRegistry,
|
|
401
435
|
): TelegramSectionSettingsRow[] {
|
|
402
|
-
|
|
436
|
+
const rows: TelegramSectionSettingsRow[] = [];
|
|
437
|
+
for (const section of registry
|
|
403
438
|
.getSections()
|
|
404
439
|
.filter((s) => s.registration.settings)
|
|
405
440
|
.sort((a, b) => {
|
|
@@ -407,22 +442,47 @@ export function getTelegramExtensionSettingsRows(
|
|
|
407
442
|
const orderB = b.registration.settings!.order ?? 0;
|
|
408
443
|
if (orderA !== orderB) return orderA - orderB;
|
|
409
444
|
return a.id.localeCompare(b.id);
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
})) {
|
|
446
|
+
try {
|
|
447
|
+
rows.push({
|
|
448
|
+
label:
|
|
449
|
+
section.registration.settings!.getLabel?.() ??
|
|
450
|
+
section.registration.settings!.label,
|
|
451
|
+
callback_data: `section:${section.token}:settings:open`,
|
|
452
|
+
});
|
|
453
|
+
registry.clearError(section.token, "settings_label");
|
|
454
|
+
} catch (error) {
|
|
455
|
+
registry.recordError(
|
|
456
|
+
section.token,
|
|
457
|
+
sectionErrorMessage(error),
|
|
458
|
+
"settings_label",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return rows;
|
|
416
463
|
}
|
|
417
464
|
|
|
418
465
|
/** @internal */
|
|
419
466
|
export function getTelegramSectionMainMenuRows(
|
|
420
467
|
registry: TelegramSectionRegistry,
|
|
421
468
|
): TelegramSectionMainMenuRow[] {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
469
|
+
const rows: TelegramSectionMainMenuRow[] = [];
|
|
470
|
+
for (const section of registry.getSections()) {
|
|
471
|
+
try {
|
|
472
|
+
rows.push({
|
|
473
|
+
text: section.registration.getLabel?.() ?? section.label,
|
|
474
|
+
callback_data: `section:${section.token}:open`,
|
|
475
|
+
});
|
|
476
|
+
registry.clearError(section.token, "main_label");
|
|
477
|
+
} catch (error) {
|
|
478
|
+
registry.recordError(
|
|
479
|
+
section.token,
|
|
480
|
+
sectionErrorMessage(error),
|
|
481
|
+
"main_label",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return rows;
|
|
426
486
|
}
|
|
427
487
|
|
|
428
488
|
/** @internal */
|
|
@@ -508,9 +568,11 @@ export async function handleTelegramSectionOpen(
|
|
|
508
568
|
viewWithBack.replyMarkup,
|
|
509
569
|
);
|
|
510
570
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
571
|
+
registry.clearError(token, "render");
|
|
511
572
|
return true;
|
|
512
573
|
} catch (error) {
|
|
513
|
-
const message =
|
|
574
|
+
const message = sectionErrorMessage(error);
|
|
575
|
+
registry.recordError(token, message, "render");
|
|
514
576
|
await deps.answerCallbackQuery(
|
|
515
577
|
callbackQueryId,
|
|
516
578
|
`Section error: ${message}`,
|
|
@@ -537,10 +599,10 @@ export async function handleTelegramSectionCallback(
|
|
|
537
599
|
);
|
|
538
600
|
return true;
|
|
539
601
|
}
|
|
540
|
-
// Try main handleCallback first, then settings handleCallback as fallback
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
602
|
+
// Try main handleCallback first, then settings handleCallback as fallback.
|
|
603
|
+
const mainHandler = section.registration.handleCallback;
|
|
604
|
+
const settingsHandler = section.registration.settings?.handleCallback;
|
|
605
|
+
const handler = mainHandler ?? settingsHandler;
|
|
544
606
|
if (!handler) {
|
|
545
607
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
546
608
|
return true;
|
|
@@ -555,16 +617,12 @@ export async function handleTelegramSectionCallback(
|
|
|
555
617
|
payload,
|
|
556
618
|
callbackQueryId,
|
|
557
619
|
deps,
|
|
558
|
-
`section:${token}:open
|
|
620
|
+
mainHandler ? `section:${token}:open` : "settings:list",
|
|
559
621
|
);
|
|
560
622
|
let result = await handler(ctx);
|
|
561
623
|
// Fallback: if main handler passed and settings handler exists, try settings
|
|
562
|
-
// with the correct navigation context (back → settings list)
|
|
563
|
-
if (
|
|
564
|
-
result === "pass" &&
|
|
565
|
-
handler !== section.registration.settings?.handleCallback &&
|
|
566
|
-
section.registration.settings?.handleCallback
|
|
567
|
-
) {
|
|
624
|
+
// with the correct navigation context (back → settings list).
|
|
625
|
+
if (result === "pass" && mainHandler && settingsHandler) {
|
|
568
626
|
const settingsCtx = buildTelegramSectionCallbackContext(
|
|
569
627
|
section.id,
|
|
570
628
|
token,
|
|
@@ -576,14 +634,16 @@ export async function handleTelegramSectionCallback(
|
|
|
576
634
|
deps,
|
|
577
635
|
"settings:list",
|
|
578
636
|
);
|
|
579
|
-
result = await
|
|
637
|
+
result = await settingsHandler(settingsCtx);
|
|
580
638
|
}
|
|
581
639
|
if (result === "pass") {
|
|
582
640
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
583
641
|
}
|
|
642
|
+
registry.clearError(token, "callback");
|
|
584
643
|
return true;
|
|
585
644
|
} catch (error) {
|
|
586
|
-
const message =
|
|
645
|
+
const message = sectionErrorMessage(error);
|
|
646
|
+
registry.recordError(token, message, "callback");
|
|
587
647
|
await deps.answerCallbackQuery(
|
|
588
648
|
callbackQueryId,
|
|
589
649
|
`Section error: ${message}`,
|
|
@@ -632,9 +692,11 @@ export async function handleTelegramSectionSettingsOpen(
|
|
|
632
692
|
viewWithBack.replyMarkup,
|
|
633
693
|
);
|
|
634
694
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
695
|
+
registry.clearError(token, "settings_open");
|
|
635
696
|
return true;
|
|
636
697
|
} catch (error) {
|
|
637
|
-
const message =
|
|
698
|
+
const message = sectionErrorMessage(error);
|
|
699
|
+
registry.recordError(token, message, "settings_open");
|
|
638
700
|
await deps.answerCallbackQuery(
|
|
639
701
|
callbackQueryId,
|
|
640
702
|
`Section error: ${message}`,
|
|
@@ -677,9 +739,11 @@ export async function handleTelegramSectionSettingsCallback(
|
|
|
677
739
|
if (result === "pass") {
|
|
678
740
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
679
741
|
}
|
|
742
|
+
registry.clearError(token, "settings_callback");
|
|
680
743
|
return true;
|
|
681
744
|
} catch (error) {
|
|
682
|
-
const message =
|
|
745
|
+
const message = sectionErrorMessage(error);
|
|
746
|
+
registry.recordError(token, message, "settings_callback");
|
|
683
747
|
await deps.answerCallbackQuery(
|
|
684
748
|
callbackQueryId,
|
|
685
749
|
`Section error: ${message}`,
|
package/lib/status.ts
CHANGED
|
@@ -462,7 +462,7 @@ export function buildTelegramStatusBarText(
|
|
|
462
462
|
? theme.fg("success", state.queuedStatus)
|
|
463
463
|
: "";
|
|
464
464
|
if (state.compactionInProgress) {
|
|
465
|
-
return `${label} ${theme.fg("
|
|
465
|
+
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
466
466
|
}
|
|
467
467
|
if (state.processing) {
|
|
468
468
|
const processingStatus = state.queuedStatus
|