@llblab/pi-telegram 0.27.11 → 0.28.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 +150 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +396 -441
- package/README.md +9 -6
- package/api/updates.ts +5 -0
- package/docs/architecture.md +71 -26
- package/docs/multi-instance-bus.md +23 -5
- package/docs/public-api.md +7 -6
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +356 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -2
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +21 -21
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +17 -0
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +3 -3
- package/scripts/check-downgrade.mjs +435 -0
package/lib/lifecycle.ts
CHANGED
|
@@ -229,6 +229,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
229
229
|
ctx: ExtensionContext,
|
|
230
230
|
): Promise<void>;
|
|
231
231
|
};
|
|
232
|
+
inboundWorker: { onSessionShutdown(): Promise<void> };
|
|
232
233
|
capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
|
|
233
234
|
queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
|
|
234
235
|
}
|
|
@@ -283,6 +284,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
|
|
|
283
284
|
};
|
|
284
285
|
delivery: TelegramBridgeSessionServiceRuntime["delivery"];
|
|
285
286
|
polling: TelegramBridgeSessionServiceRuntime["polling"];
|
|
287
|
+
inboundWorker: TelegramBridgeSessionServiceRuntime["inboundWorker"];
|
|
286
288
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
287
289
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
288
290
|
};
|
|
@@ -309,6 +311,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
|
|
|
309
311
|
}),
|
|
310
312
|
delivery: ports.services.delivery,
|
|
311
313
|
polling: ports.services.polling,
|
|
314
|
+
inboundWorker: ports.services.inboundWorker,
|
|
312
315
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
313
316
|
queueWatchdog: ports.services.queueWatchdog,
|
|
314
317
|
},
|
|
@@ -349,6 +352,7 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
349
352
|
await deps.services.delivery.onSessionShutdown();
|
|
350
353
|
deps.services.queueWatchdog.stop();
|
|
351
354
|
deps.services.capabilityMonitor.stop();
|
|
355
|
+
await deps.services.inboundWorker.onSessionShutdown();
|
|
352
356
|
},
|
|
353
357
|
},
|
|
354
358
|
isSessionActive,
|
package/lib/locks.ts
CHANGED
|
@@ -23,7 +23,8 @@ import { resolveTelegramOwnersPath } from "./paths.ts";
|
|
|
23
23
|
|
|
24
24
|
export const TELEGRAM_LOCK_KEY = "default";
|
|
25
25
|
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
26
|
-
const
|
|
26
|
+
export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
|
|
27
|
+
export const TELEGRAM_OWNERSHIP_REFRESH_MS = 2_000;
|
|
27
28
|
const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
|
|
28
29
|
const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
|
|
29
30
|
const TELEGRAM_LOCK_TRANSACTION_ATTEMPTS = 80;
|
|
@@ -276,20 +277,15 @@ function createLockTransactionContentionError(path: string): Error {
|
|
|
276
277
|
);
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
function isLockTransactionContentionError(
|
|
280
|
-
error: unknown,
|
|
281
|
-
path: string,
|
|
282
|
-
): boolean {
|
|
280
|
+
function isLockTransactionContentionError(error: unknown): boolean {
|
|
283
281
|
const code = (error as { code?: unknown })?.code;
|
|
284
|
-
|
|
282
|
+
return (
|
|
285
283
|
code === "EEXIST" ||
|
|
286
284
|
code === "ENOTEMPTY" ||
|
|
287
285
|
code === "ENOTDIR" ||
|
|
288
|
-
code === "EISDIR"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
return existsSync(path) && (code === "EPERM" || code === "EACCES");
|
|
286
|
+
code === "EISDIR" ||
|
|
287
|
+
isRetryableLockWriteError(error)
|
|
288
|
+
);
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
function removeLockTransactionGuard(path: string): void {
|
|
@@ -298,6 +294,7 @@ function removeLockTransactionGuard(path: string): void {
|
|
|
298
294
|
|
|
299
295
|
function createLockTransactionGuard(
|
|
300
296
|
path: string,
|
|
297
|
+
options: TelegramFileTransactionOptions = {},
|
|
301
298
|
): TelegramLockTransactionOwner {
|
|
302
299
|
const owner: TelegramLockTransactionOwner = {
|
|
303
300
|
pid: process.pid,
|
|
@@ -313,7 +310,7 @@ function createLockTransactionGuard(
|
|
|
313
310
|
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
314
311
|
);
|
|
315
312
|
if (existsSync(path)) throw createLockTransactionContentionError(path);
|
|
316
|
-
renameSync(stagedPath, path);
|
|
313
|
+
(options.publishRename ?? renameSync)(stagedPath, path);
|
|
317
314
|
return owner;
|
|
318
315
|
} finally {
|
|
319
316
|
try {
|
|
@@ -368,6 +365,7 @@ type TelegramTransactionGlobal = typeof globalThis & {
|
|
|
368
365
|
|
|
369
366
|
export interface TelegramFileTransactionOptions {
|
|
370
367
|
recoveryRename?: typeof renameSync;
|
|
368
|
+
publishRename?: typeof renameSync;
|
|
371
369
|
attempts?: number;
|
|
372
370
|
retryDelayMs?: number;
|
|
373
371
|
}
|
|
@@ -465,9 +463,9 @@ function acquireRecoverableDirectoryGuard(
|
|
|
465
463
|
): TelegramLockTransactionOwner | undefined {
|
|
466
464
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
467
465
|
try {
|
|
468
|
-
return createLockTransactionGuard(path);
|
|
466
|
+
return createLockTransactionGuard(path, options);
|
|
469
467
|
} catch (error) {
|
|
470
|
-
if (!isLockTransactionContentionError(error
|
|
468
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
471
469
|
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
472
470
|
}
|
|
473
471
|
}
|
|
@@ -528,11 +526,12 @@ function acquireLegacyRecoveryGuard(
|
|
|
528
526
|
|
|
529
527
|
function createRecoveredLockTransactionGuard(
|
|
530
528
|
path: string,
|
|
529
|
+
options: TelegramFileTransactionOptions = {},
|
|
531
530
|
): TelegramLockTransactionOwner | undefined {
|
|
532
531
|
try {
|
|
533
|
-
return createLockTransactionGuard(path);
|
|
532
|
+
return createLockTransactionGuard(path, options);
|
|
534
533
|
} catch (error) {
|
|
535
|
-
if (isLockTransactionContentionError(error
|
|
534
|
+
if (isLockTransactionContentionError(error)) return undefined;
|
|
536
535
|
throw error;
|
|
537
536
|
}
|
|
538
537
|
}
|
|
@@ -550,7 +549,7 @@ function recoverAbandonedLockTransaction(
|
|
|
550
549
|
}
|
|
551
550
|
if (isDirectory) {
|
|
552
551
|
if (!reclaimAbandonedDirectoryGuard(path, options)) return undefined;
|
|
553
|
-
const recoveredOwner = createRecoveredLockTransactionGuard(path);
|
|
552
|
+
const recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
554
553
|
try {
|
|
555
554
|
reclaimAbandonedDirectoryGuard(`${path}.recovery`, options);
|
|
556
555
|
return recoveredOwner;
|
|
@@ -584,7 +583,7 @@ function recoverAbandonedLockTransaction(
|
|
|
584
583
|
} catch {
|
|
585
584
|
/* stale debris cannot retain transaction authority */
|
|
586
585
|
}
|
|
587
|
-
recoveredOwner = createRecoveredLockTransactionGuard(path);
|
|
586
|
+
recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
588
587
|
return recoveredOwner;
|
|
589
588
|
} finally {
|
|
590
589
|
try {
|
|
@@ -617,9 +616,9 @@ function acquireLockTransaction(
|
|
|
617
616
|
mkdirSync(dirname(path), { recursive: true });
|
|
618
617
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
619
618
|
try {
|
|
620
|
-
return createLockTransactionGuard(path);
|
|
619
|
+
return createLockTransactionGuard(path, options);
|
|
621
620
|
} catch (error) {
|
|
622
|
-
if (!isLockTransactionContentionError(error
|
|
621
|
+
if (!isLockTransactionContentionError(error)) throw error;
|
|
623
622
|
const recoveredOwner = recoverAbandonedLockTransaction(path, options);
|
|
624
623
|
if (recoveredOwner !== undefined) return recoveredOwner;
|
|
625
624
|
if (attempt === attempts - 1) {
|
|
@@ -1163,7 +1162,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1163
1162
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1164
1163
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1165
1164
|
let sessionAutoStartGeneration = 0;
|
|
1166
|
-
const ownershipCheckMs =
|
|
1165
|
+
const ownershipCheckMs =
|
|
1166
|
+
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1167
1167
|
const ownershipRefreshMs =
|
|
1168
1168
|
deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
1169
1169
|
const stopOwnershipWatcher = () => {
|
package/lib/media.ts
CHANGED
|
@@ -101,6 +101,8 @@ export interface TelegramMediaGroupState<TMessage, TContext = unknown> {
|
|
|
101
101
|
context?: TContext;
|
|
102
102
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
103
103
|
dispatching?: boolean;
|
|
104
|
+
dispatchPromise?: Promise<void>;
|
|
105
|
+
dispatchNow?: () => Promise<void>;
|
|
104
106
|
suspended?: boolean;
|
|
105
107
|
reschedule?: () => void;
|
|
106
108
|
}
|
|
@@ -118,6 +120,7 @@ export interface TelegramMediaGroupController<
|
|
|
118
120
|
) => unknown | Promise<unknown>;
|
|
119
121
|
}) => boolean;
|
|
120
122
|
removeMessages: (messageIds: number[]) => number;
|
|
123
|
+
flushMessage: (messageId: number) => Promise<boolean>;
|
|
121
124
|
suspend: () => void;
|
|
122
125
|
resume: (context: TContext) => void;
|
|
123
126
|
clear: () => void;
|
|
@@ -129,6 +132,7 @@ export interface TelegramMediaGroupDispatchRuntimeDeps<
|
|
|
129
132
|
> {
|
|
130
133
|
mediaGroups: TelegramMediaGroupController<TMessage, TContext>;
|
|
131
134
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
135
|
+
onDeferredMessage?: (message: TMessage) => void;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
export interface TelegramMediaGroupDispatchRuntime<
|
|
@@ -504,45 +508,59 @@ export function queueTelegramMediaGroupMessage<
|
|
|
504
508
|
const key = getTelegramMediaGroupKey(options.message);
|
|
505
509
|
if (!key) return false;
|
|
506
510
|
const existing = options.groups.get(key) ?? { messages: [] };
|
|
507
|
-
existing.messages.
|
|
511
|
+
const duplicateIndex = existing.messages.findIndex(
|
|
512
|
+
(message) => message.message_id === options.message.message_id,
|
|
513
|
+
);
|
|
514
|
+
if (duplicateIndex >= 0) {
|
|
515
|
+
existing.messages[duplicateIndex] = options.message;
|
|
516
|
+
} else {
|
|
517
|
+
existing.messages.push(options.message);
|
|
518
|
+
}
|
|
508
519
|
existing.context = options.context;
|
|
520
|
+
const dispatchQueued = (): Promise<void> => {
|
|
521
|
+
existing.flushTimer = undefined;
|
|
522
|
+
const state = options.groups.get(key);
|
|
523
|
+
if (!state) return Promise.resolve();
|
|
524
|
+
if (state.dispatching) return state.dispatchPromise ?? Promise.resolve();
|
|
525
|
+
const dispatchedMessages = [...state.messages];
|
|
526
|
+
const dispatchedIds = new Set(
|
|
527
|
+
dispatchedMessages.map((message) => message.message_id),
|
|
528
|
+
);
|
|
529
|
+
state.dispatching = true;
|
|
530
|
+
const operation = Promise.resolve(
|
|
531
|
+
options.dispatchMessages(dispatchedMessages, state.context),
|
|
532
|
+
).then(
|
|
533
|
+
() => {
|
|
534
|
+
if (options.groups.get(key) !== state) return;
|
|
535
|
+
state.messages = state.messages.filter(
|
|
536
|
+
(message) => !dispatchedIds.has(message.message_id),
|
|
537
|
+
);
|
|
538
|
+
state.dispatching = false;
|
|
539
|
+
state.dispatchPromise = undefined;
|
|
540
|
+
if (state.messages.length === 0) options.groups.delete(key);
|
|
541
|
+
else if (!state.flushTimer) scheduleDispatch();
|
|
542
|
+
},
|
|
543
|
+
(error) => {
|
|
544
|
+
if (options.groups.get(key) === state) {
|
|
545
|
+
state.dispatching = false;
|
|
546
|
+
state.dispatchPromise = undefined;
|
|
547
|
+
if (!state.flushTimer) scheduleDispatch();
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
state.dispatchPromise = operation;
|
|
553
|
+
return operation;
|
|
554
|
+
};
|
|
509
555
|
const scheduleDispatch = (): void => {
|
|
510
556
|
if (existing.suspended) return;
|
|
511
557
|
existing.flushTimer = options.setTimer(() => {
|
|
512
|
-
|
|
513
|
-
const state = options.groups.get(key);
|
|
514
|
-
if (!state) return;
|
|
515
|
-
if (state.dispatching) {
|
|
516
|
-
scheduleDispatch();
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const dispatchedMessages = [...state.messages];
|
|
520
|
-
const dispatchedIds = new Set(
|
|
521
|
-
dispatchedMessages.map((message) => message.message_id),
|
|
522
|
-
);
|
|
523
|
-
state.dispatching = true;
|
|
524
|
-
void Promise.resolve(
|
|
525
|
-
options.dispatchMessages(dispatchedMessages, state.context),
|
|
526
|
-
).then(
|
|
527
|
-
() => {
|
|
528
|
-
if (options.groups.get(key) !== state) return;
|
|
529
|
-
state.messages = state.messages.filter(
|
|
530
|
-
(message) => !dispatchedIds.has(message.message_id),
|
|
531
|
-
);
|
|
532
|
-
state.dispatching = false;
|
|
533
|
-
if (state.messages.length === 0) options.groups.delete(key);
|
|
534
|
-
else if (!state.flushTimer) scheduleDispatch();
|
|
535
|
-
},
|
|
536
|
-
() => {
|
|
537
|
-
if (options.groups.get(key) !== state) return;
|
|
538
|
-
state.dispatching = false;
|
|
539
|
-
if (!state.flushTimer) scheduleDispatch();
|
|
540
|
-
},
|
|
541
|
-
);
|
|
558
|
+
void dispatchQueued().catch(() => undefined);
|
|
542
559
|
}, options.debounceMs);
|
|
543
560
|
existing.flushTimer.unref?.();
|
|
544
561
|
};
|
|
545
562
|
existing.reschedule = scheduleDispatch;
|
|
563
|
+
existing.dispatchNow = dispatchQueued;
|
|
546
564
|
if (existing.flushTimer) options.clearTimer(existing.flushTimer);
|
|
547
565
|
scheduleDispatch();
|
|
548
566
|
options.groups.set(key, existing);
|
|
@@ -575,6 +593,24 @@ export function createTelegramMediaGroupController<
|
|
|
575
593
|
}),
|
|
576
594
|
removeMessages: (messageIds) =>
|
|
577
595
|
removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
|
|
596
|
+
async flushMessage(messageId) {
|
|
597
|
+
for (const state of groups.values()) {
|
|
598
|
+
if (!state.messages.some((message) => message.message_id === messageId)) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
602
|
+
state.flushTimer = undefined;
|
|
603
|
+
await state.dispatchNow?.();
|
|
604
|
+
if (
|
|
605
|
+
state.messages.some((message) => message.message_id === messageId) &&
|
|
606
|
+
!state.dispatching
|
|
607
|
+
) {
|
|
608
|
+
await state.dispatchNow?.();
|
|
609
|
+
}
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
return false;
|
|
613
|
+
},
|
|
578
614
|
suspend: () => {
|
|
579
615
|
for (const state of groups.values()) {
|
|
580
616
|
state.suspended = true;
|
|
@@ -614,7 +650,10 @@ export function createTelegramMediaGroupDispatchRuntime<
|
|
|
614
650
|
? Promise.resolve()
|
|
615
651
|
: deps.dispatchMessages(messages, queuedCtx),
|
|
616
652
|
});
|
|
617
|
-
if (queuedMediaGroup)
|
|
653
|
+
if (queuedMediaGroup) {
|
|
654
|
+
deps.onDeferredMessage?.(message);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
618
657
|
await deps.dispatchMessages([message], ctx);
|
|
619
658
|
},
|
|
620
659
|
};
|
package/lib/menu-queue.ts
CHANGED
|
@@ -34,6 +34,7 @@ interface TelegramQueueMenuItem {
|
|
|
34
34
|
queuePosition: number;
|
|
35
35
|
isPriority: boolean;
|
|
36
36
|
priorityEmoji?: string;
|
|
37
|
+
reactionSuppressionEmoji?: string;
|
|
37
38
|
hasAttachments: boolean;
|
|
38
39
|
statusSummary: string;
|
|
39
40
|
promptText: string;
|
|
@@ -62,6 +63,8 @@ function toTelegramQueueMenuItems<Context>(
|
|
|
62
63
|
queuePosition: index + 1,
|
|
63
64
|
isPriority: item.queueLane === "priority",
|
|
64
65
|
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
66
|
+
reactionSuppressionEmoji:
|
|
67
|
+
item.kind === "prompt" ? item.reactionSuppressionEmoji : undefined,
|
|
65
68
|
hasAttachments:
|
|
66
69
|
item.kind === "prompt" && item.queuedAttachments.length > 0,
|
|
67
70
|
statusSummary: item.statusSummary,
|
|
@@ -84,11 +87,13 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
84
87
|
const refreshRow = [{ text: "🌀 Refresh", callback_data: refreshData }];
|
|
85
88
|
if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
|
|
86
89
|
const rows = items.map((item, index) => {
|
|
87
|
-
const prefix = item.
|
|
88
|
-
? `${item.
|
|
89
|
-
: item.
|
|
90
|
-
? "
|
|
91
|
-
:
|
|
90
|
+
const prefix = item.reactionSuppressionEmoji
|
|
91
|
+
? `${item.reactionSuppressionEmoji} suppressed · `
|
|
92
|
+
: item.isPriority
|
|
93
|
+
? `${item.priorityEmoji ?? "⚡"} `
|
|
94
|
+
: item.hasAttachments
|
|
95
|
+
? "📎 "
|
|
96
|
+
: "";
|
|
92
97
|
const label = `${index + 1}. ${prefix}${item.statusSummary}`;
|
|
93
98
|
return [
|
|
94
99
|
{
|
|
@@ -152,10 +157,17 @@ function escapeTelegramQueueMenuHtmlPreview(text: string): string {
|
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
155
|
-
const badge = item.
|
|
160
|
+
const badge = item.reactionSuppressionEmoji
|
|
161
|
+
? ` ${item.reactionSuppressionEmoji}`
|
|
162
|
+
: item.isPriority
|
|
163
|
+
? ` ${item.priorityEmoji ?? "⚡"}`
|
|
164
|
+
: "";
|
|
156
165
|
const heading = `<b>${item.queuePosition}.</b>${badge}`;
|
|
166
|
+
const suppression = item.reactionSuppressionEmoji
|
|
167
|
+
? "\n<i>Suppressed by reaction. Remove it to restore this turn.</i>"
|
|
168
|
+
: "";
|
|
157
169
|
const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
|
|
158
|
-
return `${heading}\n${preview}`;
|
|
170
|
+
return `${heading}${suppression}\n${preview}`;
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
@@ -820,11 +832,13 @@ function toggleQueuedTelegramPromptPriority<Context>(
|
|
|
820
832
|
replyToMessageId,
|
|
821
833
|
);
|
|
822
834
|
if (!item) return false;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
835
|
+
deps.queueMutationRuntime.applyReactionByMessageId(
|
|
836
|
+
replyToMessageId,
|
|
837
|
+
item.queueLane === "priority"
|
|
838
|
+
? { kind: "default" }
|
|
839
|
+
: { kind: "priority", emoji: "⚡" },
|
|
840
|
+
ctx,
|
|
841
|
+
);
|
|
828
842
|
return true;
|
|
829
843
|
}
|
|
830
844
|
|
|
@@ -844,11 +858,11 @@ function setQueuedTelegramPromptPriority<Context>(
|
|
|
844
858
|
replyToMessageId,
|
|
845
859
|
);
|
|
846
860
|
if (!item) return false;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
861
|
+
deps.queueMutationRuntime.applyReactionByMessageId(
|
|
862
|
+
replyToMessageId,
|
|
863
|
+
enabled ? { kind: "priority", emoji: "⚡" } : { kind: "default" },
|
|
864
|
+
ctx,
|
|
865
|
+
);
|
|
852
866
|
return true;
|
|
853
867
|
}
|
|
854
868
|
|
package/lib/menu.ts
CHANGED
|
@@ -229,6 +229,7 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
229
229
|
prompt: string,
|
|
230
230
|
ctx: TContext,
|
|
231
231
|
target?: { chatId: number; threadId?: number },
|
|
232
|
+
source?: unknown,
|
|
232
233
|
) => Promise<void>;
|
|
233
234
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
|
234
235
|
isVoiceReplyActive?: () => boolean;
|
|
@@ -478,6 +479,7 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
478
479
|
prompt: string,
|
|
479
480
|
ctx: TContext,
|
|
480
481
|
target?: { chatId: number; threadId?: number },
|
|
482
|
+
source?: unknown,
|
|
481
483
|
) => Promise<void>;
|
|
482
484
|
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
|
|
483
485
|
}
|
|
@@ -588,7 +590,7 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
588
590
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
589
591
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
590
592
|
? (prompt: string) =>
|
|
591
|
-
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
593
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
|
592
594
|
: async () => {},
|
|
593
595
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
594
596
|
},
|
|
@@ -611,7 +613,7 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
611
613
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
612
614
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
613
615
|
? (prompt: string) =>
|
|
614
|
-
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
616
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
|
615
617
|
: async () => {},
|
|
616
618
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
617
619
|
},
|
|
@@ -636,7 +638,7 @@ export async function handleTelegramMenuCallbackRuntime<
|
|
|
636
638
|
deps.sendInteractiveMessage ?? (async () => undefined),
|
|
637
639
|
enqueuePrompt: deps.enqueueSectionPrompt
|
|
638
640
|
? (prompt: string) =>
|
|
639
|
-
deps.enqueueSectionPrompt!(prompt, ctx, target)
|
|
641
|
+
deps.enqueueSectionPrompt!(prompt, ctx, target, query)
|
|
640
642
|
: async () => {},
|
|
641
643
|
deleteMessage: deps.deleteMessage ?? (async () => {}),
|
|
642
644
|
},
|
package/lib/model.ts
CHANGED
|
@@ -188,6 +188,19 @@ function isAliasModelId(id: string): boolean {
|
|
|
188
188
|
return !/-\d{8}$/.test(id);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
function findUniqueModelMatch<TModel extends MenuModel>(
|
|
192
|
+
availableModels: TModel[],
|
|
193
|
+
matches: (model: TModel) => boolean,
|
|
194
|
+
): { model?: TModel; ambiguous: boolean } {
|
|
195
|
+
let model: TModel | undefined;
|
|
196
|
+
for (const candidate of availableModels) {
|
|
197
|
+
if (!matches(candidate)) continue;
|
|
198
|
+
if (model) return { ambiguous: true };
|
|
199
|
+
model = candidate;
|
|
200
|
+
}
|
|
201
|
+
return { model, ambiguous: false };
|
|
202
|
+
}
|
|
203
|
+
|
|
191
204
|
function findExactModelReferenceMatch<TModel extends MenuModel = MenuModel>(
|
|
192
205
|
modelReference: string,
|
|
193
206
|
availableModels: TModel[],
|
|
@@ -195,29 +208,35 @@ function findExactModelReferenceMatch<TModel extends MenuModel = MenuModel>(
|
|
|
195
208
|
const trimmedReference = modelReference.trim();
|
|
196
209
|
if (!trimmedReference) return undefined;
|
|
197
210
|
const normalizedReference = trimmedReference.toLowerCase();
|
|
198
|
-
const
|
|
211
|
+
const canonicalMatch = findUniqueModelMatch(
|
|
212
|
+
availableModels,
|
|
199
213
|
(model) => getCanonicalModelId(model).toLowerCase() === normalizedReference,
|
|
200
214
|
);
|
|
201
|
-
if (
|
|
202
|
-
|
|
215
|
+
if (canonicalMatch.model || canonicalMatch.ambiguous) {
|
|
216
|
+
return canonicalMatch.model;
|
|
217
|
+
}
|
|
203
218
|
const slashIndex = trimmedReference.indexOf("/");
|
|
204
219
|
if (slashIndex !== -1) {
|
|
205
220
|
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
206
221
|
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
207
222
|
if (provider && modelId) {
|
|
208
|
-
const
|
|
223
|
+
const normalizedProvider = provider.toLowerCase();
|
|
224
|
+
const normalizedModelId = modelId.toLowerCase();
|
|
225
|
+
const providerMatch = findUniqueModelMatch(
|
|
226
|
+
availableModels,
|
|
209
227
|
(model) =>
|
|
210
|
-
model.provider.toLowerCase() ===
|
|
211
|
-
model.id.toLowerCase() ===
|
|
228
|
+
model.provider.toLowerCase() === normalizedProvider &&
|
|
229
|
+
model.id.toLowerCase() === normalizedModelId,
|
|
212
230
|
);
|
|
213
|
-
if (
|
|
214
|
-
|
|
231
|
+
if (providerMatch.model || providerMatch.ambiguous) {
|
|
232
|
+
return providerMatch.model;
|
|
233
|
+
}
|
|
215
234
|
}
|
|
216
235
|
}
|
|
217
|
-
|
|
236
|
+
return findUniqueModelMatch(
|
|
237
|
+
availableModels,
|
|
218
238
|
(model) => model.id.toLowerCase() === normalizedReference,
|
|
219
|
-
);
|
|
220
|
-
return idMatches.length === 1 ? idMatches[0] : undefined;
|
|
239
|
+
).model;
|
|
221
240
|
}
|
|
222
241
|
|
|
223
242
|
function tryMatchScopedModel<TModel extends MenuModel = MenuModel>(
|
|
@@ -229,20 +248,28 @@ function tryMatchScopedModel<TModel extends MenuModel = MenuModel>(
|
|
|
229
248
|
availableModels,
|
|
230
249
|
);
|
|
231
250
|
if (exactMatch) return exactMatch;
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
const normalizedPattern = modelPattern.toLowerCase();
|
|
252
|
+
let bestAlias: TModel | undefined;
|
|
253
|
+
let bestDatedVersion: TModel | undefined;
|
|
254
|
+
for (const model of availableModels) {
|
|
255
|
+
if (
|
|
256
|
+
!model.id.toLowerCase().includes(normalizedPattern) &&
|
|
257
|
+
!model.name?.toLowerCase().includes(normalizedPattern)
|
|
258
|
+
) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (isAliasModelId(model.id)) {
|
|
262
|
+
if (!bestAlias || model.id.localeCompare(bestAlias.id) > 0) {
|
|
263
|
+
bestAlias = model;
|
|
264
|
+
}
|
|
265
|
+
} else if (
|
|
266
|
+
!bestDatedVersion ||
|
|
267
|
+
model.id.localeCompare(bestDatedVersion.id) > 0
|
|
268
|
+
) {
|
|
269
|
+
bestDatedVersion = model;
|
|
270
|
+
}
|
|
243
271
|
}
|
|
244
|
-
|
|
245
|
-
return datedVersions[0];
|
|
272
|
+
return bestAlias ?? bestDatedVersion;
|
|
246
273
|
}
|
|
247
274
|
|
|
248
275
|
function parseScopedModelPattern<TModel extends MenuModel = MenuModel>(
|
package/lib/ownership.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface TelegramMessageOwnershipRecord {
|
|
|
13
13
|
instanceId: string;
|
|
14
14
|
profileKey?: string;
|
|
15
15
|
ownerGeneration?: string;
|
|
16
|
+
recipientBindingKey?: string;
|
|
16
17
|
createdAt: number;
|
|
17
18
|
updatedAt: number;
|
|
18
19
|
}
|
|
@@ -25,6 +26,7 @@ export interface TelegramMessageOwnershipStore {
|
|
|
25
26
|
instanceId: string;
|
|
26
27
|
profileKey?: string;
|
|
27
28
|
ownerGeneration?: string;
|
|
29
|
+
recipientBindingKey?: string;
|
|
28
30
|
now?: number;
|
|
29
31
|
}) => TelegramMessageOwnershipRecord;
|
|
30
32
|
get: (
|
|
@@ -45,6 +47,7 @@ export interface TelegramMessageOwnershipStore {
|
|
|
45
47
|
export interface TelegramFollowerOwnershipView {
|
|
46
48
|
instanceId: string;
|
|
47
49
|
connectedAtMs: number;
|
|
50
|
+
profileKey?: string;
|
|
48
51
|
registrationGeneration?: string;
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -100,6 +103,22 @@ export function createTelegramBusMessageOwnershipRuntime(deps: {
|
|
|
100
103
|
);
|
|
101
104
|
});
|
|
102
105
|
},
|
|
106
|
+
resolveOwnerReplacement(record) {
|
|
107
|
+
if (!record.recipientBindingKey) return undefined;
|
|
108
|
+
const follower = deps
|
|
109
|
+
.listFollowers()
|
|
110
|
+
.find(
|
|
111
|
+
(candidate) =>
|
|
112
|
+
candidate.profileKey === record.recipientBindingKey,
|
|
113
|
+
);
|
|
114
|
+
return follower
|
|
115
|
+
? {
|
|
116
|
+
instanceId: follower.instanceId,
|
|
117
|
+
ownerGeneration: getTelegramFollowerOwnershipGeneration(follower),
|
|
118
|
+
recipientBindingKey: record.recipientBindingKey,
|
|
119
|
+
}
|
|
120
|
+
: undefined;
|
|
121
|
+
},
|
|
103
122
|
});
|
|
104
123
|
const recordFollower = function (input: {
|
|
105
124
|
chatId: number;
|
|
@@ -113,6 +132,7 @@ export function createTelegramBusMessageOwnershipRuntime(deps: {
|
|
|
113
132
|
target: input.target,
|
|
114
133
|
instanceId: input.follower.instanceId,
|
|
115
134
|
ownerGeneration: getTelegramFollowerOwnershipGeneration(input.follower),
|
|
135
|
+
recipientBindingKey: input.follower.profileKey,
|
|
116
136
|
});
|
|
117
137
|
};
|
|
118
138
|
return {
|
|
@@ -155,6 +175,7 @@ function createTelegramMessageOwnershipRecord(input: {
|
|
|
155
175
|
instanceId: string;
|
|
156
176
|
profileKey?: string;
|
|
157
177
|
ownerGeneration?: string;
|
|
178
|
+
recipientBindingKey?: string;
|
|
158
179
|
now: number;
|
|
159
180
|
previous?: TelegramMessageOwnershipRecord;
|
|
160
181
|
}): TelegramMessageOwnershipRecord {
|
|
@@ -167,6 +188,9 @@ function createTelegramMessageOwnershipRecord(input: {
|
|
|
167
188
|
...(input.ownerGeneration
|
|
168
189
|
? { ownerGeneration: input.ownerGeneration }
|
|
169
190
|
: {}),
|
|
191
|
+
...(input.recipientBindingKey
|
|
192
|
+
? { recipientBindingKey: input.recipientBindingKey }
|
|
193
|
+
: {}),
|
|
170
194
|
createdAt: input.previous?.createdAt ?? input.now,
|
|
171
195
|
updatedAt: input.now,
|
|
172
196
|
};
|
|
@@ -176,6 +200,14 @@ export function createTelegramMessageOwnershipStore(
|
|
|
176
200
|
options: {
|
|
177
201
|
getProfileKey?: () => string | undefined;
|
|
178
202
|
isOwnerGenerationLive?: (record: TelegramMessageOwnershipRecord) => boolean;
|
|
203
|
+
resolveOwnerReplacement?: (
|
|
204
|
+
record: TelegramMessageOwnershipRecord,
|
|
205
|
+
) =>
|
|
206
|
+
| Pick<
|
|
207
|
+
TelegramMessageOwnershipRecord,
|
|
208
|
+
"instanceId" | "ownerGeneration" | "recipientBindingKey"
|
|
209
|
+
>
|
|
210
|
+
| undefined;
|
|
179
211
|
} = {},
|
|
180
212
|
): TelegramMessageOwnershipStore {
|
|
181
213
|
const records = new Map<string, TelegramMessageOwnershipRecord>();
|
|
@@ -198,20 +230,23 @@ export function createTelegramMessageOwnershipStore(
|
|
|
198
230
|
return record;
|
|
199
231
|
},
|
|
200
232
|
get: (chatId, messageId) => {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
options.getProfileKey?.(),
|
|
206
|
-
),
|
|
233
|
+
const key = getTelegramMessageOwnershipKey(
|
|
234
|
+
chatId,
|
|
235
|
+
messageId,
|
|
236
|
+
options.getProfileKey?.(),
|
|
207
237
|
);
|
|
238
|
+
const record = records.get(key);
|
|
208
239
|
if (!record) return undefined;
|
|
209
240
|
if (
|
|
210
241
|
record.ownerGeneration &&
|
|
211
242
|
options.isOwnerGenerationLive &&
|
|
212
243
|
!options.isOwnerGenerationLive(record)
|
|
213
244
|
) {
|
|
214
|
-
|
|
245
|
+
const replacement = options.resolveOwnerReplacement?.(record);
|
|
246
|
+
if (!replacement) return undefined;
|
|
247
|
+
const rebound = { ...record, ...replacement };
|
|
248
|
+
records.set(key, rebound);
|
|
249
|
+
return rebound;
|
|
215
250
|
}
|
|
216
251
|
return record;
|
|
217
252
|
},
|