@llblab/pi-telegram 0.54.3 → 0.55.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 +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +5 -1
- package/dist/lib/bindings.js +9 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +49 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +5 -3
- package/dist/lib/bus.js +13 -10
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +56 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +14 -2
- package/dist/lib/routing.js +546 -207
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +20 -5
- package/dist/lib/threads.js +111 -32
- package/dist/lib/turns.d.ts +2 -0
- package/dist/lib/turns.js +6 -1
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +16 -15
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +18 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +8 -6
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +13 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +17 -15
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +59 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +644 -283
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +130 -38
- package/lib/turns.ts +8 -1
- package/lib/updates.ts +58 -21
- package/package.json +1 -1
package/lib/text-groups.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import { setTimeout as waitForTimeout } from "node:timers/promises";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
createTelegramPendingGroups,
|
|
11
11
|
extractTelegramMessageText,
|
|
12
|
+
type TelegramPendingGroup,
|
|
12
13
|
type TelegramMessageForwardOrigin,
|
|
13
14
|
type TelegramMessageUser,
|
|
14
15
|
type TelegramRichMessage,
|
|
@@ -32,19 +33,6 @@ export interface TelegramTextGroupMessage {
|
|
|
32
33
|
forward_sender_name?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export interface TelegramTextGroupState<TMessage, TContext = unknown> {
|
|
36
|
-
messages: TMessage[];
|
|
37
|
-
context?: TContext;
|
|
38
|
-
flushTimer?: ReturnType<typeof setTimeout>;
|
|
39
|
-
dispatching?: boolean;
|
|
40
|
-
suspended?: boolean;
|
|
41
|
-
reschedule?: (delayMs?: number) => void;
|
|
42
|
-
dispatchNow?: () => Promise<void>;
|
|
43
|
-
dispatchPromise?: Promise<void>;
|
|
44
|
-
dispatchLimit?: number;
|
|
45
|
-
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
36
|
export type TelegramForwardCommentBatchPosition = "comment" | "forward";
|
|
49
37
|
|
|
50
38
|
export interface TelegramTextGroupController<TMessage, TContext = unknown> {
|
|
@@ -157,11 +145,11 @@ function canStartTelegramTextGroup(
|
|
|
157
145
|
function canAppendTelegramTextGroupMessage<
|
|
158
146
|
TMessage extends TelegramTextGroupMessage,
|
|
159
147
|
>(
|
|
160
|
-
|
|
148
|
+
group: TelegramPendingGroup<TMessage, unknown, unknown>,
|
|
161
149
|
message: TMessage,
|
|
162
150
|
): boolean {
|
|
163
151
|
const text = extractTelegramTextGroupText(message);
|
|
164
|
-
const previous =
|
|
152
|
+
const previous = group.messages.at(-1);
|
|
165
153
|
return (
|
|
166
154
|
!!previous &&
|
|
167
155
|
message.message_id > previous.message_id &&
|
|
@@ -172,114 +160,12 @@ function canAppendTelegramTextGroupMessage<
|
|
|
172
160
|
);
|
|
173
161
|
}
|
|
174
162
|
|
|
175
|
-
export function queueTelegramTextGroupMessage<
|
|
176
|
-
TMessage extends TelegramTextGroupMessage,
|
|
177
|
-
TContext = unknown,
|
|
178
|
-
>(options: {
|
|
179
|
-
message: TMessage;
|
|
180
|
-
context: TContext;
|
|
181
|
-
groups: Map<string, TelegramTextGroupState<TMessage, TContext>>;
|
|
182
|
-
debounceMs: number;
|
|
183
|
-
minSplitLength: number;
|
|
184
|
-
setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
185
|
-
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
|
186
|
-
dispatchMessages: (
|
|
187
|
-
messages: TMessage[],
|
|
188
|
-
ctx: TContext,
|
|
189
|
-
) => unknown | Promise<unknown>;
|
|
190
|
-
forceStart?: boolean;
|
|
191
|
-
dispatchImmediately?: boolean;
|
|
192
|
-
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
193
|
-
delayMs?: number;
|
|
194
|
-
}): boolean {
|
|
195
|
-
const key = getTelegramTextGroupKey(options.message);
|
|
196
|
-
if (!key) return false;
|
|
197
|
-
const existing = options.groups.get(key);
|
|
198
|
-
const duplicateIndex = existing?.messages.findIndex(
|
|
199
|
-
(message) => message.message_id === options.message.message_id,
|
|
200
|
-
);
|
|
201
|
-
if (existing && duplicateIndex !== undefined && duplicateIndex >= 0) {
|
|
202
|
-
existing.messages[duplicateIndex] = options.message;
|
|
203
|
-
existing.context = options.context;
|
|
204
|
-
existing.forwardPairCandidate = options.forwardPairCandidate;
|
|
205
|
-
if (!existing.suspended && !existing.dispatching && !existing.flushTimer) {
|
|
206
|
-
existing.reschedule?.();
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
if (
|
|
211
|
-
!existing &&
|
|
212
|
-
!options.forceStart &&
|
|
213
|
-
!canStartTelegramTextGroup(options.message, options.minSplitLength)
|
|
214
|
-
)
|
|
215
|
-
return false;
|
|
216
|
-
if (existing && !canAppendTelegramTextGroupMessage(existing, options.message))
|
|
217
|
-
return false;
|
|
218
|
-
const state = existing ?? { messages: [] };
|
|
219
|
-
state.messages.push(options.message);
|
|
220
|
-
state.context = options.context;
|
|
221
|
-
state.forwardPairCandidate = options.forwardPairCandidate;
|
|
222
|
-
const dispatchQueued = (): Promise<void> => {
|
|
223
|
-
state.flushTimer = undefined;
|
|
224
|
-
const queued = options.groups.get(key);
|
|
225
|
-
if (!queued || queued.context === undefined) return Promise.resolve();
|
|
226
|
-
if (queued.dispatching) return queued.dispatchPromise ?? Promise.resolve();
|
|
227
|
-
const dispatchCount = queued.dispatchLimit ?? queued.messages.length;
|
|
228
|
-
queued.dispatchLimit = undefined;
|
|
229
|
-
const dispatchedMessages = queued.messages.slice(0, dispatchCount);
|
|
230
|
-
const dispatchedIds = new Set(
|
|
231
|
-
dispatchedMessages.map((message) => message.message_id),
|
|
232
|
-
);
|
|
233
|
-
queued.dispatching = true;
|
|
234
|
-
const operation = Promise.resolve(
|
|
235
|
-
options.dispatchMessages(dispatchedMessages, queued.context),
|
|
236
|
-
).then(
|
|
237
|
-
() => {
|
|
238
|
-
if (options.groups.get(key) !== queued) return;
|
|
239
|
-
queued.messages = queued.messages.filter(
|
|
240
|
-
(message) => !dispatchedIds.has(message.message_id),
|
|
241
|
-
);
|
|
242
|
-
queued.dispatching = false;
|
|
243
|
-
queued.dispatchPromise = undefined;
|
|
244
|
-
if (queued.messages.length === 0) options.groups.delete(key);
|
|
245
|
-
else if (!queued.flushTimer) scheduleDispatch();
|
|
246
|
-
},
|
|
247
|
-
(error) => {
|
|
248
|
-
if (options.groups.get(key) === queued) {
|
|
249
|
-
queued.dispatching = false;
|
|
250
|
-
queued.dispatchPromise = undefined;
|
|
251
|
-
if (!queued.flushTimer) scheduleDispatch();
|
|
252
|
-
}
|
|
253
|
-
throw error;
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
queued.dispatchPromise = operation;
|
|
257
|
-
return operation;
|
|
258
|
-
};
|
|
259
|
-
const scheduleDispatch = (delayMs = options.debounceMs): void => {
|
|
260
|
-
if (state.suspended) return;
|
|
261
|
-
state.flushTimer = options.setTimer(() => {
|
|
262
|
-
void dispatchQueued().catch(() => undefined);
|
|
263
|
-
}, delayMs);
|
|
264
|
-
state.flushTimer.unref?.();
|
|
265
|
-
};
|
|
266
|
-
state.reschedule = scheduleDispatch;
|
|
267
|
-
state.dispatchNow = dispatchQueued;
|
|
268
|
-
if (state.flushTimer) options.clearTimer(state.flushTimer);
|
|
269
|
-
scheduleDispatch(
|
|
270
|
-
options.dispatchImmediately ? 0 : (options.delayMs ?? options.debounceMs),
|
|
271
|
-
);
|
|
272
|
-
options.groups.set(key, state);
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
163
|
export function createTelegramTextGroupController<
|
|
277
164
|
TMessage extends TelegramTextGroupMessage,
|
|
278
165
|
TContext = unknown,
|
|
279
166
|
>(
|
|
280
167
|
options: TelegramTextGroupControllerOptions = {},
|
|
281
168
|
): TelegramTextGroupController<TMessage, TContext> {
|
|
282
|
-
const groups = new Map<string, TelegramTextGroupState<TMessage, TContext>>();
|
|
283
169
|
const plannedForwardCommentStarts = new Set<string>();
|
|
284
170
|
const plannedForwardCommentEnds = new Set<string>();
|
|
285
171
|
const debounceMs = options.debounceMs ?? TELEGRAM_TEXT_GROUP_DEBOUNCE_MS;
|
|
@@ -305,7 +191,65 @@ export function createTelegramTextGroupController<
|
|
|
305
191
|
: (timer: ReturnType<typeof setTimeout>): void => {
|
|
306
192
|
(timer as unknown as AbortController).abort();
|
|
307
193
|
});
|
|
308
|
-
const
|
|
194
|
+
const groups = createTelegramPendingGroups<
|
|
195
|
+
TMessage,
|
|
196
|
+
TContext,
|
|
197
|
+
TelegramForwardCommentBatchPosition
|
|
198
|
+
>({ debounceMs, requireContext: true, setTimer, clearTimer });
|
|
199
|
+
/** Admission policy for one message; the shared group machine owns timers, batches and dispatch. */
|
|
200
|
+
const queueTextGroupMessage = (options: {
|
|
201
|
+
key: string;
|
|
202
|
+
message: TMessage;
|
|
203
|
+
context: TContext;
|
|
204
|
+
dispatchMessages: (
|
|
205
|
+
messages: TMessage[],
|
|
206
|
+
ctx: TContext,
|
|
207
|
+
) => unknown | Promise<unknown>;
|
|
208
|
+
forceStart: boolean;
|
|
209
|
+
dispatchImmediately: boolean;
|
|
210
|
+
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
211
|
+
delayMs?: number;
|
|
212
|
+
}): boolean => {
|
|
213
|
+
const existing = groups.get(options.key);
|
|
214
|
+
const dispatchMessages = (messages: TMessage[], ctx?: TContext) =>
|
|
215
|
+
options.dispatchMessages(messages, ctx as TContext);
|
|
216
|
+
const queued = {
|
|
217
|
+
key: options.key,
|
|
218
|
+
message: options.message,
|
|
219
|
+
context: options.context,
|
|
220
|
+
...(options.forwardPairCandidate === undefined
|
|
221
|
+
? {}
|
|
222
|
+
: { meta: options.forwardPairCandidate }),
|
|
223
|
+
dispatchMessages,
|
|
224
|
+
};
|
|
225
|
+
if (
|
|
226
|
+
existing?.messages.some(
|
|
227
|
+
(message) => message.message_id === options.message.message_id,
|
|
228
|
+
)
|
|
229
|
+
) {
|
|
230
|
+
groups.queue({ ...queued, delayMs: debounceMs, rearm: "ifIdle" });
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
!existing &&
|
|
235
|
+
!options.forceStart &&
|
|
236
|
+
!canStartTelegramTextGroup(options.message, minSplitLength)
|
|
237
|
+
)
|
|
238
|
+
return false;
|
|
239
|
+
if (
|
|
240
|
+
existing &&
|
|
241
|
+
!canAppendTelegramTextGroupMessage(existing, options.message)
|
|
242
|
+
)
|
|
243
|
+
return false;
|
|
244
|
+
groups.queue({
|
|
245
|
+
...queued,
|
|
246
|
+
delayMs: options.dispatchImmediately
|
|
247
|
+
? 0
|
|
248
|
+
: (options.delayMs ?? debounceMs),
|
|
249
|
+
rearm: "always",
|
|
250
|
+
});
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
309
253
|
return {
|
|
310
254
|
prepareUpdateBatch(updates) {
|
|
311
255
|
for (let index = 0; index + 1 < updates.length; index += 1) {
|
|
@@ -354,23 +298,19 @@ export function createTelegramTextGroupController<
|
|
|
354
298
|
const forwarded = isTelegramForwardedMessage(message);
|
|
355
299
|
const existing = key ? groups.get(key) : undefined;
|
|
356
300
|
const text = extractTelegramTextGroupText(message);
|
|
357
|
-
if (existing && isTelegramTextGroupClearingCommand(text))
|
|
358
|
-
|
|
359
|
-
groups.delete(key!);
|
|
360
|
-
}
|
|
301
|
+
if (existing && isTelegramTextGroupClearingCommand(text))
|
|
302
|
+
groups.clearKey(key!);
|
|
361
303
|
const candidatePosition: TelegramForwardCommentBatchPosition = forwarded
|
|
362
304
|
? "forward"
|
|
363
305
|
: "comment";
|
|
364
|
-
const existingCandidate = existing?.
|
|
306
|
+
const existingCandidate = existing?.meta;
|
|
365
307
|
const pairCompleted =
|
|
366
308
|
existingCandidate !== undefined &&
|
|
367
309
|
existingCandidate !== candidatePosition;
|
|
368
310
|
const separateFromCandidate =
|
|
369
311
|
existingCandidate === candidatePosition &&
|
|
370
312
|
!isTelegramTextGroupCommand(text);
|
|
371
|
-
if (separateFromCandidate && existing)
|
|
372
|
-
existing.dispatchLimit = existing.messages.length;
|
|
373
|
-
}
|
|
313
|
+
if (separateFromCandidate && existing) groups.closeBatch(key!);
|
|
374
314
|
const forceStart =
|
|
375
315
|
plannedStart ||
|
|
376
316
|
(forwardCommentWaitMs !== false &&
|
|
@@ -384,15 +324,12 @@ export function createTelegramTextGroupController<
|
|
|
384
324
|
pairCompleted ||
|
|
385
325
|
separateFromCandidate ||
|
|
386
326
|
plannedForwardCommentEnds.delete(identity) ||
|
|
387
|
-
(forwarded && !!key && groups.
|
|
388
|
-
return
|
|
327
|
+
(forwarded && !!key && groups.get(key) !== undefined);
|
|
328
|
+
if (!key) return false;
|
|
329
|
+
return queueTextGroupMessage({
|
|
330
|
+
key,
|
|
389
331
|
message,
|
|
390
332
|
context,
|
|
391
|
-
groups,
|
|
392
|
-
debounceMs,
|
|
393
|
-
minSplitLength,
|
|
394
|
-
setTimer,
|
|
395
|
-
clearTimer,
|
|
396
333
|
dispatchMessages,
|
|
397
334
|
forceStart,
|
|
398
335
|
dispatchImmediately,
|
|
@@ -408,9 +345,11 @@ export function createTelegramTextGroupController<
|
|
|
408
345
|
: undefined,
|
|
409
346
|
});
|
|
410
347
|
},
|
|
411
|
-
|
|
348
|
+
flushMessage: groups.flushMessage,
|
|
349
|
+
suspend: groups.suspend,
|
|
350
|
+
resume: groups.resume,
|
|
412
351
|
clear: () => {
|
|
413
|
-
|
|
352
|
+
groups.clear();
|
|
414
353
|
plannedForwardCommentStarts.clear();
|
|
415
354
|
plannedForwardCommentEnds.clear();
|
|
416
355
|
},
|
package/lib/thread-display.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Owns mode-specific labels and serialized, authority-fenced title application.
|
|
5
5
|
* Excludes routing, name allocation, profile mutation, and live-owner discovery.
|
|
6
6
|
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { isDeepStrictEqual } from "node:util";
|
|
8
9
|
|
|
9
10
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
11
|
+
import { normalizeTelegramWorkspacePath } from "./workspace-identity.ts";
|
|
10
12
|
import type { TelegramApiCallOptions } from "./telegram-api.ts";
|
|
11
13
|
import type {
|
|
12
14
|
TelegramTopicTargetStore,
|
|
@@ -34,8 +36,11 @@ function directorySegments(cwd: string): string[] {
|
|
|
34
36
|
function distinguishingDirectorySegments(
|
|
35
37
|
cwd: string,
|
|
36
38
|
directories: readonly string[],
|
|
39
|
+
homeDirectory?: string,
|
|
37
40
|
): string[] {
|
|
38
|
-
|
|
41
|
+
// Substitute only exact home identity; qualification must still distinguish ordinary folders named home.
|
|
42
|
+
const segments = (directory: string) => directory === homeDirectory ? ["home"] : directorySegments(directory);
|
|
43
|
+
const parts = segments(cwd);
|
|
39
44
|
if (!parts.length) return [];
|
|
40
45
|
for (let depth = 1; depth <= parts.length; depth++) {
|
|
41
46
|
const candidate = labelText(parts.slice(-depth).join("/"));
|
|
@@ -43,7 +48,7 @@ function distinguishingDirectorySegments(
|
|
|
43
48
|
(other) =>
|
|
44
49
|
other !== cwd &&
|
|
45
50
|
labelText(
|
|
46
|
-
|
|
51
|
+
segments(other).slice(-depth).join("/"),
|
|
47
52
|
).toLowerCase() === candidate.toLowerCase(),
|
|
48
53
|
);
|
|
49
54
|
if (!collides) return parts.slice(-depth);
|
|
@@ -69,8 +74,9 @@ function formatDirectoryLabel(
|
|
|
69
74
|
cwd: string,
|
|
70
75
|
directories: readonly string[],
|
|
71
76
|
mode: "directory-snake" | "directory-title",
|
|
77
|
+
homeDirectory?: string,
|
|
72
78
|
): string {
|
|
73
|
-
const segments = distinguishingDirectorySegments(cwd, directories);
|
|
79
|
+
const segments = distinguishingDirectorySegments(cwd, directories, homeDirectory);
|
|
74
80
|
if (!segments.length) return "/";
|
|
75
81
|
const formatted = segments
|
|
76
82
|
.map((segment) => {
|
|
@@ -143,6 +149,11 @@ export function resolveTelegramWorkspaceDisplayNames(
|
|
|
143
149
|
liveBindingKeys: ReadonlySet<string> = new Set(),
|
|
144
150
|
): Map<string, string> {
|
|
145
151
|
const labels = new Map<string, string>();
|
|
152
|
+
const homeDirectory =
|
|
153
|
+
(mode === "directory-snake" || mode === "directory-title") &&
|
|
154
|
+
(process.platform === "linux" || process.platform === "darwin")
|
|
155
|
+
? normalizeTelegramWorkspacePath(homedir())
|
|
156
|
+
: undefined;
|
|
146
157
|
const directories = Array.from(
|
|
147
158
|
new Set(bindings.map((binding) => binding.cwd)),
|
|
148
159
|
);
|
|
@@ -172,7 +183,7 @@ export function resolveTelegramWorkspaceDisplayNames(
|
|
|
172
183
|
const base =
|
|
173
184
|
mode === "directories"
|
|
174
185
|
? directoryLabel(binding.cwd, directories)
|
|
175
|
-
: formatDirectoryLabel(binding.cwd, directories, mode);
|
|
186
|
+
: formatDirectoryLabel(binding.cwd, directories, mode, homeDirectory);
|
|
176
187
|
bases.set(binding.bindingKey, base);
|
|
177
188
|
const showSuffix =
|
|
178
189
|
mode === "directories"
|