@llblab/pi-telegram 0.24.0 → 0.24.1
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 +2 -2
- package/BACKLOG.md +6 -8
- package/CHANGELOG.md +124 -274
- package/docs/architecture.md +13 -1
- package/docs/multi-instance-bus.md +3 -3
- package/docs/public-api.md +2 -0
- package/lib/bus-transport.ts +7 -14
- package/lib/bus.ts +48 -19
- package/lib/command-templates.ts +36 -20
- package/lib/config.ts +6 -34
- package/lib/inbound.ts +22 -26
- package/lib/lifecycle.ts +0 -13
- package/lib/locks.ts +46 -26
- package/lib/logs.ts +76 -18
- package/lib/media.ts +0 -9
- package/lib/menu-model.ts +5 -12
- package/lib/menu.ts +0 -19
- package/lib/outbound.ts +2 -12
- package/lib/pi.ts +0 -4
- package/lib/queue.ts +0 -5
- package/lib/replies.ts +0 -23
- package/lib/runtime.ts +1 -2
- package/lib/sections.ts +1 -48
- package/lib/status.ts +14 -8
- package/lib/telegram-api.ts +0 -4
- package/lib/threads.ts +41 -8
- package/package.json +1 -1
package/lib/locks.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { basename, dirname, join } from "node:path";
|
|
|
22
22
|
import { resolveTelegramOwnersPath } from "./paths.ts";
|
|
23
23
|
|
|
24
24
|
export const TELEGRAM_LOCK_KEY = "default";
|
|
25
|
-
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS =
|
|
25
|
+
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
26
|
+
const TELEGRAM_OWNERSHIP_REFRESH_MS = 2_000;
|
|
26
27
|
const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
|
|
27
28
|
const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
|
|
28
29
|
const TELEGRAM_LOCK_TRANSACTION_ATTEMPTS = 80;
|
|
@@ -181,8 +182,7 @@ interface TelegramLockTransactionOwner {
|
|
|
181
182
|
generation: string;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
const TELEGRAM_TRANSACTION_OWNER_PATTERN =
|
|
185
|
-
/^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
185
|
+
const TELEGRAM_TRANSACTION_OWNER_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
186
186
|
|
|
187
187
|
function getLockTransactionOwnerFile(generation: string): string {
|
|
188
188
|
return `owner.${generation}.json`;
|
|
@@ -350,6 +350,8 @@ type TelegramTransactionGlobal = typeof globalThis & {
|
|
|
350
350
|
|
|
351
351
|
export interface TelegramFileTransactionOptions {
|
|
352
352
|
recoveryRename?: typeof renameSync;
|
|
353
|
+
attempts?: number;
|
|
354
|
+
retryDelayMs?: number;
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
function getActiveTransactionReclaims(): Set<string> {
|
|
@@ -366,7 +368,13 @@ function reclaimAbandonedDirectoryGuard(
|
|
|
366
368
|
} catch {
|
|
367
369
|
return false;
|
|
368
370
|
}
|
|
369
|
-
|
|
371
|
+
let entries: string[];
|
|
372
|
+
try {
|
|
373
|
+
entries = readdirSync(path);
|
|
374
|
+
} catch {
|
|
375
|
+
// Another recovery candidate may remove the observed guard after lstat.
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
370
378
|
if (entries.length !== 1) return false;
|
|
371
379
|
const entry = entries[0];
|
|
372
380
|
let observedPid: number;
|
|
@@ -382,10 +390,7 @@ function reclaimAbandonedDirectoryGuard(
|
|
|
382
390
|
observedReclaimGeneration = match[2];
|
|
383
391
|
}
|
|
384
392
|
const activeReclaims = getActiveTransactionReclaims();
|
|
385
|
-
if (
|
|
386
|
-
observedPid === process.pid &&
|
|
387
|
-
observedReclaimGeneration !== undefined
|
|
388
|
-
) {
|
|
393
|
+
if (observedPid === process.pid && observedReclaimGeneration !== undefined) {
|
|
389
394
|
if (activeReclaims.has(observedReclaimGeneration)) return false;
|
|
390
395
|
} else if (isProcessAlive(observedPid)) {
|
|
391
396
|
return false;
|
|
@@ -564,10 +569,7 @@ function recoverAbandonedLockTransaction(
|
|
|
564
569
|
}
|
|
565
570
|
|
|
566
571
|
const recoveryGuardPath = `${path}.recovery`;
|
|
567
|
-
const recoveryOwner = acquireLegacyRecoveryGuard(
|
|
568
|
-
recoveryGuardPath,
|
|
569
|
-
options,
|
|
570
|
-
);
|
|
572
|
+
const recoveryOwner = acquireLegacyRecoveryGuard(recoveryGuardPath, options);
|
|
571
573
|
if (!recoveryOwner) return undefined;
|
|
572
574
|
let recoveredOwner: TelegramLockTransactionOwner | undefined;
|
|
573
575
|
try {
|
|
@@ -606,24 +608,28 @@ function acquireLockTransaction(
|
|
|
606
608
|
path: string,
|
|
607
609
|
options: TelegramFileTransactionOptions = {},
|
|
608
610
|
): TelegramLockTransactionOwner {
|
|
611
|
+
const attempts = Math.max(
|
|
612
|
+
1,
|
|
613
|
+
options.attempts ?? TELEGRAM_LOCK_TRANSACTION_ATTEMPTS,
|
|
614
|
+
);
|
|
615
|
+
const retryDelayMs = Math.max(
|
|
616
|
+
0,
|
|
617
|
+
options.retryDelayMs ?? TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS,
|
|
618
|
+
);
|
|
609
619
|
mkdirSync(dirname(path), { recursive: true });
|
|
610
|
-
for (
|
|
611
|
-
let attempt = 0;
|
|
612
|
-
attempt < TELEGRAM_LOCK_TRANSACTION_ATTEMPTS;
|
|
613
|
-
attempt += 1
|
|
614
|
-
) {
|
|
620
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
615
621
|
try {
|
|
616
622
|
return createLockTransactionGuard(path);
|
|
617
623
|
} catch (error) {
|
|
618
624
|
if (!isLockTransactionContentionError(error, path)) throw error;
|
|
619
625
|
const recoveredOwner = recoverAbandonedLockTransaction(path, options);
|
|
620
626
|
if (recoveredOwner !== undefined) return recoveredOwner;
|
|
621
|
-
if (attempt ===
|
|
627
|
+
if (attempt === attempts - 1) {
|
|
622
628
|
throw new Error(
|
|
623
629
|
`Timed out acquiring Telegram lock transaction: ${path}`,
|
|
624
630
|
);
|
|
625
631
|
}
|
|
626
|
-
sleepSync(
|
|
632
|
+
sleepSync(retryDelayMs);
|
|
627
633
|
}
|
|
628
634
|
}
|
|
629
635
|
throw new Error(`Failed to acquire Telegram lock transaction: ${path}`);
|
|
@@ -1140,6 +1146,7 @@ export interface TelegramLockedPollingRuntimeDeps<
|
|
|
1140
1146
|
details?: Record<string, unknown>,
|
|
1141
1147
|
) => void;
|
|
1142
1148
|
ownershipCheckMs?: number;
|
|
1149
|
+
ownershipRefreshMs?: number;
|
|
1143
1150
|
}
|
|
1144
1151
|
|
|
1145
1152
|
function snapshotLockContext(ctx: TelegramLockContext): TelegramLockContext {
|
|
@@ -1151,16 +1158,20 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1151
1158
|
>(
|
|
1152
1159
|
deps: TelegramLockedPollingRuntimeDeps<TContext>,
|
|
1153
1160
|
): TelegramLockedPollingRuntime<TContext> {
|
|
1154
|
-
let
|
|
1161
|
+
let ownershipCheckInterval: ReturnType<typeof setInterval> | undefined;
|
|
1162
|
+
let ownershipRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
1155
1163
|
let ownershipStop: Promise<void> | undefined;
|
|
1156
1164
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1157
1165
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1158
1166
|
let sessionAutoStartGeneration = 0;
|
|
1159
1167
|
const ownershipCheckMs = deps.ownershipCheckMs ?? 1000;
|
|
1168
|
+
const ownershipRefreshMs =
|
|
1169
|
+
deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
1160
1170
|
const stopOwnershipWatcher = () => {
|
|
1161
|
-
if (
|
|
1162
|
-
clearInterval(
|
|
1163
|
-
|
|
1171
|
+
if (ownershipCheckInterval) clearInterval(ownershipCheckInterval);
|
|
1172
|
+
if (ownershipRefreshInterval) clearInterval(ownershipRefreshInterval);
|
|
1173
|
+
ownershipCheckInterval = undefined;
|
|
1174
|
+
ownershipRefreshInterval = undefined;
|
|
1164
1175
|
};
|
|
1165
1176
|
const suspendPolling = async () => {
|
|
1166
1177
|
sessionAutoStartGeneration += 1;
|
|
@@ -1190,15 +1201,24 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1190
1201
|
const startOwnershipWatcher = (ctx: TContext) => {
|
|
1191
1202
|
const owner = snapshotLockContext(ctx);
|
|
1192
1203
|
stopOwnershipWatcher();
|
|
1193
|
-
|
|
1204
|
+
ownershipCheckInterval = setInterval(() => {
|
|
1205
|
+
try {
|
|
1206
|
+
if (deps.lock.owns(owner)) return;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "check" });
|
|
1209
|
+
}
|
|
1210
|
+
stopAfterOwnershipLoss();
|
|
1211
|
+
}, ownershipCheckMs);
|
|
1212
|
+
ownershipRefreshInterval = setInterval(() => {
|
|
1194
1213
|
try {
|
|
1195
1214
|
if (deps.lock.refresh(owner)) return;
|
|
1196
1215
|
} catch (error) {
|
|
1197
1216
|
deps.recordRuntimeEvent?.("lock", error, { phase: "refresh" });
|
|
1198
1217
|
}
|
|
1199
1218
|
stopAfterOwnershipLoss();
|
|
1200
|
-
},
|
|
1201
|
-
|
|
1219
|
+
}, ownershipRefreshMs);
|
|
1220
|
+
ownershipCheckInterval.unref?.();
|
|
1221
|
+
ownershipRefreshInterval.unref?.();
|
|
1202
1222
|
};
|
|
1203
1223
|
const runOwnedPollingStart = async (
|
|
1204
1224
|
ctx: TContext,
|
package/lib/logs.ts
CHANGED
|
@@ -102,6 +102,12 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
102
102
|
const getNowMs = options.getNowMs ?? Date.now;
|
|
103
103
|
const scopeKeys = new Map<string, string | undefined>();
|
|
104
104
|
let pending: Promise<void> = Promise.resolve();
|
|
105
|
+
let appendScheduled = false;
|
|
106
|
+
let queuedAppends: {
|
|
107
|
+
path: string;
|
|
108
|
+
previousPath: string;
|
|
109
|
+
line: string;
|
|
110
|
+
}[] = [];
|
|
105
111
|
|
|
106
112
|
const ensureParent = (path: string) => {
|
|
107
113
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
@@ -156,27 +162,79 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
156
162
|
};
|
|
157
163
|
|
|
158
164
|
const appendLine = (line: string) => {
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
queuedAppends.push({
|
|
166
|
+
path: resolvePath(),
|
|
167
|
+
previousPath: resolvePreviousPath(),
|
|
168
|
+
line,
|
|
169
|
+
});
|
|
170
|
+
if (appendScheduled) return;
|
|
171
|
+
appendScheduled = true;
|
|
161
172
|
pending = pending
|
|
162
173
|
.then(() => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
appendScheduled = false;
|
|
175
|
+
const batch = queuedAppends;
|
|
176
|
+
queuedAppends = [];
|
|
177
|
+
const groups = new Map<
|
|
178
|
+
string,
|
|
179
|
+
{ path: string; previousPath: string; lines: string[] }
|
|
180
|
+
>();
|
|
181
|
+
for (const entry of batch) {
|
|
182
|
+
const key = `${entry.path}\u0000${entry.previousPath}`;
|
|
183
|
+
const group = groups.get(key);
|
|
184
|
+
if (group) group.lines.push(entry.line);
|
|
185
|
+
else groups.set(key, { ...entry, lines: [entry.line] });
|
|
186
|
+
}
|
|
187
|
+
for (const group of groups.values()) {
|
|
188
|
+
try {
|
|
189
|
+
ensureParent(group.path);
|
|
190
|
+
withTelegramFileTransaction(`${group.path}.transaction`, () => {
|
|
191
|
+
let currentSize = existsSync(group.path)
|
|
192
|
+
? statSync(group.path).size
|
|
193
|
+
: 0;
|
|
194
|
+
let chunk = "";
|
|
195
|
+
let chunkBytes = 0;
|
|
196
|
+
const flushChunk = () => {
|
|
197
|
+
if (!chunk) return;
|
|
198
|
+
appendFileSync(group.path, chunk, { mode: 0o600 });
|
|
199
|
+
currentSize += chunkBytes;
|
|
200
|
+
chunk = "";
|
|
201
|
+
chunkBytes = 0;
|
|
202
|
+
};
|
|
203
|
+
const rotate = (): boolean => {
|
|
204
|
+
if (options.canReset && !options.canReset()) return false;
|
|
205
|
+
let rotated = false;
|
|
206
|
+
const commit = () => {
|
|
207
|
+
writeResetLocked(
|
|
208
|
+
group.path,
|
|
209
|
+
group.previousPath,
|
|
210
|
+
"max-bytes",
|
|
211
|
+
{ maxBytes },
|
|
212
|
+
);
|
|
213
|
+
rotated = true;
|
|
214
|
+
};
|
|
215
|
+
if (options.commitReset) options.commitReset(commit);
|
|
216
|
+
else commit();
|
|
217
|
+
if (rotated) currentSize = statSync(group.path).size;
|
|
218
|
+
return rotated;
|
|
219
|
+
};
|
|
220
|
+
for (const line of group.lines) {
|
|
221
|
+
const lineBytes = Buffer.byteLength(line);
|
|
222
|
+
if (
|
|
223
|
+
currentSize + chunkBytes > 0 &&
|
|
224
|
+
currentSize + chunkBytes + lineBytes > maxBytes
|
|
225
|
+
) {
|
|
226
|
+
flushChunk();
|
|
227
|
+
rotate();
|
|
228
|
+
}
|
|
229
|
+
chunk += line;
|
|
230
|
+
chunkBytes += lineBytes;
|
|
231
|
+
}
|
|
232
|
+
flushChunk();
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// Diagnostics failures for one profile must not drop other groups.
|
|
177
236
|
}
|
|
178
|
-
|
|
179
|
-
});
|
|
237
|
+
}
|
|
180
238
|
})
|
|
181
239
|
.catch(() => undefined);
|
|
182
240
|
};
|
package/lib/media.ts
CHANGED
|
@@ -390,15 +390,6 @@ export function appendTelegramReplyContext(
|
|
|
390
390
|
return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
export function appendTelegramForwardContext(
|
|
394
|
-
text: string,
|
|
395
|
-
forwardContext: string,
|
|
396
|
-
): string {
|
|
397
|
-
if (!forwardContext) return text;
|
|
398
|
-
const forwardBlock = `[forward|${forwardContext.replace(/:\s+/g, ":")}]`;
|
|
399
|
-
return text ? `\n\n${forwardBlock} ${text}` : `\n\n${forwardBlock}`;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
393
|
export function extractTelegramMessagePromptText(
|
|
403
394
|
message: TelegramMediaMessage,
|
|
404
395
|
): string {
|
package/lib/menu-model.ts
CHANGED
|
@@ -484,7 +484,11 @@ export function createTelegramModelMenuStateBuilder<
|
|
|
484
484
|
TelegramModelMenuStateBuilderContext<TModel>,
|
|
485
485
|
>(
|
|
486
486
|
deps: TelegramModelMenuStateBuilderDeps<TModel, TContext>,
|
|
487
|
-
): (
|
|
487
|
+
): (
|
|
488
|
+
chatId: number,
|
|
489
|
+
ctx: TContext,
|
|
490
|
+
threadId?: number,
|
|
491
|
+
) => Promise<TelegramModelMenuState<TModel>> {
|
|
488
492
|
return async (chatId, ctx, threadId) => {
|
|
489
493
|
const settingsManager = deps.createSettingsManager(ctx.cwd);
|
|
490
494
|
return deps.runtime.buildState({
|
|
@@ -745,17 +749,6 @@ export function setTelegramModelScope(
|
|
|
745
749
|
return { patterns, enabled };
|
|
746
750
|
}
|
|
747
751
|
|
|
748
|
-
export function toggleTelegramModelScope(
|
|
749
|
-
state: TelegramModelMenuState,
|
|
750
|
-
model: MenuModel,
|
|
751
|
-
): { patterns: string[]; enabled: boolean } {
|
|
752
|
-
return setTelegramModelScope(
|
|
753
|
-
state,
|
|
754
|
-
model,
|
|
755
|
-
!isTelegramModelScoped(state, model),
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
752
|
export function buildTelegramModelCallbackPlan<
|
|
760
753
|
TModel extends MenuModel = MenuModel,
|
|
761
754
|
>(
|
package/lib/menu.ts
CHANGED
|
@@ -118,25 +118,6 @@ export type {
|
|
|
118
118
|
TelegramThinkingMenuOpenDeps,
|
|
119
119
|
} from "./menu-thinking.ts";
|
|
120
120
|
|
|
121
|
-
export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
|
|
122
|
-
answerCallbackQuery: (
|
|
123
|
-
callbackQueryId: string,
|
|
124
|
-
text?: string,
|
|
125
|
-
) => Promise<void>;
|
|
126
|
-
updateModelMenuMessage: () => Promise<void>;
|
|
127
|
-
updateThinkingMenuMessage: () => Promise<void>;
|
|
128
|
-
updateStatusMessage: () => Promise<void>;
|
|
129
|
-
persistScopedModelPatterns?: (patterns: string[]) => Promise<void>;
|
|
130
|
-
setModel: (model: TModel) => Promise<boolean>;
|
|
131
|
-
setCurrentModel: (model: TModel) => void;
|
|
132
|
-
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
133
|
-
getCurrentThinkingLevel: () => ThinkingLevel;
|
|
134
|
-
stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
|
|
135
|
-
restartInterruptedTelegramTurn: (
|
|
136
|
-
selection: ScopedTelegramModel<TModel>,
|
|
137
|
-
) => Promise<boolean> | boolean;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
121
|
export interface TelegramMenuCallbackEntryDeps {
|
|
141
122
|
handleStatusAction: () => Promise<boolean>;
|
|
142
123
|
handleThinkingAction: () => Promise<boolean>;
|
package/lib/outbound.ts
CHANGED
|
@@ -197,12 +197,6 @@ export function registerTelegramOutboundHandler(
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export function hasTelegramOutboundHandler(kind: string): boolean {
|
|
201
|
-
const registry = getOrCreateOutboundHandlerRegistry();
|
|
202
|
-
const list = registry.handlers.get(kind);
|
|
203
|
-
return !!list && list.length > 0;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
200
|
export function getTelegramOutboundProgrammaticHandlers(
|
|
207
201
|
kind: string,
|
|
208
202
|
): TelegramOutboundProgrammaticHandler[] {
|
|
@@ -953,15 +947,11 @@ export function createTelegramAssistantOutputSender<
|
|
|
953
947
|
TReplyMarkup = unknown,
|
|
954
948
|
>(deps: {
|
|
955
949
|
recordOwnership?: Replies.TelegramReplyOwnershipRecorder["record"];
|
|
956
|
-
sendMessage: (
|
|
957
|
-
body: TelegramSendMessageBody,
|
|
958
|
-
) => Promise<TelegramSentMessage>;
|
|
950
|
+
sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
|
|
959
951
|
sendRichMessage: (
|
|
960
952
|
body: TelegramSendRichMessageBody,
|
|
961
953
|
) => Promise<TelegramSentMessage>;
|
|
962
|
-
editMessage: (
|
|
963
|
-
body: TelegramEditMessageTextBody,
|
|
964
|
-
) => Promise<unknown>;
|
|
954
|
+
editMessage: (body: TelegramEditMessageTextBody) => Promise<unknown>;
|
|
965
955
|
getAssistantRenderingMode: () => "rich" | "html";
|
|
966
956
|
execCommand: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["execCommand"];
|
|
967
957
|
getHandlers?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["getHandlers"];
|
package/lib/pi.ts
CHANGED
|
@@ -114,10 +114,6 @@ export function getSessionCompactionReason(
|
|
|
114
114
|
: "unknown";
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export type PiSendUserMessageOptions = NonNullable<
|
|
118
|
-
Parameters<ExtensionAPI["sendUserMessage"]>[1]
|
|
119
|
-
>;
|
|
120
|
-
|
|
121
117
|
export interface PiExtensionApiRuntimePorts {
|
|
122
118
|
sendUserMessage: ExtensionAPI["sendUserMessage"];
|
|
123
119
|
exec: ExtensionAPI["exec"];
|
package/lib/queue.ts
CHANGED
|
@@ -1764,7 +1764,6 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
|
1764
1764
|
deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
|
|
1765
1765
|
new Promise<void>((resolve) => {
|
|
1766
1766
|
timeout = setTimeout(resolve, previewTimeoutMs);
|
|
1767
|
-
timeout.unref?.();
|
|
1768
1767
|
}),
|
|
1769
1768
|
]).finally(() => {
|
|
1770
1769
|
if (timeout) clearTimeout(timeout);
|
|
@@ -2240,10 +2239,6 @@ export interface TelegramPromptDeliveryOptions {
|
|
|
2240
2239
|
deliverAs: "followUp";
|
|
2241
2240
|
}
|
|
2242
2241
|
|
|
2243
|
-
export const TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY = {
|
|
2244
|
-
deliverAs: "followUp",
|
|
2245
|
-
} as const satisfies TelegramPromptDeliveryOptions;
|
|
2246
|
-
|
|
2247
2242
|
export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
|
|
2248
2243
|
executeControlItem: (
|
|
2249
2244
|
item: Extract<
|
package/lib/replies.ts
CHANGED
|
@@ -871,29 +871,6 @@ export function dedupSendTextReply(
|
|
|
871
871
|
};
|
|
872
872
|
}
|
|
873
873
|
|
|
874
|
-
/** Wrap a sendMarkdownReply with reply dedup. */
|
|
875
|
-
export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
|
|
876
|
-
dedup: ReplyDedupRuntime,
|
|
877
|
-
inner: (
|
|
878
|
-
chatId: number,
|
|
879
|
-
replyToMessageId: number | undefined,
|
|
880
|
-
markdown: string,
|
|
881
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
882
|
-
) => Promise<number | undefined>,
|
|
883
|
-
): (
|
|
884
|
-
chatId: number,
|
|
885
|
-
replyToMessageId: number,
|
|
886
|
-
markdown: string,
|
|
887
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
888
|
-
) => Promise<number | undefined> {
|
|
889
|
-
return async (chatId, replyToMessageId, markdown, options) => {
|
|
890
|
-
const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
|
|
891
|
-
? replyToMessageId
|
|
892
|
-
: undefined;
|
|
893
|
-
return inner(chatId, effectiveReplyTo, markdown, options);
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
874
|
/**
|
|
898
875
|
* Guest reply sender: answers guest queries with native Rich Markdown content.
|
|
899
876
|
* Guest queries use InlineQueryResult input_message_content rather than chat
|
package/lib/runtime.ts
CHANGED
|
@@ -508,8 +508,7 @@ export async function waitForTelegramTypingLoopIdle(
|
|
|
508
508
|
await Promise.race([
|
|
509
509
|
inFlight,
|
|
510
510
|
new Promise<void>((resolve) => {
|
|
511
|
-
|
|
512
|
-
timer.unref?.();
|
|
511
|
+
setTimeout(resolve, timeoutMs);
|
|
513
512
|
}),
|
|
514
513
|
]);
|
|
515
514
|
}
|
package/lib/sections.ts
CHANGED
|
@@ -358,7 +358,7 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
|
|
|
358
358
|
>();
|
|
359
359
|
let nextToken = 0;
|
|
360
360
|
|
|
361
|
-
const register = (section: TelegramSectionRegistration): () => void => {
|
|
361
|
+
const register = (section: TelegramSectionRegistration): (() => void) => {
|
|
362
362
|
const duplicate = [...sections.values()].find((s) => s.id === section.id);
|
|
363
363
|
if (duplicate) {
|
|
364
364
|
throw new Error(`Telegram section id already registered: ${section.id}`);
|
|
@@ -708,50 +708,3 @@ export async function handleTelegramSectionSettingsOpen(
|
|
|
708
708
|
return true;
|
|
709
709
|
}
|
|
710
710
|
}
|
|
711
|
-
|
|
712
|
-
export async function handleTelegramSectionSettingsCallback(
|
|
713
|
-
registry: TelegramSectionRegistry,
|
|
714
|
-
token: TelegramSectionToken,
|
|
715
|
-
action: string,
|
|
716
|
-
payload: string,
|
|
717
|
-
chatId: number,
|
|
718
|
-
messageId: number,
|
|
719
|
-
callbackQueryId: string,
|
|
720
|
-
deps: TelegramSectionCallbackHandlerDeps,
|
|
721
|
-
): Promise<boolean> {
|
|
722
|
-
const section = registry.getByToken(token);
|
|
723
|
-
if (!section || !section.registration.settings?.handleCallback) {
|
|
724
|
-
await deps.answerCallbackQuery(
|
|
725
|
-
callbackQueryId,
|
|
726
|
-
"This section is no longer available.",
|
|
727
|
-
);
|
|
728
|
-
return true;
|
|
729
|
-
}
|
|
730
|
-
try {
|
|
731
|
-
const ctx = buildTelegramSectionCallbackContext(
|
|
732
|
-
section.id,
|
|
733
|
-
token,
|
|
734
|
-
chatId,
|
|
735
|
-
messageId,
|
|
736
|
-
action,
|
|
737
|
-
payload,
|
|
738
|
-
callbackQueryId,
|
|
739
|
-
deps,
|
|
740
|
-
`settings:list`,
|
|
741
|
-
);
|
|
742
|
-
const result = await section.registration.settings.handleCallback(ctx);
|
|
743
|
-
if (result === "pass") {
|
|
744
|
-
await deps.answerCallbackQuery(callbackQueryId);
|
|
745
|
-
}
|
|
746
|
-
registry.clearError(token, "settings_callback");
|
|
747
|
-
return true;
|
|
748
|
-
} catch (error) {
|
|
749
|
-
const message = sectionErrorMessage(error);
|
|
750
|
-
registry.recordError(token, message, "settings_callback");
|
|
751
|
-
await deps.answerCallbackQuery(
|
|
752
|
-
callbackQueryId,
|
|
753
|
-
`Section error: ${message}`,
|
|
754
|
-
);
|
|
755
|
-
return true;
|
|
756
|
-
}
|
|
757
|
-
}
|
package/lib/status.ts
CHANGED
|
@@ -262,9 +262,10 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
262
262
|
statusKey?: string;
|
|
263
263
|
getConfig: () => TelegramBridgeStatusConfig;
|
|
264
264
|
getActiveProfileName?: () => string | undefined;
|
|
265
|
-
getDiagnosticPaths?: (
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
getDiagnosticPaths?: (profileName?: string) => {
|
|
266
|
+
state: string;
|
|
267
|
+
logs: string;
|
|
268
|
+
};
|
|
268
269
|
isPollingActive: () => boolean;
|
|
269
270
|
getActiveSourceMessageIds: () => number[] | undefined;
|
|
270
271
|
hasActiveTurn: () => boolean;
|
|
@@ -295,8 +296,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
295
296
|
TelegramBridgeStatusSyncSlice | undefined
|
|
296
297
|
>;
|
|
297
298
|
getThreadReconciliationState?: () =>
|
|
298
|
-
|
|
|
299
|
-
| undefined;
|
|
299
|
+
TelegramBridgeThreadReconciliationState | undefined;
|
|
300
300
|
getInstanceSlot?: () => string | undefined;
|
|
301
301
|
getInstanceThreadName?: () => string | undefined;
|
|
302
302
|
getNowMs?: () => number;
|
|
@@ -726,6 +726,8 @@ export function createTelegramStatusSnapshot(
|
|
|
726
726
|
};
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
+
const TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS = 100;
|
|
730
|
+
|
|
729
731
|
export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
|
|
730
732
|
persistSnapshot: () => Promise<void>;
|
|
731
733
|
recordError: (error: unknown) => void;
|
|
@@ -738,7 +740,7 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
|
|
|
738
740
|
timer = setTimer(() => {
|
|
739
741
|
timer = undefined;
|
|
740
742
|
void deps.persistSnapshot().catch(deps.recordError);
|
|
741
|
-
},
|
|
743
|
+
}, TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS);
|
|
742
744
|
if (typeof timer !== "number") timer?.unref?.();
|
|
743
745
|
};
|
|
744
746
|
}
|
|
@@ -1084,7 +1086,9 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1084
1086
|
return [
|
|
1085
1087
|
"connection:",
|
|
1086
1088
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
1087
|
-
...(state.activeProfileName
|
|
1089
|
+
...(state.activeProfileName
|
|
1090
|
+
? [`- profile: ${state.activeProfileName}`]
|
|
1091
|
+
: []),
|
|
1088
1092
|
`- user: ${state.allowedUserId ?? "not paired"}`,
|
|
1089
1093
|
...(state.botThreadMode ? [`- thread mode: ${state.botThreadMode}`] : []),
|
|
1090
1094
|
...(state.busRole ? [`- role: ${state.busRole}`] : []),
|
|
@@ -1139,7 +1143,9 @@ export function buildTelegramBridgeDiagnosticStatusLines(
|
|
|
1139
1143
|
return [
|
|
1140
1144
|
"connection:",
|
|
1141
1145
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
1142
|
-
...(state.activeProfileName
|
|
1146
|
+
...(state.activeProfileName
|
|
1147
|
+
? [`- profile: ${state.activeProfileName}`]
|
|
1148
|
+
: []),
|
|
1143
1149
|
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
|
|
1144
1150
|
...(state.botThreadMode
|
|
1145
1151
|
? [
|
package/lib/telegram-api.ts
CHANGED
|
@@ -297,10 +297,6 @@ export type TelegramSendRichMessageBody = Record<string, unknown> & {
|
|
|
297
297
|
reply_parameters?: TelegramReplyParameters;
|
|
298
298
|
};
|
|
299
299
|
|
|
300
|
-
export type TelegramInputRichMessageContent = {
|
|
301
|
-
rich_message: TelegramInputRichMessage;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
300
|
export type TelegramEditMessageTextBody = Record<string, unknown> & {
|
|
305
301
|
chat_id: number;
|
|
306
302
|
message_id: number;
|