@llblab/pi-telegram 0.27.12 → 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 +393 -443
- 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 +4 -2
- 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;
|
|
@@ -1161,7 +1162,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1161
1162
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1162
1163
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1163
1164
|
let sessionAutoStartGeneration = 0;
|
|
1164
|
-
const ownershipCheckMs =
|
|
1165
|
+
const ownershipCheckMs =
|
|
1166
|
+
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1165
1167
|
const ownershipRefreshMs =
|
|
1166
1168
|
deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
1167
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
|
},
|
package/lib/paths.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* from environment and runtime identity. It does not read config, manage
|
|
8
8
|
* state, or import broader Telegram domains.
|
|
9
9
|
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join, resolve } from "node:path";
|
|
12
13
|
|
|
@@ -86,6 +87,40 @@ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** Durable inbound update journal (<agentDir>/tmp/telegram/inbox[.<profile>].json). */
|
|
91
|
+
export function resolveTelegramUpdateJournalPath(
|
|
92
|
+
agentDir = resolveAgentDir(),
|
|
93
|
+
profileName?: string,
|
|
94
|
+
): string {
|
|
95
|
+
return resolveTelegramProfileTempFilePath(
|
|
96
|
+
"inbox",
|
|
97
|
+
"json",
|
|
98
|
+
agentDir,
|
|
99
|
+
profileName,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
104
|
+
export function resolveTelegramFollowerJournalPath(
|
|
105
|
+
recipientBindingKey: string,
|
|
106
|
+
agentDir = resolveAgentDir(),
|
|
107
|
+
profileName?: string,
|
|
108
|
+
): string {
|
|
109
|
+
if (!recipientBindingKey) {
|
|
110
|
+
throw new Error("Telegram follower journal binding key is required.");
|
|
111
|
+
}
|
|
112
|
+
const bindingHash = createHash("sha256")
|
|
113
|
+
.update(recipientBindingKey)
|
|
114
|
+
.digest("hex")
|
|
115
|
+
.slice(0, 16);
|
|
116
|
+
return resolveTelegramProfileTempFilePath(
|
|
117
|
+
`follower-inbox-${bindingHash}`,
|
|
118
|
+
"json",
|
|
119
|
+
agentDir,
|
|
120
|
+
profileName,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
90
125
|
export function resolveTelegramRuntimeLogPath(): string {
|
|
91
126
|
return resolveTelegramProfileTempFilePath("logs", "jsonl");
|