@llblab/pi-telegram 0.20.0 → 0.20.2
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 -1
- package/BACKLOG.md +5 -17
- package/CHANGELOG.md +22 -0
- package/README.md +61 -58
- package/docs/README.md +2 -2
- package/docs/architecture.md +5 -3
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +7 -4
- package/docs/outbound.md +5 -5
- package/index.ts +138 -208
- package/lib/bindings.ts +39 -13
- package/lib/bus-follower.ts +138 -7
- package/lib/bus-leader.ts +185 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +83 -23
- package/lib/paths.ts +11 -0
- package/lib/prompts.ts +19 -7
- package/lib/setup.ts +27 -10
- package/lib/status.ts +9 -2
- package/lib/threads.ts +64 -26
- package/lib/turns.ts +0 -2
- package/package.json +1 -1
package/lib/bus.ts
CHANGED
|
@@ -34,6 +34,40 @@ import { resolveAgentDir } from "./paths.ts";
|
|
|
34
34
|
|
|
35
35
|
export type TelegramBusRole = "leader" | "follower";
|
|
36
36
|
|
|
37
|
+
export interface TelegramBusProcessRuntime {
|
|
38
|
+
instanceId: string;
|
|
39
|
+
manualFollowerOwnerId: string;
|
|
40
|
+
getLeaderSocketPath: () => string;
|
|
41
|
+
getFollowerSocketPath: () => string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createTelegramBusProcessRuntime(input: {
|
|
45
|
+
getActiveProfileName: () => string | undefined;
|
|
46
|
+
pid: number;
|
|
47
|
+
parentPid: number;
|
|
48
|
+
createdAtMs: number;
|
|
49
|
+
}): TelegramBusProcessRuntime {
|
|
50
|
+
const instanceId = `${input.pid}:${input.createdAtMs}`;
|
|
51
|
+
const manualFollowerOwnerId = String(input.parentPid || input.pid);
|
|
52
|
+
return {
|
|
53
|
+
instanceId,
|
|
54
|
+
manualFollowerOwnerId,
|
|
55
|
+
getLeaderSocketPath: () =>
|
|
56
|
+
getTelegramBusSocketPath(
|
|
57
|
+
undefined,
|
|
58
|
+
undefined,
|
|
59
|
+
input.getActiveProfileName(),
|
|
60
|
+
),
|
|
61
|
+
getFollowerSocketPath: () =>
|
|
62
|
+
getTelegramBusFollowerSocketPath(
|
|
63
|
+
instanceId,
|
|
64
|
+
undefined,
|
|
65
|
+
undefined,
|
|
66
|
+
input.getActiveProfileName(),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
export function createTelegramBusAuthSecret(): string {
|
|
38
72
|
return randomBytes(32).toString("base64url");
|
|
39
73
|
}
|
|
@@ -41,16 +75,23 @@ export function createTelegramBusAuthSecret(): string {
|
|
|
41
75
|
export function getTelegramBusSocketPath(
|
|
42
76
|
agentDir = resolveAgentDir(),
|
|
43
77
|
platform = getPlatform(),
|
|
78
|
+
profileName?: string,
|
|
44
79
|
): string {
|
|
45
|
-
return getTelegramBusLeaderEndpoint({ agentDir, platform });
|
|
80
|
+
return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
export function getTelegramBusFollowerSocketPath(
|
|
49
84
|
instanceId: string,
|
|
50
85
|
agentDir = resolveAgentDir(),
|
|
51
86
|
platform = getPlatform(),
|
|
87
|
+
profileName?: string,
|
|
52
88
|
): string {
|
|
53
|
-
return getTelegramBusFollowerEndpoint({
|
|
89
|
+
return getTelegramBusFollowerEndpoint({
|
|
90
|
+
agentDir,
|
|
91
|
+
platform,
|
|
92
|
+
instanceId,
|
|
93
|
+
profileName,
|
|
94
|
+
});
|
|
54
95
|
}
|
|
55
96
|
|
|
56
97
|
export interface TelegramBusInstanceRegistration {
|
|
@@ -344,8 +385,16 @@ export interface TelegramBusLocalServer {
|
|
|
344
385
|
stop: () => Promise<void>;
|
|
345
386
|
}
|
|
346
387
|
|
|
388
|
+
export type TelegramBusSocketPathSource = string | (() => string);
|
|
389
|
+
|
|
390
|
+
export function resolveTelegramBusSocketPath(
|
|
391
|
+
source: TelegramBusSocketPathSource,
|
|
392
|
+
): string {
|
|
393
|
+
return typeof source === "function" ? source() : source;
|
|
394
|
+
}
|
|
395
|
+
|
|
347
396
|
export interface TelegramBusLocalServerDeps {
|
|
348
|
-
socketPath:
|
|
397
|
+
socketPath: TelegramBusSocketPathSource;
|
|
349
398
|
handleEnvelope: (
|
|
350
399
|
envelope: TelegramBusEnvelope,
|
|
351
400
|
) =>
|
|
@@ -362,7 +411,7 @@ export interface TelegramBusLocalClientOptions {
|
|
|
362
411
|
}
|
|
363
412
|
|
|
364
413
|
export interface TelegramBusForeignOwnedForwarderDeps {
|
|
365
|
-
socketPath:
|
|
414
|
+
socketPath: TelegramBusSocketPathSource;
|
|
366
415
|
createRequestId: () => string;
|
|
367
416
|
getNowMs?: () => number;
|
|
368
417
|
timeoutMs?: number;
|
|
@@ -401,12 +450,13 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
401
450
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
402
451
|
const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
|
|
403
452
|
if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
|
|
453
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
404
454
|
const response = await sendTelegramBusLocalEnvelope({
|
|
405
|
-
socketPath
|
|
455
|
+
socketPath,
|
|
406
456
|
envelope,
|
|
407
457
|
timeoutMs: deps.timeoutMs,
|
|
408
458
|
retry: getTelegramBusTransportRetryPolicy({
|
|
409
|
-
endpoint:
|
|
459
|
+
endpoint: socketPath,
|
|
410
460
|
operation: "operation",
|
|
411
461
|
}),
|
|
412
462
|
});
|
|
@@ -558,6 +608,7 @@ export function createTelegramBusLocalServer(
|
|
|
558
608
|
deps: TelegramBusLocalServerDeps,
|
|
559
609
|
): TelegramBusLocalServer {
|
|
560
610
|
let server: Server | undefined;
|
|
611
|
+
let activeSocketPath: string | undefined;
|
|
561
612
|
const sockets = new Set<Socket>();
|
|
562
613
|
const closeSocket = (socket: Socket) => {
|
|
563
614
|
sockets.delete(socket);
|
|
@@ -566,16 +617,18 @@ export function createTelegramBusLocalServer(
|
|
|
566
617
|
return {
|
|
567
618
|
start: async () => {
|
|
568
619
|
if (server) return;
|
|
569
|
-
const
|
|
620
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
621
|
+
activeSocketPath = socketPath;
|
|
622
|
+
const usesWindowsPipe = isTelegramBusPipePath(socketPath);
|
|
570
623
|
deps.recordTransportEvent?.(
|
|
571
624
|
"server-start",
|
|
572
|
-
getTelegramBusEndpointDiagnostics(
|
|
625
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
573
626
|
);
|
|
574
627
|
if (!usesWindowsPipe) {
|
|
575
|
-
const socketDir = dirname(
|
|
628
|
+
const socketDir = dirname(socketPath);
|
|
576
629
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
577
630
|
chmodSync(socketDir, 0o700);
|
|
578
|
-
if (existsSync(
|
|
631
|
+
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
579
632
|
}
|
|
580
633
|
server = createServer((socket) => {
|
|
581
634
|
sockets.add(socket);
|
|
@@ -591,14 +644,14 @@ export function createTelegramBusLocalServer(
|
|
|
591
644
|
socket,
|
|
592
645
|
deps.handleEnvelope,
|
|
593
646
|
deps.recordTransportEvent,
|
|
594
|
-
|
|
647
|
+
socketPath,
|
|
595
648
|
);
|
|
596
649
|
}
|
|
597
650
|
});
|
|
598
651
|
socket.on("close", () => sockets.delete(socket));
|
|
599
652
|
socket.on("error", (error) => {
|
|
600
653
|
deps.recordTransportEvent?.("server-socket-error", {
|
|
601
|
-
...getTelegramBusEndpointDiagnostics(
|
|
654
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
602
655
|
...classifyTelegramBusTransportError(error),
|
|
603
656
|
});
|
|
604
657
|
closeSocket(socket);
|
|
@@ -607,24 +660,28 @@ export function createTelegramBusLocalServer(
|
|
|
607
660
|
try {
|
|
608
661
|
await new Promise<void>((resolve, reject) => {
|
|
609
662
|
server?.once("error", reject);
|
|
610
|
-
server?.listen(
|
|
663
|
+
server?.listen(socketPath, resolve);
|
|
611
664
|
});
|
|
612
665
|
deps.recordTransportEvent?.(
|
|
613
666
|
"server-started",
|
|
614
|
-
getTelegramBusEndpointDiagnostics(
|
|
667
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
615
668
|
);
|
|
616
669
|
} catch (error) {
|
|
670
|
+
server = undefined;
|
|
671
|
+
activeSocketPath = undefined;
|
|
617
672
|
deps.recordTransportEvent?.("server-start-failed", {
|
|
618
|
-
...getTelegramBusEndpointDiagnostics(
|
|
673
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
619
674
|
...classifyTelegramBusTransportError(error),
|
|
620
675
|
});
|
|
621
676
|
throw error;
|
|
622
677
|
}
|
|
623
|
-
if (!usesWindowsPipe) chmodSync(
|
|
678
|
+
if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
|
|
624
679
|
},
|
|
625
680
|
stop: async () => {
|
|
626
681
|
const activeServer = server;
|
|
682
|
+
const socketPath = activeSocketPath;
|
|
627
683
|
server = undefined;
|
|
684
|
+
activeSocketPath = undefined;
|
|
628
685
|
for (const socket of sockets) closeSocket(socket);
|
|
629
686
|
if (activeServer) {
|
|
630
687
|
await new Promise<void>((resolve) =>
|
|
@@ -632,15 +689,18 @@ export function createTelegramBusLocalServer(
|
|
|
632
689
|
);
|
|
633
690
|
}
|
|
634
691
|
if (
|
|
635
|
-
|
|
636
|
-
|
|
692
|
+
socketPath &&
|
|
693
|
+
!isTelegramBusPipePath(socketPath) &&
|
|
694
|
+
existsSync(socketPath)
|
|
637
695
|
) {
|
|
638
|
-
unlinkSync(
|
|
696
|
+
unlinkSync(socketPath);
|
|
697
|
+
}
|
|
698
|
+
if (socketPath) {
|
|
699
|
+
deps.recordTransportEvent?.(
|
|
700
|
+
"server-stopped",
|
|
701
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
702
|
+
);
|
|
639
703
|
}
|
|
640
|
-
deps.recordTransportEvent?.(
|
|
641
|
-
"server-stopped",
|
|
642
|
-
getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
643
|
-
);
|
|
644
704
|
},
|
|
645
705
|
};
|
|
646
706
|
}
|
package/lib/paths.ts
CHANGED
|
@@ -71,6 +71,17 @@ export function resolveTelegramProfileTempFilePath(
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
|
|
75
|
+
state: string;
|
|
76
|
+
logs: string;
|
|
77
|
+
} {
|
|
78
|
+
const suffix = getTelegramProfilePathSuffix(profileName);
|
|
79
|
+
return {
|
|
80
|
+
state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
|
|
81
|
+
logs: `~/.pi/agent/tmp/telegram/logs${suffix}.jsonl`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
75
86
|
export function resolveTelegramRuntimeLogPath(): string {
|
|
76
87
|
return resolveTelegramProfileTempFilePath("logs", "jsonl");
|
package/lib/prompts.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
8
|
|
|
9
|
+
import { getTelegramDiagnosticsDisplayPaths } from "./paths.ts";
|
|
9
10
|
import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi.ts";
|
|
10
11
|
import { TELEGRAM_PREFIX } from "./turns.ts";
|
|
11
12
|
|
|
@@ -17,7 +18,9 @@ const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
|
17
18
|
|
|
18
19
|
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload.`;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
function buildTelegramHelpText(profileName?: string): string {
|
|
22
|
+
const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
|
|
23
|
+
return `--- TELEGRAM BRIDGE HELP ---
|
|
21
24
|
|
|
22
25
|
How to understand Telegram turns:
|
|
23
26
|
- \`[telegram|thread:name|from:user|guest:group]\` marks Telegram origin and attributes.
|
|
@@ -60,15 +63,19 @@ Configurable handlers:
|
|
|
60
63
|
- If command-template config is not enough, build a companion extension through the public pi-telegram APIs; do not import package-private \`lib/*\` paths.
|
|
61
64
|
|
|
62
65
|
Debugging pi-telegram:
|
|
63
|
-
- Inspect
|
|
64
|
-
- Inspect
|
|
66
|
+
- Inspect \`${diagnosticsPaths.state}\` for runtime state, roster, bindings, slots, reservations, and diagnostics.
|
|
67
|
+
- Inspect \`${diagnosticsPaths.logs}\` for redacted runtime event evidence.
|
|
65
68
|
- Use terminal \`telegram-status\` for compact human health; use \`telegram-status --debug\` for the full human-readable diagnostic dump.`;
|
|
69
|
+
}
|
|
66
70
|
|
|
67
|
-
export function getTelegramHelpText(): string {
|
|
68
|
-
return
|
|
71
|
+
export function getTelegramHelpText(profileName?: string): string {
|
|
72
|
+
return buildTelegramHelpText(profileName);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
export function registerTelegramHelpTool(
|
|
75
|
+
export function registerTelegramHelpTool(
|
|
76
|
+
pi: ExtensionAPI,
|
|
77
|
+
options: { getActiveProfileName?: () => string | undefined } = {},
|
|
78
|
+
): void {
|
|
72
79
|
pi.registerTool({
|
|
73
80
|
name: "telegram_help",
|
|
74
81
|
label: "Telegram Help",
|
|
@@ -77,7 +84,12 @@ export function registerTelegramHelpTool(pi: ExtensionAPI): void {
|
|
|
77
84
|
parameters: Type.Object({}),
|
|
78
85
|
async execute() {
|
|
79
86
|
return {
|
|
80
|
-
content: [
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: getTelegramHelpText(options.getActiveProfileName?.()),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
81
93
|
details: {},
|
|
82
94
|
};
|
|
83
95
|
},
|
package/lib/setup.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface TelegramPollingStartResult {
|
|
|
27
27
|
message?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export type TelegramSetupCompletion =
|
|
31
|
+
| { status: "success"; config: TelegramSetupConfig }
|
|
32
|
+
| { status: "cancelled" | "unavailable" | "busy" | "validation-failed" }
|
|
33
|
+
| { status: "polling-failed"; config: TelegramSetupConfig };
|
|
34
|
+
|
|
30
35
|
export interface TelegramSetupDeps {
|
|
31
36
|
hasUI: boolean;
|
|
32
37
|
env: NodeJS.ProcessEnv;
|
|
@@ -120,8 +125,8 @@ export function getTelegramBotTokenPromptSpec(
|
|
|
120
125
|
|
|
121
126
|
export async function runTelegramSetup(
|
|
122
127
|
deps: TelegramSetupDeps,
|
|
123
|
-
): Promise<
|
|
124
|
-
if (!deps.hasUI) return
|
|
128
|
+
): Promise<TelegramSetupCompletion> {
|
|
129
|
+
if (!deps.hasUI) return { status: "unavailable" };
|
|
125
130
|
const tokenPrompt = getTelegramBotTokenPromptSpec(
|
|
126
131
|
deps.env,
|
|
127
132
|
deps.config.botToken,
|
|
@@ -130,7 +135,7 @@ export async function runTelegramSetup(
|
|
|
130
135
|
tokenPrompt.method === "editor"
|
|
131
136
|
? await deps.promptEditor("Telegram bot token", tokenPrompt.value)
|
|
132
137
|
: await deps.promptInput("Telegram bot token", tokenPrompt.value);
|
|
133
|
-
if (!token) return
|
|
138
|
+
if (!token) return { status: "cancelled" };
|
|
134
139
|
const nextConfig: TelegramSetupConfig = {
|
|
135
140
|
...deps.config,
|
|
136
141
|
botToken: token.trim(),
|
|
@@ -141,11 +146,11 @@ export async function runTelegramSetup(
|
|
|
141
146
|
} catch (error) {
|
|
142
147
|
const message = error instanceof Error ? error.message : String(error);
|
|
143
148
|
deps.notify(`Telegram API check failed: ${message}`, "error");
|
|
144
|
-
return
|
|
149
|
+
return { status: "validation-failed" };
|
|
145
150
|
}
|
|
146
151
|
if (!data.ok || !data.result) {
|
|
147
152
|
deps.notify(data.description || "Invalid Telegram bot token", "error");
|
|
148
|
-
return
|
|
153
|
+
return { status: "validation-failed" };
|
|
149
154
|
}
|
|
150
155
|
nextConfig.botId = data.result.id;
|
|
151
156
|
nextConfig.botUsername = data.result.username;
|
|
@@ -158,21 +163,33 @@ export async function runTelegramSetup(
|
|
|
158
163
|
"Send /start to your bot in Telegram to pair this extension with your account.",
|
|
159
164
|
"info",
|
|
160
165
|
);
|
|
161
|
-
|
|
166
|
+
let startResult: unknown;
|
|
167
|
+
try {
|
|
168
|
+
startResult = await deps.startPolling();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
deps.notify(`Telegram polling failed: ${message}`, "error");
|
|
172
|
+
deps.updateStatus();
|
|
173
|
+
return { status: "polling-failed", config: nextConfig };
|
|
174
|
+
}
|
|
162
175
|
if (isTelegramPollingStartResult(startResult) && startResult.message) {
|
|
163
176
|
deps.notify(startResult.message, startResult.ok ? "info" : "error");
|
|
164
177
|
}
|
|
165
178
|
deps.updateStatus();
|
|
166
|
-
|
|
179
|
+
if (isTelegramPollingStartResult(startResult) && !startResult.ok) {
|
|
180
|
+
return { status: "polling-failed", config: nextConfig };
|
|
181
|
+
}
|
|
182
|
+
return { status: "success", config: nextConfig };
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
export function createTelegramSetupPromptRuntime<
|
|
170
186
|
TContext extends TelegramSetupPromptContext,
|
|
171
187
|
>(deps: TelegramSetupPromptRuntimeDeps<TContext>) {
|
|
172
|
-
return async (ctx: TContext): Promise<
|
|
173
|
-
if (!ctx.hasUI
|
|
188
|
+
return async (ctx: TContext): Promise<TelegramSetupCompletion> => {
|
|
189
|
+
if (!ctx.hasUI) return { status: "unavailable" };
|
|
190
|
+
if (!deps.setupGuard.start()) return { status: "busy" };
|
|
174
191
|
try {
|
|
175
|
-
await runTelegramSetup({
|
|
192
|
+
return await runTelegramSetup({
|
|
176
193
|
hasUI: ctx.hasUI,
|
|
177
194
|
env: deps.env ?? process.env,
|
|
178
195
|
config: deps.getConfig(),
|
package/lib/status.ts
CHANGED
|
@@ -1065,6 +1065,13 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1065
1065
|
: state.activeSourceMessageIds?.length
|
|
1066
1066
|
? "active"
|
|
1067
1067
|
: "idle";
|
|
1068
|
+
const profileSuffix = state.activeProfileName
|
|
1069
|
+
? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
|
|
1070
|
+
: "";
|
|
1071
|
+
const diagnosticsPaths = {
|
|
1072
|
+
state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
|
|
1073
|
+
logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
|
|
1074
|
+
};
|
|
1068
1075
|
return [
|
|
1069
1076
|
"connection:",
|
|
1070
1077
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
@@ -1094,8 +1101,8 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1094
1101
|
...buildTelegramThreadReconciliationLines(state),
|
|
1095
1102
|
"",
|
|
1096
1103
|
"diagnostics:",
|
|
1097
|
-
|
|
1098
|
-
|
|
1104
|
+
`- state: ${diagnosticsPaths.state}`,
|
|
1105
|
+
`- logs: ${diagnosticsPaths.logs}`,
|
|
1099
1106
|
"- full dump: /telegram-status --debug",
|
|
1100
1107
|
];
|
|
1101
1108
|
}
|
package/lib/threads.ts
CHANGED
|
@@ -133,30 +133,32 @@ function getNextMonotonicSlot(
|
|
|
133
133
|
nowMs: number,
|
|
134
134
|
lastSlot?: string,
|
|
135
135
|
): string | undefined {
|
|
136
|
-
let
|
|
137
|
-
for (const record of records.values()) {
|
|
138
|
-
if (!record.slot || !isCurrentThreadRecord(record)) continue;
|
|
139
|
-
maxCode = Math.max(maxCode, record.slot.charCodeAt(0));
|
|
140
|
-
}
|
|
141
|
-
for (const reservation of reservations) {
|
|
142
|
-
if (
|
|
143
|
-
reservation.expiresAtMs !== undefined &&
|
|
144
|
-
reservation.expiresAtMs <= nowMs
|
|
145
|
-
)
|
|
146
|
-
continue;
|
|
147
|
-
if (!reservation.slot) continue;
|
|
148
|
-
maxCode = Math.max(maxCode, reservation.slot.charCodeAt(0));
|
|
149
|
-
}
|
|
150
|
-
for (const provision of pendingProvisions) {
|
|
151
|
-
if (provision.expiresAtMs !== undefined && provision.expiresAtMs <= nowMs)
|
|
152
|
-
continue;
|
|
153
|
-
if (!provision.slot) continue;
|
|
154
|
-
maxCode = Math.max(maxCode, provision.slot.charCodeAt(0));
|
|
155
|
-
}
|
|
136
|
+
let cursorCode: number | undefined;
|
|
156
137
|
if (lastSlot && /^[A-Z]$/.test(lastSlot)) {
|
|
157
|
-
|
|
138
|
+
cursorCode = lastSlot.charCodeAt(0);
|
|
139
|
+
} else {
|
|
140
|
+
cursorCode = "A".charCodeAt(0) - 1;
|
|
141
|
+
for (const record of records.values()) {
|
|
142
|
+
if (!record.slot || !isCurrentThreadRecord(record)) continue;
|
|
143
|
+
cursorCode = Math.max(cursorCode, record.slot.charCodeAt(0));
|
|
144
|
+
}
|
|
145
|
+
for (const reservation of reservations) {
|
|
146
|
+
if (
|
|
147
|
+
reservation.expiresAtMs !== undefined &&
|
|
148
|
+
reservation.expiresAtMs <= nowMs
|
|
149
|
+
)
|
|
150
|
+
continue;
|
|
151
|
+
if (!reservation.slot) continue;
|
|
152
|
+
cursorCode = Math.max(cursorCode, reservation.slot.charCodeAt(0));
|
|
153
|
+
}
|
|
154
|
+
for (const provision of pendingProvisions) {
|
|
155
|
+
if (provision.expiresAtMs !== undefined && provision.expiresAtMs <= nowMs)
|
|
156
|
+
continue;
|
|
157
|
+
if (!provision.slot) continue;
|
|
158
|
+
cursorCode = Math.max(cursorCode, provision.slot.charCodeAt(0));
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
|
-
let code =
|
|
161
|
+
let code = cursorCode + 1;
|
|
160
162
|
if (code > "Z".charCodeAt(0)) code = "A".charCodeAt(0);
|
|
161
163
|
for (let attempt = 0; attempt < 26; attempt++) {
|
|
162
164
|
const candidate = String.fromCharCode(code);
|
|
@@ -228,6 +230,39 @@ export interface TelegramTopicTargetStore {
|
|
|
228
230
|
) => TelegramTopicTargetRecord | undefined;
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
export function reconcileTelegramFreshAllocationCursor(
|
|
234
|
+
store: Pick<
|
|
235
|
+
TelegramTopicTargetStore,
|
|
236
|
+
"getBotState" | "list" | "setBotState"
|
|
237
|
+
>,
|
|
238
|
+
nowMs = Date.now(),
|
|
239
|
+
): boolean {
|
|
240
|
+
const currentCursor = store.getBotState().lastSlot;
|
|
241
|
+
const slottedRecords = store
|
|
242
|
+
.list()
|
|
243
|
+
.filter((record) => !!record.slot && /^[A-Z]$/.test(record.slot));
|
|
244
|
+
if (slottedRecords.some((record) => record.slot === currentCursor)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
const latestLiveRecord = slottedRecords.reduce<
|
|
248
|
+
TelegramTopicTargetRecord | undefined
|
|
249
|
+
>((latest, record) => {
|
|
250
|
+
if (!latest) return record;
|
|
251
|
+
if (record.createdAtMs !== latest.createdAtMs) {
|
|
252
|
+
return record.createdAtMs > latest.createdAtMs ? record : latest;
|
|
253
|
+
}
|
|
254
|
+
return record.updatedAtMs > latest.updatedAtMs ? record : latest;
|
|
255
|
+
}, undefined);
|
|
256
|
+
const nextCursor = latestLiveRecord?.slot;
|
|
257
|
+
if (nextCursor === currentCursor) return false;
|
|
258
|
+
store.setBotState({
|
|
259
|
+
lastSlot: nextCursor,
|
|
260
|
+
updatedAtMs: nowMs,
|
|
261
|
+
lastReconcileAction: "live-cursor-realignment",
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
231
266
|
export interface TelegramTopicTargetStoreOptions {
|
|
232
267
|
path: string | (() => string);
|
|
233
268
|
getNowMs?: () => number;
|
|
@@ -881,9 +916,6 @@ export function createTelegramTopicTargetStore(
|
|
|
881
916
|
|
|
882
917
|
const rememberSlot = (slot: string | undefined, nowMs = getNowMs()) => {
|
|
883
918
|
if (!slot || !/^[A-Z]$/.test(slot)) return;
|
|
884
|
-
const currentCode =
|
|
885
|
-
botState.lastSlot?.charCodeAt(0) ?? "A".charCodeAt(0) - 1;
|
|
886
|
-
if (slot.charCodeAt(0) < currentCode) return;
|
|
887
919
|
botState = { ...botState, lastSlot: slot, updatedAtMs: nowMs };
|
|
888
920
|
};
|
|
889
921
|
const rememberIdentity = (record: TelegramTopicTargetRecord) => {
|
|
@@ -1151,6 +1183,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1151
1183
|
upsert(record) {
|
|
1152
1184
|
const next = cloneRecord(record);
|
|
1153
1185
|
const nextOwnerKey = getRecordOwnerKey(next);
|
|
1186
|
+
const previousRecord = records.get(nextOwnerKey);
|
|
1154
1187
|
if (isCurrentThreadRecord(next)) {
|
|
1155
1188
|
for (const existing of Array.from(records.values())) {
|
|
1156
1189
|
const existingOwnerKey = getRecordOwnerKey(existing);
|
|
@@ -1180,7 +1213,12 @@ export function createTelegramTopicTargetStore(
|
|
|
1180
1213
|
return cloneRecord(next);
|
|
1181
1214
|
}
|
|
1182
1215
|
records.set(nextOwnerKey, next);
|
|
1183
|
-
|
|
1216
|
+
if (
|
|
1217
|
+
!previousRecord ||
|
|
1218
|
+
!targetMatches(previousRecord.target, next.target)
|
|
1219
|
+
) {
|
|
1220
|
+
rememberSlot(next.slot, next.updatedAtMs);
|
|
1221
|
+
}
|
|
1184
1222
|
rememberIdentity(next);
|
|
1185
1223
|
loaded = true;
|
|
1186
1224
|
dirty = true;
|
package/lib/turns.ts
CHANGED
|
@@ -8,8 +8,6 @@ import { readFile } from "node:fs/promises";
|
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
appendTelegramForwardContext,
|
|
12
|
-
appendTelegramReplyContext,
|
|
13
11
|
buildTelegramReplyContextBlock,
|
|
14
12
|
collectTelegramMessageIds,
|
|
15
13
|
downloadTelegramMessageFiles,
|