@llblab/pi-telegram 0.17.5 → 0.18.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 +67 -32
- package/BACKLOG.md +59 -19
- package/CHANGELOG.md +36 -15
- package/README.md +63 -35
- package/docs/README.md +3 -1
- package/docs/architecture.md +55 -23
- package/docs/callback-namespaces.md +1 -1
- package/docs/inbound.md +1 -1
- package/docs/locks.md +0 -2
- package/docs/multi-instance-bus.md +483 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +12 -10
- package/docs/sections.md +2 -2
- package/docs/ui-style.md +76 -0
- package/index.ts +789 -32
- package/lib/bindings.ts +68 -12
- package/lib/bus-api.ts +314 -0
- package/lib/bus-follower.ts +853 -0
- package/lib/bus-leader.ts +915 -0
- package/lib/bus.ts +866 -0
- package/lib/command-templates.ts +9 -11
- package/lib/commands.ts +133 -47
- package/lib/config.ts +53 -5
- package/lib/lifecycle.ts +23 -7
- package/lib/locks.ts +230 -66
- package/lib/media.ts +30 -2
- package/lib/menu-model.ts +48 -17
- package/lib/menu-queue.ts +51 -20
- package/lib/menu-settings.ts +9 -5
- package/lib/menu-status.ts +3 -0
- package/lib/menu-thinking.ts +3 -0
- package/lib/menu.ts +67 -26
- package/lib/outbound-attachments.ts +102 -17
- package/lib/outbound-buttons.ts +6 -2
- package/lib/outbound-voice.ts +31 -11
- package/lib/outbound.ts +6 -4
- package/lib/ownership.ts +119 -0
- package/lib/pi.ts +26 -3
- package/lib/polling.ts +477 -7
- package/lib/preview.ts +141 -88
- package/lib/prompt-templates.ts +3 -3
- package/lib/prompts.ts +80 -30
- package/lib/queue.ts +193 -91
- package/lib/rendering.ts +0 -25
- package/lib/replies.ts +187 -55
- package/lib/routing.ts +1673 -9
- package/lib/runtime-log.ts +123 -0
- package/lib/runtime.ts +84 -12
- package/lib/sections.ts +28 -21
- package/lib/setup.ts +1 -1
- package/lib/status.ts +532 -9
- package/lib/sync.ts +618 -0
- package/lib/target.ts +49 -0
- package/lib/telegram-api.ts +405 -40
- package/lib/text-groups.ts +5 -1
- package/lib/thread-reconciler.ts +915 -0
- package/lib/threads.ts +2205 -0
- package/lib/turns.ts +48 -3
- package/lib/updates.ts +355 -32
- package/package.json +24 -2
- package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/outbound-voice.ts
CHANGED
|
@@ -8,11 +8,17 @@ import { unlink } from "node:fs/promises";
|
|
|
8
8
|
import { basename, extname } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
11
|
+
import { buildTelegramMultipartReplyParameters } from "./replies.ts";
|
|
12
|
+
import {
|
|
13
|
+
getTelegramTargetThreadParams,
|
|
14
|
+
type TelegramTarget,
|
|
15
|
+
} from "./target.ts";
|
|
11
16
|
import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
|
|
12
17
|
|
|
13
18
|
export interface TelegramVoiceReplyTurnView {
|
|
14
19
|
chatId: number;
|
|
15
20
|
replyToMessageId: number;
|
|
21
|
+
target?: TelegramTarget;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface TelegramVoiceReplySenderDeps {
|
|
@@ -26,7 +32,12 @@ export interface TelegramVoiceReplySenderDeps {
|
|
|
26
32
|
stdin?: string;
|
|
27
33
|
retry?: number;
|
|
28
34
|
},
|
|
29
|
-
) => Promise<{
|
|
35
|
+
) => Promise<{
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr: string;
|
|
38
|
+
code: number;
|
|
39
|
+
killed: boolean;
|
|
40
|
+
}>;
|
|
30
41
|
sendMultipart: (
|
|
31
42
|
method: string,
|
|
32
43
|
fields: Record<string, string>,
|
|
@@ -68,15 +79,14 @@ export interface TelegramVoiceReplySenderPorts<THandler = unknown> {
|
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
function buildVoiceReplyParameters(
|
|
82
|
+
chatId: number,
|
|
71
83
|
replyToPrompt: boolean | undefined,
|
|
72
84
|
replyToMessageId: number | undefined,
|
|
85
|
+
target?: TelegramTarget,
|
|
73
86
|
): string | undefined {
|
|
74
87
|
if (replyToPrompt === false || replyToMessageId === undefined)
|
|
75
88
|
return undefined;
|
|
76
|
-
return
|
|
77
|
-
message_id: replyToMessageId,
|
|
78
|
-
allow_sending_without_reply: true,
|
|
79
|
-
});
|
|
89
|
+
return buildTelegramMultipartReplyParameters(chatId, replyToMessageId, target);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
async function ensureTelegramVoiceFileFormat(
|
|
@@ -116,7 +126,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
116
126
|
deps: TelegramVoiceReplySenderDeps,
|
|
117
127
|
ports: TelegramVoiceReplySenderPorts<THandler> = {},
|
|
118
128
|
) {
|
|
119
|
-
async
|
|
129
|
+
const uploadVoiceFile = async (
|
|
120
130
|
turn: TelegramVoiceReplyTurnView,
|
|
121
131
|
filePath: string,
|
|
122
132
|
options?: {
|
|
@@ -124,13 +134,15 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
124
134
|
replyMarkup?: unknown;
|
|
125
135
|
transcriptText?: string;
|
|
126
136
|
},
|
|
127
|
-
): Promise<void> {
|
|
137
|
+
): Promise<void> => {
|
|
128
138
|
const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
|
|
129
139
|
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
130
140
|
await sendVoiceChatAction(deps, turn.chatId);
|
|
131
141
|
const replyParameters = buildVoiceReplyParameters(
|
|
142
|
+
turn.chatId,
|
|
132
143
|
options?.replyToPrompt,
|
|
133
144
|
turn.replyToMessageId,
|
|
145
|
+
turn.target,
|
|
134
146
|
);
|
|
135
147
|
await deps.sendMultipart(
|
|
136
148
|
"sendVoice",
|
|
@@ -138,6 +150,13 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
138
150
|
chat_id: String(turn.chatId),
|
|
139
151
|
...(options?.transcriptText ? { caption: options.transcriptText } : {}),
|
|
140
152
|
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
153
|
+
...(turn.target
|
|
154
|
+
? Object.fromEntries(
|
|
155
|
+
Object.entries(getTelegramTargetThreadParams(turn.target)).map(
|
|
156
|
+
([key, value]) => [key, String(value)],
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
: {}),
|
|
141
160
|
...(options?.replyMarkup !== undefined && options.replyMarkup !== null
|
|
142
161
|
? {
|
|
143
162
|
reply_markup:
|
|
@@ -151,9 +170,9 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
151
170
|
voiceFilePath,
|
|
152
171
|
basename(voiceFilePath),
|
|
153
172
|
);
|
|
154
|
-
}
|
|
173
|
+
};
|
|
155
174
|
|
|
156
|
-
return async
|
|
175
|
+
return async (
|
|
157
176
|
turn: TelegramVoiceReplyTurnView,
|
|
158
177
|
text: string,
|
|
159
178
|
options?: {
|
|
@@ -162,8 +181,9 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
162
181
|
replyToPrompt?: boolean;
|
|
163
182
|
replyMarkup?: unknown;
|
|
164
183
|
},
|
|
165
|
-
): Promise<void> {
|
|
166
|
-
for (const handler of ports.findVoiceHandlers?.(deps.getHandlers?.()) ??
|
|
184
|
+
): Promise<void> => {
|
|
185
|
+
for (const handler of ports.findVoiceHandlers?.(deps.getHandlers?.()) ??
|
|
186
|
+
[]) {
|
|
167
187
|
try {
|
|
168
188
|
const filePath = await ports.generateVoiceFile?.(text, {
|
|
169
189
|
lang: options?.lang,
|
package/lib/outbound.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type TelegramVoiceReplyItem,
|
|
20
20
|
} from "./outbound-markup.ts";
|
|
21
21
|
import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.ts";
|
|
22
|
+
import type { TelegramTarget } from "./target.ts";
|
|
22
23
|
|
|
23
24
|
const OUTBOUND_HANDLER_REGISTRY_KEY = "__piTelegramOutboundHandlers__";
|
|
24
25
|
const VOICE_EVENT_RECORDER_KEY = "__piTelegramVoiceEventRecorder__";
|
|
@@ -102,6 +103,7 @@ export interface TelegramVoiceExecResult {
|
|
|
102
103
|
export interface TelegramVoiceReplyTurnView {
|
|
103
104
|
chatId: number;
|
|
104
105
|
replyToMessageId: number;
|
|
106
|
+
target?: TelegramTarget;
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
export interface TelegramVoiceReplySenderDeps {
|
|
@@ -207,13 +209,13 @@ export interface TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup = unknown> {
|
|
|
207
209
|
chatId: number,
|
|
208
210
|
replyToMessageId: number | undefined,
|
|
209
211
|
text: string,
|
|
210
|
-
options?: { parseMode?: "HTML" },
|
|
212
|
+
options?: { parseMode?: "HTML"; target?: TelegramTarget },
|
|
211
213
|
) => Promise<number | undefined>;
|
|
212
214
|
sendMarkdownReply: (
|
|
213
215
|
chatId: number,
|
|
214
216
|
replyToMessageId: number | undefined,
|
|
215
217
|
markdown: string,
|
|
216
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
218
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
217
219
|
) => Promise<number | undefined>;
|
|
218
220
|
cwd?: string;
|
|
219
221
|
recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
|
|
@@ -867,14 +869,14 @@ export function createTelegramOutboundReplyArtifactSender(
|
|
|
867
869
|
deps: TelegramVoiceReplySenderDeps,
|
|
868
870
|
) {
|
|
869
871
|
const sendVoiceReply = createTelegramVoiceReplySender(deps);
|
|
870
|
-
return async
|
|
872
|
+
return async (
|
|
871
873
|
turn: TelegramVoiceReplyTurnView,
|
|
872
874
|
plan: Pick<
|
|
873
875
|
TelegramOutboundReplyPlan,
|
|
874
876
|
"voiceText" | "voiceReplies" | "lang" | "rate" | "replyMarkup"
|
|
875
877
|
>,
|
|
876
878
|
options?: { replyToPrompt?: boolean },
|
|
877
|
-
): Promise<void> {
|
|
879
|
+
): Promise<void> => {
|
|
878
880
|
// Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
|
|
879
881
|
const voiceReplies = plan.voiceReplies?.length
|
|
880
882
|
? plan.voiceReplies
|
package/lib/ownership.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram message ownership helpers
|
|
3
|
+
* Zones: telegram routing, multi-instance bus, in-memory coordination
|
|
4
|
+
* Owns live message-id ownership lookup so callbacks/reactions can resolve a Telegram UI surface back to the instance/target that produced it
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
8
|
+
|
|
9
|
+
export interface TelegramMessageOwnershipRecord {
|
|
10
|
+
chatId: number;
|
|
11
|
+
messageId: number;
|
|
12
|
+
target: TelegramTarget;
|
|
13
|
+
instanceId: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TelegramMessageOwnershipStore {
|
|
19
|
+
record: (input: {
|
|
20
|
+
chatId: number;
|
|
21
|
+
messageId: number;
|
|
22
|
+
target?: TelegramTarget;
|
|
23
|
+
instanceId: string;
|
|
24
|
+
now?: number;
|
|
25
|
+
}) => TelegramMessageOwnershipRecord;
|
|
26
|
+
get: (
|
|
27
|
+
chatId: number,
|
|
28
|
+
messageId: number,
|
|
29
|
+
) => TelegramMessageOwnershipRecord | undefined;
|
|
30
|
+
forget: (chatId: number, messageId: number) => boolean;
|
|
31
|
+
forgetTarget: (target: TelegramTarget) => number;
|
|
32
|
+
prune: (options: {
|
|
33
|
+
now: number;
|
|
34
|
+
maxAgeMs?: number;
|
|
35
|
+
maxRecords?: number;
|
|
36
|
+
}) => number;
|
|
37
|
+
entries: () => TelegramMessageOwnershipRecord[];
|
|
38
|
+
clear: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getTelegramMessageOwnershipKey(
|
|
42
|
+
chatId: number,
|
|
43
|
+
messageId: number,
|
|
44
|
+
): string {
|
|
45
|
+
return `${chatId}:${messageId}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTelegramMessageOwnershipRecord(input: {
|
|
49
|
+
chatId: number;
|
|
50
|
+
messageId: number;
|
|
51
|
+
target?: TelegramTarget;
|
|
52
|
+
instanceId: string;
|
|
53
|
+
now: number;
|
|
54
|
+
previous?: TelegramMessageOwnershipRecord;
|
|
55
|
+
}): TelegramMessageOwnershipRecord {
|
|
56
|
+
return {
|
|
57
|
+
chatId: input.chatId,
|
|
58
|
+
messageId: input.messageId,
|
|
59
|
+
target: input.target ?? { chatId: input.chatId },
|
|
60
|
+
instanceId: input.instanceId,
|
|
61
|
+
createdAt: input.previous?.createdAt ?? input.now,
|
|
62
|
+
updatedAt: input.now,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createTelegramMessageOwnershipStore(): TelegramMessageOwnershipStore {
|
|
67
|
+
const records = new Map<string, TelegramMessageOwnershipRecord>();
|
|
68
|
+
return {
|
|
69
|
+
record: (input) => {
|
|
70
|
+
const key = getTelegramMessageOwnershipKey(input.chatId, input.messageId);
|
|
71
|
+
const now = input.now ?? Date.now();
|
|
72
|
+
const record = createTelegramMessageOwnershipRecord({
|
|
73
|
+
...input,
|
|
74
|
+
now,
|
|
75
|
+
previous: records.get(key),
|
|
76
|
+
});
|
|
77
|
+
records.set(key, record);
|
|
78
|
+
return record;
|
|
79
|
+
},
|
|
80
|
+
get: (chatId, messageId) =>
|
|
81
|
+
records.get(getTelegramMessageOwnershipKey(chatId, messageId)),
|
|
82
|
+
forget: (chatId, messageId) =>
|
|
83
|
+
records.delete(getTelegramMessageOwnershipKey(chatId, messageId)),
|
|
84
|
+
forgetTarget: (target) => {
|
|
85
|
+
const targetKey = getTelegramTargetKey(target);
|
|
86
|
+
let removed = 0;
|
|
87
|
+
for (const [key, record] of records) {
|
|
88
|
+
if (getTelegramTargetKey(record.target) !== targetKey) continue;
|
|
89
|
+
records.delete(key);
|
|
90
|
+
removed += 1;
|
|
91
|
+
}
|
|
92
|
+
return removed;
|
|
93
|
+
},
|
|
94
|
+
prune: ({ now, maxAgeMs, maxRecords }) => {
|
|
95
|
+
let removed = 0;
|
|
96
|
+
if (maxAgeMs !== undefined) {
|
|
97
|
+
for (const [key, record] of records) {
|
|
98
|
+
if (now - record.updatedAt <= maxAgeMs) continue;
|
|
99
|
+
records.delete(key);
|
|
100
|
+
removed += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (maxRecords !== undefined && records.size > maxRecords) {
|
|
104
|
+
const oldest = [...records.entries()].sort(
|
|
105
|
+
(left, right) => left[1].updatedAt - right[1].updatedAt,
|
|
106
|
+
);
|
|
107
|
+
for (const [key] of oldest.slice(0, records.size - maxRecords)) {
|
|
108
|
+
records.delete(key);
|
|
109
|
+
removed += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return removed;
|
|
113
|
+
},
|
|
114
|
+
entries: () => [...records.values()],
|
|
115
|
+
clear: () => {
|
|
116
|
+
records.clear();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/lib/pi.ts
CHANGED
|
@@ -33,6 +33,29 @@ export type {
|
|
|
33
33
|
SlashCommandInfo,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export interface ToolExecutionStartEvent {
|
|
37
|
+
type: "tool_execution_start";
|
|
38
|
+
toolCallId: string;
|
|
39
|
+
toolName: string;
|
|
40
|
+
args: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ToolExecutionUpdateEvent {
|
|
44
|
+
type: "tool_execution_update";
|
|
45
|
+
toolCallId: string;
|
|
46
|
+
toolName: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
partialResult: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ToolExecutionEndEvent {
|
|
52
|
+
type: "tool_execution_end";
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
toolName: string;
|
|
55
|
+
result: unknown;
|
|
56
|
+
isError: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
export interface PiSettingsManager {
|
|
37
60
|
reload: () => Promise<void>;
|
|
38
61
|
flush: () => Promise<void>;
|
|
@@ -72,8 +95,8 @@ export function canStartPollingInExtensionContext(ctx: unknown): boolean {
|
|
|
72
95
|
export function formatPollingStartBlockedByRunMode(ctx: unknown): string {
|
|
73
96
|
const mode = getExtensionContextMode(ctx);
|
|
74
97
|
return mode
|
|
75
|
-
? `Telegram polling is unavailable in
|
|
76
|
-
: "Telegram polling is unavailable in this
|
|
98
|
+
? `Telegram polling is unavailable in Pi ${mode} mode. Use /telegram-connect from a long-lived Pi session.`
|
|
99
|
+
: "Telegram polling is unavailable in this Pi run mode.";
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
export type PiSendUserMessageOptions = NonNullable<
|
|
@@ -118,7 +141,7 @@ export function createScopedModelPatternPersister(deps: {
|
|
|
118
141
|
createSettingsManager: (cwd: string) => PiSettingsManager;
|
|
119
142
|
clearCachedModelMenuInputs: () => void;
|
|
120
143
|
}): (patterns: string[], ctx: ExtensionContext) => Promise<void> {
|
|
121
|
-
return async
|
|
144
|
+
return async (patterns, ctx) => {
|
|
122
145
|
const settingsManager = deps.createSettingsManager(ctx.cwd);
|
|
123
146
|
settingsManager.setEnabledModels(
|
|
124
147
|
patterns.length > 0 ? patterns : undefined,
|