@llblab/pi-telegram 0.18.2 → 0.18.4
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/BACKLOG.md +31 -2
- package/CHANGELOG.md +15 -0
- package/index.ts +30 -1
- package/lib/bus-follower.ts +69 -22
- package/lib/bus-leader.ts +10 -2
- package/lib/bus.ts +8 -0
- package/lib/menu-model.ts +10 -17
- package/lib/queue.ts +70 -0
- package/lib/routing.ts +40 -69
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -2,16 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
_Current deterministic status: Threaded Mode implementation, native typing/activity status, regression coverage, docs/context reconciliation, typecheck, full tests, pack check, audit, Domain DAG validation, context validation, and core live Threaded Mode smoke are green. This backlog intentionally tracks only release-relevant remaining work: native Windows smoke, evidence-gated Telegram client follow-ups, and upstream Pi API blockers._
|
|
4
4
|
|
|
5
|
+
## P0 — Live Threaded Mode Regression Sweep
|
|
6
|
+
|
|
7
|
+
Context: live Linux testing exposed regressions around prompt dispatch readiness, visible thread rename noise during automatic leader reclaim, status thread-name fallback flicker, and follower voice/update forwarding health. These are local runtime correctness issues and must be validated before returning to Windows smoke.
|
|
8
|
+
|
|
9
|
+
Open work:
|
|
10
|
+
|
|
11
|
+
- [x] Hide minimal model-menu one-page pagination and keep scope tabs progressive.
|
|
12
|
+
- [x] Add immediate-plus-deferred inbound prompt dispatch so queued prompts do not wait for a later `/reload` or command.
|
|
13
|
+
- [x] Stop automatic leader reclaim/reconciliation paths from visibly calling `editForumTopic` for internal identity restoration.
|
|
14
|
+
- [x] Prevent leader auto-claim of an unknown unbound thread while another live thread target exists, covering same-directory leader/follower smear risk.
|
|
15
|
+
- [x] Prefer local live leader/follower target labels over stale shared thread-store records when building prompt prefixes.
|
|
16
|
+
- [x] Permit follower-safe bot identity reads and own-chat native activity through the leader API proxy without granting cross-thread message/file/topic writes.
|
|
17
|
+
- [x] Preserve leader thread-name fallback in live status state to reduce `Dune`/generic `Telegram` flicker.
|
|
18
|
+
- [ ] Live smoke on Linux with one leader and one follower:
|
|
19
|
+
- [x] clean-state pass after removing `tmp/telegram` so stale diagnostic snapshots do not obscure live behavior;
|
|
20
|
+
- [ ] dirty-state pass with old `state.json`/`logs.jsonl` present to prove live locks, bus registration, target ownership, and reconciliation override stale diagnostics;
|
|
21
|
+
- [x] follower thread receives raw voice/message updates through the leader bus;
|
|
22
|
+
- [x] follower-local handlers from that instance's `telegram.json` process voice independently;
|
|
23
|
+
- [x] same-directory leader/follower sessions keep distinct thread bindings;
|
|
24
|
+
- [ ] leader reload recovers without duplicate visible thread renames;
|
|
25
|
+
- [ ] prompts dispatch without a second command;
|
|
26
|
+
- [ ] status remains stable around thread name and role while active turns start/end.
|
|
27
|
+
- [ ] Add/keep proactive race protections so `state.json` never becomes authoritative over live lock ownership, bus registration, or current target identity.
|
|
28
|
+
|
|
29
|
+
Done when: local Linux live Threaded Mode smoke is stable for leader reload, follower connect, prompt dispatch, voice forwarding, and status naming without visible rename noise.
|
|
30
|
+
|
|
5
31
|
## P0 — Native Windows Threaded Mode Support
|
|
6
32
|
|
|
7
|
-
Context: Threaded Mode uses a local leader/follower IPC bus. Unix-like platforms use Node `net` over Unix sockets; native Windows uses Node `net` named-pipe paths. The product expectation is identical behavior across both transports: leader/follower registration, heartbeats, forwarded Telegram API calls, thread target preservation, lifecycle cleanup, and shutdown semantics should not depend on socket-vs-pipe transport.
|
|
33
|
+
Context: Threaded Mode uses a local leader/follower IPC bus. Unix-like platforms use Node `net` over Unix sockets; native Windows uses Node `net` named-pipe paths. The product expectation is identical behavior across both transports: leader/follower registration, heartbeats, forwarded Telegram API calls, thread target preservation, lifecycle cleanup, and shutdown semantics should not depend on socket-vs-pipe transport. This remains after the local Linux regression sweep is green.
|
|
8
34
|
|
|
9
35
|
Open work:
|
|
10
36
|
|
|
11
37
|
- [ ] Live smoke Threaded Mode on native Windows without WSL.
|
|
12
38
|
- Scope: leader/follower `/telegram-connect`, follower heartbeat, forwarded Bot API calls, restore flows, lifecycle announcements, shutdown cleanup, and reconnect/reload behavior.
|
|
39
|
+
- Observed: a same-directory follower can see a live leader lock but fail registration with `connect ENOENT \\.\\pipe\\...`, leaving the follower disconnected during leader reload/hot activation timing.
|
|
40
|
+
- Observed: Windows/QEMU live polling/dispatch can lag until reload; inbound messages appear to increase the extension queue count, but the next queued item is not dispatched promptly.
|
|
13
41
|
- Baseline: deterministic path tests run everywhere, and a Windows-only named-pipe roundtrip regression runs when the suite executes on `win32`. Live Windows smoke remains unavailable in this environment.
|
|
14
|
-
- [
|
|
42
|
+
- [x] Add a minimized registration-boundary regression for transient leader endpoint startup races.
|
|
43
|
+
- [x] Add a session-bound queue dispatch watchdog so queued Telegram work can recover if a one-shot wakeup/timer is missed.
|
|
15
44
|
|
|
16
45
|
Done when: Threaded Mode leader/follower operation works on native Windows with the same safety guarantees as Unix-like systems, and unsupported transport assumptions are covered by tests/docs.
|
|
17
46
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.18.4: Windows Threaded Mode hotfix
|
|
6
|
+
|
|
7
|
+
- `[Windows IPC]` Follower registration now retries transient local bus connection failures while the leader named pipe/socket is still coming online. Impact: a same-directory Windows follower is less likely to fail `/telegram-connect` with `connect ENOENT \\.\\pipe\\...` during leader reload or hot Threaded Mode activation.
|
|
8
|
+
- `[Queue]` A session-bound queue dispatch watchdog now retries dispatch while Telegram work remains queued. Impact: if a platform drops the one-shot deferred dispatch wakeup, queued Telegram messages can resume without waiting for a manual `/reload`.
|
|
9
|
+
|
|
10
|
+
## 0.18.3: Threaded Mode live hotfix
|
|
11
|
+
|
|
12
|
+
- `[Threaded Mode]` Inbound Telegram prompts now request an immediate dispatch and a session-bound deferred retry. Impact: hosts where Pi is not yet dispatch-ready at update handling time no longer need a later `/reload` or command to process the queued prompt.
|
|
13
|
+
- `[Thread Lifecycle]` Automatic leader reclaim/reconciliation paths no longer call `editForumTopic` just to restore internal thread identity, and an unknown unbound thread is no longer auto-claimed by the leader while another live thread target exists. Prompt thread labels now prefer the local live leader/follower target over stale shared thread-store records. Impact: ordinary prompts and voice messages should not produce duplicate Telegram service messages such as `renamed the thread to ...`, and a same-directory follower thread is less likely to be smeared onto the leader binding or mislabeled in prompts.
|
|
14
|
+
- `[Threaded Mode]` Follower Bot API authorization now permits safe bot identity reads and chat-level typing/activity within the follower's assigned chat, while preserving thread-scoped write restrictions for messages/files/topic mutation. Impact: forwarded follower updates no longer fail just because native activity/capability paths use the aggregate chat surface.
|
|
15
|
+
- `[Status]` Leader target assignment now carries the live thread name into status fallback state. Impact: the status bar is less likely to flicker from `Dune Leader/Active` to generic `Telegram Leader` when the current active turn or thread-store lookup changes.
|
|
16
|
+
- `[Model Menu]` The Telegram model menu now hides one-page pagination controls and keeps scope tabs hidden unless scoped models exist. Impact: the minimal model menu shows only main-menu navigation and the available models.
|
|
17
|
+
|
|
3
18
|
## 0.18.2: setup pairing start hotfix
|
|
4
19
|
|
|
5
20
|
- `[Setup]` `/telegram-setup` now updates the live in-memory config immediately after persisting the validated bot token and before starting polling. Impact: first-time setup no longer shows `Send /start...` followed by `Telegram bot is not configured`, and `/start` can be received without restarting Pi.
|
package/index.ts
CHANGED
|
@@ -73,6 +73,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
73
73
|
BusFollower.createTelegramBusFollowerRegistrationState();
|
|
74
74
|
let telegramBusLeaderTarget: Config.TelegramProactivePushTarget | undefined;
|
|
75
75
|
let telegramBusLeaderSlot: string | undefined;
|
|
76
|
+
let telegramBusLeaderThreadName: string | undefined;
|
|
76
77
|
let telegramTopicModeUnavailable = false;
|
|
77
78
|
let telegramTopicProvisioningCount = 0;
|
|
78
79
|
const messageOwnershipStore = Ownership.createTelegramMessageOwnershipStore();
|
|
@@ -279,7 +280,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
279
280
|
if (threadStore.getBotState().threadMode === "disabled") return undefined;
|
|
280
281
|
return (
|
|
281
282
|
getCurrentThreadRecord()?.threadName ??
|
|
282
|
-
telegramBusFollowerRegistrationState.getThreadName()
|
|
283
|
+
telegramBusFollowerRegistrationState.getThreadName() ??
|
|
284
|
+
telegramBusLeaderThreadName
|
|
283
285
|
);
|
|
284
286
|
},
|
|
285
287
|
});
|
|
@@ -452,6 +454,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
452
454
|
...promptDispatchRuntime,
|
|
453
455
|
sendUserMessage,
|
|
454
456
|
}).dispatchNext;
|
|
457
|
+
const queueDispatchWatchdogRuntime =
|
|
458
|
+
Queue.createTelegramQueueDispatchWatchdogRuntime({
|
|
459
|
+
hasQueuedItems: telegramQueueStore.hasQueuedItems,
|
|
460
|
+
dispatchNextQueuedTelegramTurn,
|
|
461
|
+
recordRuntimeEvent,
|
|
462
|
+
});
|
|
455
463
|
const nativeMarkdownDraftSender =
|
|
456
464
|
TelegramApi.createTelegramNativeMarkdownDraftSender({
|
|
457
465
|
sendMessageDraft,
|
|
@@ -647,6 +655,22 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
647
655
|
followers: telegramBusFollowerRegistry.list(),
|
|
648
656
|
});
|
|
649
657
|
},
|
|
658
|
+
getLocalThreadLabelForTarget(target) {
|
|
659
|
+
const followerTarget = telegramBusFollowerRegistrationState.getTarget();
|
|
660
|
+
if (
|
|
661
|
+
followerTarget?.chatId === target.chatId &&
|
|
662
|
+
followerTarget.threadId === target.threadId
|
|
663
|
+
) {
|
|
664
|
+
return telegramBusFollowerRegistrationState.getThreadName();
|
|
665
|
+
}
|
|
666
|
+
if (
|
|
667
|
+
telegramBusLeaderTarget?.chatId === target.chatId &&
|
|
668
|
+
telegramBusLeaderTarget.threadId === target.threadId
|
|
669
|
+
) {
|
|
670
|
+
return telegramBusLeaderThreadName;
|
|
671
|
+
}
|
|
672
|
+
return undefined;
|
|
673
|
+
},
|
|
650
674
|
getCurrentLeaderEpoch,
|
|
651
675
|
getThreadReconciliationMachineState() {
|
|
652
676
|
return threadReconciliationRuntime.getState();
|
|
@@ -681,6 +705,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
681
705
|
dispatchNextQueuedTelegramTurn,
|
|
682
706
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
683
707
|
deferredQueueDispatchRuntime.request,
|
|
708
|
+
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
684
709
|
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
685
710
|
stopTypingLoop: typing.stop,
|
|
686
711
|
answerCallbackQuery,
|
|
@@ -880,6 +905,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
880
905
|
setLeaderTarget(input) {
|
|
881
906
|
telegramBusLeaderTarget = input.target;
|
|
882
907
|
telegramBusLeaderSlot = input.slot;
|
|
908
|
+
telegramBusLeaderThreadName = input.threadName;
|
|
883
909
|
},
|
|
884
910
|
onProvisioningStart() {
|
|
885
911
|
telegramTopicProvisioningCount += 1;
|
|
@@ -1099,6 +1125,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1099
1125
|
clearLeaderTarget() {
|
|
1100
1126
|
telegramBusLeaderTarget = undefined;
|
|
1101
1127
|
telegramBusLeaderSlot = undefined;
|
|
1128
|
+
telegramBusLeaderThreadName = undefined;
|
|
1102
1129
|
},
|
|
1103
1130
|
getSyncState() {
|
|
1104
1131
|
return telegramSyncState;
|
|
@@ -1147,8 +1174,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1147
1174
|
async onSessionStart(event, ctx) {
|
|
1148
1175
|
await lockedPollingRuntime.onSessionStart(event, ctx);
|
|
1149
1176
|
telegramThreadCapabilityMonitor.start(ctx);
|
|
1177
|
+
queueDispatchWatchdogRuntime.start(ctx);
|
|
1150
1178
|
},
|
|
1151
1179
|
async onSessionShutdown() {
|
|
1180
|
+
queueDispatchWatchdogRuntime.stop();
|
|
1152
1181
|
telegramThreadCapabilityMonitor.stop();
|
|
1153
1182
|
},
|
|
1154
1183
|
},
|
package/lib/bus-follower.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
|
|
23
23
|
export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
24
24
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
25
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = 10;
|
|
26
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = 150;
|
|
25
27
|
|
|
26
28
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
|
|
27
29
|
"__piTelegramFollowerSessionHandoff";
|
|
@@ -150,6 +152,8 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
150
152
|
getPid?: () => number;
|
|
151
153
|
timeoutMs?: number;
|
|
152
154
|
registrationTimeoutMs?: number;
|
|
155
|
+
registrationRetryAttempts?: number;
|
|
156
|
+
registrationRetryDelayMs?: number;
|
|
153
157
|
heartbeatMs?: number;
|
|
154
158
|
recordRuntimeEvent?: (
|
|
155
159
|
category: string,
|
|
@@ -364,6 +368,19 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
364
368
|
);
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
function isRetryableTelegramBusRegistrationError(error: unknown): boolean {
|
|
372
|
+
if (!(error instanceof Error)) return false;
|
|
373
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
374
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function delayTelegramBusFollowerRegistration(ms: number): Promise<void> {
|
|
378
|
+
return new Promise((resolve) => {
|
|
379
|
+
const timer = setTimeout(resolve, ms);
|
|
380
|
+
timer.unref?.();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
367
384
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
368
385
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
369
386
|
): () => Promise<void> {
|
|
@@ -588,6 +605,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
588
605
|
const heartbeatMs = deps.heartbeatMs ?? 1000;
|
|
589
606
|
const registrationTimeoutMs =
|
|
590
607
|
deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
|
|
608
|
+
const registrationRetryAttempts =
|
|
609
|
+
deps.registrationRetryAttempts ??
|
|
610
|
+
TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
|
|
611
|
+
const registrationRetryDelayMs =
|
|
612
|
+
deps.registrationRetryDelayMs ??
|
|
613
|
+
TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS;
|
|
591
614
|
let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
|
|
592
615
|
let activeLeaderSocketPath: string | undefined;
|
|
593
616
|
let activeAuthSecret: string | undefined;
|
|
@@ -646,30 +669,54 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
646
669
|
await deps.startReceiving?.();
|
|
647
670
|
activeAuthSecret = deps.getLeaderAuthSecret?.(leader);
|
|
648
671
|
deps.setActiveAuthSecret?.(activeAuthSecret);
|
|
672
|
+
const createRegistrationEnvelope = (): Extract<
|
|
673
|
+
TelegramBusEnvelope,
|
|
674
|
+
{ kind: "follower.register" }
|
|
675
|
+
> => ({
|
|
676
|
+
kind: "follower.register",
|
|
677
|
+
requestId: deps.createRequestId(),
|
|
678
|
+
auth: activeAuthSecret,
|
|
679
|
+
registration: {
|
|
680
|
+
instanceId: deps.instanceId,
|
|
681
|
+
profileKey:
|
|
682
|
+
deps.getProfileKey?.(ctx) ??
|
|
683
|
+
(ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
|
|
684
|
+
threadName:
|
|
685
|
+
deps.getThreadName?.(ctx) ??
|
|
686
|
+
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
687
|
+
cwd: ctx.cwd,
|
|
688
|
+
pid: getPid(),
|
|
689
|
+
busSocketPath: deps.followerBusSocketPath,
|
|
690
|
+
connectedAtMs: getNowMs(),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
649
693
|
let response: TelegramBusEnvelope | undefined;
|
|
650
694
|
try {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
695
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
696
|
+
try {
|
|
697
|
+
response = await sendTelegramBusLocalEnvelope({
|
|
698
|
+
socketPath: leaderSocketPath,
|
|
699
|
+
timeoutMs: registrationTimeoutMs,
|
|
700
|
+
envelope: createRegistrationEnvelope(),
|
|
701
|
+
});
|
|
702
|
+
break;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (
|
|
705
|
+
attempt >= registrationRetryAttempts ||
|
|
706
|
+
!isRetryableTelegramBusRegistrationError(error)
|
|
707
|
+
) {
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
711
|
+
phase: "follower-register-retry",
|
|
712
|
+
attempt,
|
|
713
|
+
socketPath: leaderSocketPath,
|
|
714
|
+
});
|
|
715
|
+
await delayTelegramBusFollowerRegistration(
|
|
716
|
+
registrationRetryDelayMs,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
673
720
|
} catch (error) {
|
|
674
721
|
stopHeartbeat();
|
|
675
722
|
activeLeaderSocketPath = undefined;
|
package/lib/bus-leader.ts
CHANGED
|
@@ -68,7 +68,11 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
|
|
|
68
68
|
) => void;
|
|
69
69
|
getSyncState: () => Sync.TelegramSyncState;
|
|
70
70
|
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
71
|
-
setLeaderTarget: (input: {
|
|
71
|
+
setLeaderTarget: (input: {
|
|
72
|
+
target: TelegramTarget;
|
|
73
|
+
slot?: string;
|
|
74
|
+
threadName?: string;
|
|
75
|
+
}) => void;
|
|
72
76
|
onProvisioningStart?: () => void;
|
|
73
77
|
onProvisioningEnd?: () => void;
|
|
74
78
|
recordRuntimeEvent: (
|
|
@@ -476,7 +480,11 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
476
480
|
deps.onProvisioningEnd?.();
|
|
477
481
|
}
|
|
478
482
|
if (!ownTarget) return;
|
|
479
|
-
deps.setLeaderTarget({
|
|
483
|
+
deps.setLeaderTarget({
|
|
484
|
+
target: ownTarget.target,
|
|
485
|
+
slot: ownTarget.slot,
|
|
486
|
+
threadName: ownTarget.threadName,
|
|
487
|
+
});
|
|
480
488
|
const nowMs = getNowMs();
|
|
481
489
|
let syncState = deps.getSyncState();
|
|
482
490
|
syncState = Sync.markTelegramSyncSliceFresh(syncState, "target-bindings", {
|
package/lib/bus.ts
CHANGED
|
@@ -127,6 +127,12 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
127
127
|
if (target.threadId === undefined) return true;
|
|
128
128
|
return matchesId(record.message_thread_id, target.threadId);
|
|
129
129
|
};
|
|
130
|
+
const isTargetChatScoped = (body: unknown): boolean => {
|
|
131
|
+
if (!target) return false;
|
|
132
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
133
|
+
const record = body as Record<string, unknown>;
|
|
134
|
+
return matchesId(record.chat_id, target.chatId);
|
|
135
|
+
};
|
|
130
136
|
if (input.method === "downloadFile") return true;
|
|
131
137
|
if (input.method === "call") {
|
|
132
138
|
const apiMethod = input.args[0];
|
|
@@ -137,6 +143,8 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
137
143
|
) {
|
|
138
144
|
return true;
|
|
139
145
|
}
|
|
146
|
+
if (apiMethod === "getMe") return true;
|
|
147
|
+
if (apiMethod === "sendChatAction") return isTargetChatScoped(input.args[1]);
|
|
140
148
|
return allowedCallMethods.has(apiMethod) && isTargetScoped(input.args[1]);
|
|
141
149
|
}
|
|
142
150
|
if (input.method === "callMultipart") {
|
package/lib/menu-model.ts
CHANGED
|
@@ -1023,23 +1023,16 @@ export function buildModelMenuReplyMarkup(
|
|
|
1023
1023
|
menuPage.page === 0 ? menuPage.pageCount - 1 : menuPage.page - 1;
|
|
1024
1024
|
const nextPage =
|
|
1025
1025
|
menuPage.page === menuPage.pageCount - 1 ? 0 : menuPage.page + 1;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
: [
|
|
1037
|
-
{
|
|
1038
|
-
text: `${menuPage.page + 1}/${menuPage.pageCount}`,
|
|
1039
|
-
callback_data: "model:pages",
|
|
1040
|
-
},
|
|
1041
|
-
],
|
|
1042
|
-
);
|
|
1026
|
+
if (menuPage.pageCount > 1) {
|
|
1027
|
+
rows.push([
|
|
1028
|
+
{ text: "⬅️", callback_data: `model:page:${previousPage}` },
|
|
1029
|
+
{
|
|
1030
|
+
text: `${menuPage.page + 1}/${menuPage.pageCount}`,
|
|
1031
|
+
callback_data: "model:pages",
|
|
1032
|
+
},
|
|
1033
|
+
{ text: "➡️", callback_data: `model:page:${nextPage}` },
|
|
1034
|
+
]);
|
|
1035
|
+
}
|
|
1043
1036
|
rows.push(
|
|
1044
1037
|
...menuPage.items.map((entry, index) => [
|
|
1045
1038
|
{
|
package/lib/queue.ts
CHANGED
|
@@ -1920,6 +1920,76 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
|
|
|
1920
1920
|
};
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
1923
|
+
// --- Dispatch Watchdog Runtime ---
|
|
1924
|
+
|
|
1925
|
+
export interface TelegramQueueDispatchWatchdogRuntime<TContext = unknown> {
|
|
1926
|
+
start: (ctx: TContext) => void;
|
|
1927
|
+
stop: () => void;
|
|
1928
|
+
poke: () => void;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
export interface TelegramQueueDispatchWatchdogRuntimeDeps<
|
|
1932
|
+
TContext = unknown,
|
|
1933
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
1934
|
+
hasQueuedItems: () => boolean;
|
|
1935
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1936
|
+
intervalMs?: number;
|
|
1937
|
+
setInterval?: (
|
|
1938
|
+
callback: () => void,
|
|
1939
|
+
ms: number,
|
|
1940
|
+
) => ReturnType<typeof setInterval>;
|
|
1941
|
+
clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
export function createTelegramQueueDispatchWatchdogRuntime<
|
|
1945
|
+
TContext = unknown,
|
|
1946
|
+
>(
|
|
1947
|
+
deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
|
|
1948
|
+
): TelegramQueueDispatchWatchdogRuntime<TContext> {
|
|
1949
|
+
const intervalMs = deps.intervalMs ?? 1000;
|
|
1950
|
+
const setIntervalFn: NonNullable<
|
|
1951
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["setInterval"]
|
|
1952
|
+
> = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
|
|
1953
|
+
const clearIntervalFn: NonNullable<
|
|
1954
|
+
TelegramQueueDispatchWatchdogRuntimeDeps<TContext>["clearInterval"]
|
|
1955
|
+
> = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
1956
|
+
let ctx: TContext | undefined;
|
|
1957
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
1958
|
+
let dispatchInFlight = false;
|
|
1959
|
+
const tick = (): void => {
|
|
1960
|
+
if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems()) return;
|
|
1961
|
+
dispatchInFlight = true;
|
|
1962
|
+
try {
|
|
1963
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
1966
|
+
phase: "queue-watchdog",
|
|
1967
|
+
});
|
|
1968
|
+
} finally {
|
|
1969
|
+
dispatchInFlight = false;
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
const stop = (): void => {
|
|
1973
|
+
ctx = undefined;
|
|
1974
|
+
if (!interval) return;
|
|
1975
|
+
clearIntervalFn(interval);
|
|
1976
|
+
interval = undefined;
|
|
1977
|
+
};
|
|
1978
|
+
return {
|
|
1979
|
+
start: (nextCtx) => {
|
|
1980
|
+
ctx = nextCtx;
|
|
1981
|
+
if (!interval) {
|
|
1982
|
+
const nextInterval = setIntervalFn(tick, intervalMs);
|
|
1983
|
+
interval = nextInterval;
|
|
1984
|
+
nextInterval.unref?.();
|
|
1985
|
+
}
|
|
1986
|
+
tick();
|
|
1987
|
+
},
|
|
1988
|
+
stop,
|
|
1989
|
+
poke: tick,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1923
1993
|
// --- Dispatch Runtime ---
|
|
1924
1994
|
|
|
1925
1995
|
export interface TelegramPromptDeliveryOptions {
|
package/lib/routing.ts
CHANGED
|
@@ -415,6 +415,9 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
415
415
|
getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
|
|
416
416
|
getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
|
|
417
417
|
getLiveThreadTargets?: () => Queue.TelegramQueueTarget[];
|
|
418
|
+
getLocalThreadLabelForTarget?: (
|
|
419
|
+
target: Queue.TelegramQueueTarget,
|
|
420
|
+
) => string | undefined;
|
|
418
421
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
419
422
|
getThreadReconciliationMachineState?: () =>
|
|
420
423
|
| ThreadReconciler.ThreadReconciliationMachineState
|
|
@@ -484,6 +487,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
484
487
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
485
488
|
dispatch: (ctx: TContext) => void,
|
|
486
489
|
) => void;
|
|
490
|
+
hasDeferredDispatchContext?: () => boolean;
|
|
487
491
|
startTypingLoop?: (
|
|
488
492
|
ctx: TContext,
|
|
489
493
|
chatId?: number,
|
|
@@ -593,6 +597,17 @@ export function createTelegramInboundRouteRuntime<
|
|
|
593
597
|
const guidedUnboundTopicKeys = new Set<string>();
|
|
594
598
|
let nextUnboundRerouteId = 0;
|
|
595
599
|
let nextAllTabCommandId = 0;
|
|
600
|
+
const requestDispatchNextQueuedTelegramTurn = (ctx: TContext): void => {
|
|
601
|
+
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
602
|
+
if (
|
|
603
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn &&
|
|
604
|
+
deps.hasDeferredDispatchContext?.() !== false
|
|
605
|
+
) {
|
|
606
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
607
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
596
611
|
const prunePendingUnboundReroutes = () => {
|
|
597
612
|
const nowMs = Date.now();
|
|
598
613
|
for (const [id, entry] of pendingUnboundReroutes) {
|
|
@@ -712,7 +727,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
712
727
|
};
|
|
713
728
|
deps.queueMutationRuntime.append(turn, ctx);
|
|
714
729
|
deps.updateStatus(ctx);
|
|
715
|
-
|
|
730
|
+
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
716
731
|
},
|
|
717
732
|
});
|
|
718
733
|
const cloneTelegramMessagesForThread = (
|
|
@@ -1257,7 +1272,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1257
1272
|
context,
|
|
1258
1273
|
);
|
|
1259
1274
|
deps.updateStatus(context);
|
|
1260
|
-
|
|
1275
|
+
requestDispatchNextQueuedTelegramTurn(context);
|
|
1261
1276
|
},
|
|
1262
1277
|
},
|
|
1263
1278
|
);
|
|
@@ -1350,10 +1365,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1350
1365
|
const chatId = message.chat.id;
|
|
1351
1366
|
const threadId = message.message_thread_id;
|
|
1352
1367
|
if (!threadId) return undefined;
|
|
1368
|
+
const localLabel = deps.getLocalThreadLabelForTarget?.({
|
|
1369
|
+
chatId,
|
|
1370
|
+
threadId,
|
|
1371
|
+
});
|
|
1372
|
+
if (localLabel) return localLabel;
|
|
1353
1373
|
const records = deps.threadStore.list();
|
|
1374
|
+
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1354
1375
|
for (const r of records) {
|
|
1355
1376
|
if (r.target.chatId !== chatId || r.target.threadId !== threadId)
|
|
1356
1377
|
continue;
|
|
1378
|
+
if (currentInstanceId && r.instanceId && r.instanceId !== currentInstanceId)
|
|
1379
|
+
continue;
|
|
1357
1380
|
return r.threadName &&
|
|
1358
1381
|
Threads.isTelegramTopicThreadNameValidForSlot(r.threadName, r.slot)
|
|
1359
1382
|
? r.threadName
|
|
@@ -1380,7 +1403,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1380
1403
|
statusSummary: "continue",
|
|
1381
1404
|
};
|
|
1382
1405
|
deps.queueMutationRuntime.append(continueTurn, ctx);
|
|
1383
|
-
|
|
1406
|
+
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
1384
1407
|
};
|
|
1385
1408
|
const reservedCommandNames = () =>
|
|
1386
1409
|
new Set(Commands.getTelegramReservedCommandNames());
|
|
@@ -1454,7 +1477,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1454
1477
|
: { ...turn, replyToMessageId: 0 };
|
|
1455
1478
|
},
|
|
1456
1479
|
updateStatus: deps.updateStatus,
|
|
1457
|
-
dispatchNextQueuedTelegramTurn:
|
|
1480
|
+
dispatchNextQueuedTelegramTurn: requestDispatchNextQueuedTelegramTurn,
|
|
1458
1481
|
}).enqueue;
|
|
1459
1482
|
const sendUnboundRerouteChooserNow = async (
|
|
1460
1483
|
messages: TMessage[],
|
|
@@ -1831,7 +1854,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1831
1854
|
Queue.appendTelegramQueueItem(items, guestTurn),
|
|
1832
1855
|
);
|
|
1833
1856
|
deps.updateStatus(ctx);
|
|
1834
|
-
|
|
1857
|
+
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
1835
1858
|
};
|
|
1836
1859
|
return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
|
|
1837
1860
|
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
@@ -1951,7 +1974,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1951
1974
|
]).trim();
|
|
1952
1975
|
const instanceId = deps.getCurrentInstanceId?.();
|
|
1953
1976
|
const leaderProfileKey = getLeaderTopicProfileKey(ctx, instanceId);
|
|
1954
|
-
const
|
|
1977
|
+
const records = deps.threadStore.list();
|
|
1978
|
+
const routableRecords = getTelegramRoutableThreadRecords(
|
|
1979
|
+
records,
|
|
1980
|
+
deps.getLiveThreadTargets?.(),
|
|
1981
|
+
);
|
|
1982
|
+
const hasAnyRoutableThread = routableRecords.length > 0;
|
|
1983
|
+
const existing = records.find((r) => {
|
|
1955
1984
|
return (
|
|
1956
1985
|
r.target.chatId === target.chatId &&
|
|
1957
1986
|
r.target.threadId === target.threadId
|
|
@@ -2003,7 +2032,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2003
2032
|
deps.threadStore.list(),
|
|
2004
2033
|
leaderProfileKey,
|
|
2005
2034
|
instanceId,
|
|
2006
|
-
)
|
|
2035
|
+
) &&
|
|
2036
|
+
!hasAnyRoutableThread
|
|
2007
2037
|
) {
|
|
2008
2038
|
const priorLeaderRecord =
|
|
2009
2039
|
deps.threadStore.getByProfileKey(leaderProfileKey);
|
|
@@ -2039,25 +2069,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2039
2069
|
slot,
|
|
2040
2070
|
});
|
|
2041
2071
|
await deps.threadStore.persist();
|
|
2042
|
-
if (deps.callApi) {
|
|
2043
|
-
try {
|
|
2044
|
-
await deps.callApi("editForumTopic", {
|
|
2045
|
-
chat_id: target.chatId,
|
|
2046
|
-
message_thread_id: target.threadId,
|
|
2047
|
-
name: Threads.getTelegramTopicTitleForThreadName(
|
|
2048
|
-
threadName,
|
|
2049
|
-
slot,
|
|
2050
|
-
),
|
|
2051
|
-
});
|
|
2052
|
-
} catch (renameError) {
|
|
2053
|
-
deps.recordRuntimeEvent?.("telegram", renameError, {
|
|
2054
|
-
phase: "leader-topic-reclaim-rename",
|
|
2055
|
-
chatId: target.chatId,
|
|
2056
|
-
threadId: target.threadId,
|
|
2057
|
-
slot,
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
2072
|
deps.recordRuntimeEvent?.(
|
|
2062
2073
|
"bus",
|
|
2063
2074
|
"Bus leader reclaimed unbound thread",
|
|
@@ -2106,13 +2117,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2106
2117
|
);
|
|
2107
2118
|
return;
|
|
2108
2119
|
}
|
|
2109
|
-
const records = deps.threadStore.list();
|
|
2110
2120
|
const command = getKnownTelegramAllTabCommand(text);
|
|
2111
|
-
if (
|
|
2112
|
-
command &&
|
|
2113
|
-
getTelegramRoutableThreadRecords(records, deps.getLiveThreadTargets?.())
|
|
2114
|
-
.length > 0
|
|
2115
|
-
) {
|
|
2121
|
+
if (command && hasAnyRoutableThread) {
|
|
2116
2122
|
if (
|
|
2117
2123
|
await sendAllTabCommandChooser(command, text, message as TMessage, {
|
|
2118
2124
|
target: { chatId: target.chatId, threadId: target.threadId },
|
|
@@ -2172,23 +2178,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2172
2178
|
slot,
|
|
2173
2179
|
});
|
|
2174
2180
|
await deps.threadStore.persist();
|
|
2175
|
-
try {
|
|
2176
|
-
await deps.callApi("editForumTopic", {
|
|
2177
|
-
chat_id: target.chatId,
|
|
2178
|
-
message_thread_id: target.threadId,
|
|
2179
|
-
name: Threads.getTelegramTopicTitleForThreadName(
|
|
2180
|
-
threadName,
|
|
2181
|
-
slot,
|
|
2182
|
-
),
|
|
2183
|
-
});
|
|
2184
|
-
} catch (renameError) {
|
|
2185
|
-
deps.recordRuntimeEvent?.("telegram", renameError, {
|
|
2186
|
-
phase: "leader-topic-unbound-reclaim-rename",
|
|
2187
|
-
chatId: target.chatId,
|
|
2188
|
-
threadId: target.threadId,
|
|
2189
|
-
slot,
|
|
2190
|
-
});
|
|
2191
|
-
}
|
|
2192
2181
|
deps.recordRuntimeEvent?.(
|
|
2193
2182
|
"bus",
|
|
2194
2183
|
"Bus leader reclaimed stale-current unbound thread",
|
|
@@ -2208,7 +2197,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2208
2197
|
}
|
|
2209
2198
|
if (
|
|
2210
2199
|
leaderProfileKey &&
|
|
2211
|
-
!hasActiveLeaderTopic(records, leaderProfileKey, instanceId)
|
|
2200
|
+
!hasActiveLeaderTopic(records, leaderProfileKey, instanceId) &&
|
|
2201
|
+
!hasAnyRoutableThread
|
|
2212
2202
|
) {
|
|
2213
2203
|
const priorLeaderRecord =
|
|
2214
2204
|
deps.threadStore.getByProfileKey(leaderProfileKey);
|
|
@@ -2254,25 +2244,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2254
2244
|
slot,
|
|
2255
2245
|
});
|
|
2256
2246
|
await deps.threadStore.persist();
|
|
2257
|
-
if (deps.callApi) {
|
|
2258
|
-
try {
|
|
2259
|
-
await deps.callApi("editForumTopic", {
|
|
2260
|
-
chat_id: target.chatId,
|
|
2261
|
-
message_thread_id: target.threadId,
|
|
2262
|
-
name: Threads.getTelegramTopicTitleForThreadName(
|
|
2263
|
-
threadName,
|
|
2264
|
-
slot,
|
|
2265
|
-
),
|
|
2266
|
-
});
|
|
2267
|
-
} catch (renameError) {
|
|
2268
|
-
deps.recordRuntimeEvent?.("telegram", renameError, {
|
|
2269
|
-
phase: "leader-topic-reclaim-rename",
|
|
2270
|
-
chatId: target.chatId,
|
|
2271
|
-
threadId: target.threadId,
|
|
2272
|
-
slot,
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
2247
|
deps.recordRuntimeEvent?.(
|
|
2277
2248
|
"bus",
|
|
2278
2249
|
"Bus leader reclaimed unbound thread",
|