@llblab/pi-telegram 0.20.1 → 0.20.3
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 +5 -3
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +3 -2
- package/docs/architecture.md +5 -5
- package/docs/multi-instance-bus.md +22 -21
- package/index.ts +133 -219
- package/lib/bindings.ts +55 -14
- package/lib/bus-follower.ts +161 -32
- package/lib/bus-leader.ts +213 -42
- package/lib/bus.ts +34 -0
- 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/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,
|