@llblab/pi-telegram 0.19.3 → 0.20.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 +6 -3
- package/BACKLOG.md +7 -6
- package/CHANGELOG.md +14 -2
- package/README.md +8 -5
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -1
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +2 -0
- package/docs/outbound.md +5 -5
- package/docs/public-api.md +1 -1
- package/index.ts +95 -26
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +37 -13
- package/lib/bus-leader.ts +28 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +66 -43
- package/lib/commands.ts +41 -8
- package/lib/config.ts +165 -26
- package/lib/locks.ts +78 -25
- package/lib/{runtime-log.ts → logs.ts} +58 -27
- package/lib/media.ts +97 -4
- package/lib/outbound-buttons.ts +1 -1
- package/lib/outbound.ts +5 -8
- package/lib/paths.ts +77 -0
- package/lib/prompt-templates.ts +3 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +32 -1
- package/lib/routing.ts +106 -41
- package/lib/status.ts +6 -1
- package/lib/sync.ts +22 -17
- package/lib/telegram-api.ts +13 -14
- package/lib/threads.ts +142 -55
- package/lib/turns.ts +47 -13
- package/package.json +3 -3
- /package/{banner.png → screenshot.png} +0 -0
package/lib/locks.ts
CHANGED
|
@@ -12,20 +12,38 @@ import {
|
|
|
12
12
|
unlinkSync,
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { resolveTelegramLocksPath } from "./paths.ts";
|
|
17
17
|
|
|
18
18
|
export const TELEGRAM_LOCK_KEY = "@llblab/pi-telegram";
|
|
19
19
|
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 5_000;
|
|
20
|
+
const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
|
|
21
|
+
const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
|
|
20
22
|
|
|
21
|
-
function
|
|
22
|
-
return
|
|
23
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
24
|
-
: join(homedir(), ".pi", "agent");
|
|
23
|
+
function getLocksPath(): string {
|
|
24
|
+
return resolveTelegramLocksPath();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the scoped lock key for the active Telegram profile.
|
|
29
|
+
* Default profile → @llblab/pi-telegram
|
|
30
|
+
* Named profile → @llblab/pi-telegram:<name>
|
|
31
|
+
*/
|
|
32
|
+
export function resolveTelegramLockKey(activeProfile?: string): string {
|
|
33
|
+
if (activeProfile) return `${TELEGRAM_LOCK_KEY}:${activeProfile}`;
|
|
34
|
+
return TELEGRAM_LOCK_KEY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TelegramActiveProfileGetter {
|
|
38
|
+
getActiveProfileName: () => string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createTelegramLockKeyResolver(
|
|
42
|
+
activeProfile: TelegramActiveProfileGetter,
|
|
43
|
+
): () => string {
|
|
44
|
+
return function getTelegramLockKey() {
|
|
45
|
+
return resolveTelegramLockKey(activeProfile.getActiveProfileName());
|
|
46
|
+
};
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
export interface TelegramLockEntry {
|
|
@@ -81,7 +99,7 @@ export interface TelegramLockContextStore<
|
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
export interface TelegramLockRuntimeOptions {
|
|
84
|
-
key?: string;
|
|
102
|
+
key?: string | (() => string | undefined);
|
|
85
103
|
locksPath?: string;
|
|
86
104
|
pid?: number;
|
|
87
105
|
isProcessAlive?: (pid: number) => boolean;
|
|
@@ -104,23 +122,46 @@ export function readLocks(path = getLocksPath()): Record<string, unknown> {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
function isRetryableLockWriteError(error: unknown): boolean {
|
|
126
|
+
const code = (error as { code?: unknown })?.code;
|
|
127
|
+
return code === "EPERM" || code === "EBUSY" || code === "EACCES";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sleepSync(ms: number): void {
|
|
131
|
+
const buffer = new SharedArrayBuffer(4);
|
|
132
|
+
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
export function writeLocks(path: string, locks: Record<string, unknown>): void {
|
|
108
136
|
mkdirSync(dirname(path), { recursive: true });
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
mode: 0o600,
|
|
114
|
-
});
|
|
115
|
-
renameSync(tempPath, path);
|
|
116
|
-
} catch (error) {
|
|
137
|
+
const payload = `${JSON.stringify(locks, null, 2)}\n`;
|
|
138
|
+
let lastError: unknown;
|
|
139
|
+
for (let attempt = 0; attempt < TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS; attempt += 1) {
|
|
140
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${attempt}.tmp`;
|
|
117
141
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
142
|
+
writeFileSync(tempPath, payload, {
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
mode: 0o600,
|
|
145
|
+
});
|
|
146
|
+
renameSync(tempPath, path);
|
|
147
|
+
return;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
lastError = error;
|
|
150
|
+
try {
|
|
151
|
+
unlinkSync(tempPath);
|
|
152
|
+
} catch {
|
|
153
|
+
/* best effort */
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
!isRetryableLockWriteError(error) ||
|
|
157
|
+
attempt === TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS - 1
|
|
158
|
+
) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
sleepSync(TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS * (attempt + 1));
|
|
121
162
|
}
|
|
122
|
-
throw error;
|
|
123
163
|
}
|
|
164
|
+
throw lastError;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
export function parseTelegramLockEntry(
|
|
@@ -238,10 +279,18 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
238
279
|
nowMs: getNowMs(),
|
|
239
280
|
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
240
281
|
});
|
|
241
|
-
const
|
|
282
|
+
const resolveEffectiveKey = (): string => {
|
|
283
|
+
if (typeof key === "function") return key() || TELEGRAM_LOCK_KEY;
|
|
284
|
+
return key;
|
|
285
|
+
};
|
|
286
|
+
const readLock = () => {
|
|
287
|
+
const effectiveKey = resolveEffectiveKey();
|
|
288
|
+
return parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
289
|
+
};
|
|
242
290
|
const writeLock = (lock: TelegramLockEntry) => {
|
|
291
|
+
const effectiveKey = resolveEffectiveKey();
|
|
243
292
|
const locks = readLocks(locksPath);
|
|
244
|
-
locks[
|
|
293
|
+
locks[effectiveKey] = lock;
|
|
245
294
|
writeLocks(locksPath, locks);
|
|
246
295
|
};
|
|
247
296
|
return {
|
|
@@ -262,7 +311,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
262
311
|
const state = getLockState(readLock(), pid, isAlive, stateOptions());
|
|
263
312
|
if (state.kind === "active-here" || state.kind === "stale") {
|
|
264
313
|
const locks = readLocks(locksPath);
|
|
265
|
-
delete locks[
|
|
314
|
+
delete locks[resolveEffectiveKey()];
|
|
266
315
|
writeLocks(locksPath, locks);
|
|
267
316
|
}
|
|
268
317
|
return state;
|
|
@@ -411,7 +460,11 @@ export function createTelegramLockedPollingRuntime<
|
|
|
411
460
|
const owner = snapshotLockContext(ctx);
|
|
412
461
|
stopOwnershipWatcher();
|
|
413
462
|
ownershipInterval = setInterval(() => {
|
|
414
|
-
|
|
463
|
+
try {
|
|
464
|
+
if (deps.lock.refresh(owner)) return;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "refresh" });
|
|
467
|
+
}
|
|
415
468
|
stopAfterOwnershipLoss();
|
|
416
469
|
}, ownershipCheckMs);
|
|
417
470
|
ownershipInterval.unref?.();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram
|
|
2
|
+
* Telegram diagnostics logs
|
|
3
3
|
* Zones: telegram diagnostics, filesystem, session observability
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
appendFile,
|
|
14
14
|
} from "node:fs";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
resolveAgentDir,
|
|
18
|
+
resolveTelegramProfileTempFilePath,
|
|
19
|
+
} from "./paths.ts";
|
|
20
|
+
|
|
21
|
+
export type TelegramLogPathInput = string | (() => string);
|
|
17
22
|
|
|
18
23
|
export interface TelegramRuntimeJsonlEvent {
|
|
19
24
|
at: number;
|
|
@@ -23,14 +28,14 @@ export interface TelegramRuntimeJsonlEvent {
|
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface TelegramRuntimeJsonlLogOptions {
|
|
26
|
-
path?:
|
|
27
|
-
previousPath?:
|
|
31
|
+
path?: TelegramLogPathInput;
|
|
32
|
+
previousPath?: TelegramLogPathInput;
|
|
28
33
|
maxBytes?: number;
|
|
29
34
|
getNowMs?: () => number;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface TelegramRuntimeJsonlLog {
|
|
33
|
-
|
|
38
|
+
getPath: () => string;
|
|
34
39
|
reset: (reason: string, scope?: Record<string, unknown>) => void;
|
|
35
40
|
resetIfScopeChanged: (
|
|
36
41
|
scopeKey: string,
|
|
@@ -42,21 +47,36 @@ export interface TelegramRuntimeJsonlLog {
|
|
|
42
47
|
|
|
43
48
|
const DEFAULT_MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
44
49
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
export function getTelegramRuntimeLogPath(
|
|
51
|
+
agentDir = resolveAgentDir(),
|
|
52
|
+
profileName?: string,
|
|
53
|
+
): string {
|
|
54
|
+
return resolveTelegramProfileTempFilePath(
|
|
55
|
+
"logs",
|
|
56
|
+
"jsonl",
|
|
57
|
+
agentDir,
|
|
58
|
+
profileName,
|
|
59
|
+
);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
export function
|
|
52
|
-
|
|
62
|
+
export function getTelegramPreviousRuntimeLogPath(
|
|
63
|
+
agentDir = resolveAgentDir(),
|
|
64
|
+
profileName?: string,
|
|
65
|
+
): string {
|
|
66
|
+
return resolveTelegramProfileTempFilePath(
|
|
67
|
+
"logs",
|
|
68
|
+
"previous.jsonl",
|
|
69
|
+
agentDir,
|
|
70
|
+
profileName,
|
|
71
|
+
);
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
function safeJsonLine(value: unknown): string {
|
|
56
75
|
return JSON.stringify(value, (_key, item) => {
|
|
57
76
|
if (item instanceof Error) return item.message;
|
|
58
77
|
if (typeof item === "bigint") return item.toString();
|
|
59
|
-
if (typeof item === "function" || typeof item === "symbol")
|
|
78
|
+
if (typeof item === "function" || typeof item === "symbol")
|
|
79
|
+
return undefined;
|
|
60
80
|
return item;
|
|
61
81
|
});
|
|
62
82
|
}
|
|
@@ -64,27 +84,35 @@ function safeJsonLine(value: unknown): string {
|
|
|
64
84
|
export function createTelegramRuntimeJsonlLog(
|
|
65
85
|
options: TelegramRuntimeJsonlLogOptions = {},
|
|
66
86
|
): TelegramRuntimeJsonlLog {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
const resolvePath = () =>
|
|
88
|
+
typeof options.path === "function"
|
|
89
|
+
? options.path()
|
|
90
|
+
: (options.path ?? getTelegramRuntimeLogPath());
|
|
91
|
+
const resolvePreviousPath = () => {
|
|
92
|
+
if (typeof options.previousPath === "function") return options.previousPath();
|
|
93
|
+
if (options.previousPath) return options.previousPath;
|
|
94
|
+
return resolvePath().replace(/\.jsonl$/u, ".previous.jsonl");
|
|
95
|
+
};
|
|
70
96
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
71
97
|
const getNowMs = options.getNowMs ?? Date.now;
|
|
72
|
-
|
|
98
|
+
const scopeKeys = new Map<string, string | undefined>();
|
|
73
99
|
let pending: Promise<void> = Promise.resolve();
|
|
74
100
|
|
|
75
|
-
const ensureParent = () => {
|
|
101
|
+
const ensureParent = (path: string) => {
|
|
76
102
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
77
103
|
};
|
|
78
104
|
|
|
79
|
-
const preserveCurrentLog = () => {
|
|
105
|
+
const preserveCurrentLog = (path: string, previousPath: string) => {
|
|
80
106
|
if (!existsSync(path)) return;
|
|
81
107
|
mkdirSync(dirname(previousPath), { recursive: true, mode: 0o700 });
|
|
82
108
|
copyFileSync(path, previousPath);
|
|
83
109
|
};
|
|
84
110
|
|
|
85
111
|
const writeReset = (reason: string, scope?: Record<string, unknown>) => {
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
const path = resolvePath();
|
|
113
|
+
const previousPath = resolvePreviousPath();
|
|
114
|
+
ensureParent(path);
|
|
115
|
+
preserveCurrentLog(path, previousPath);
|
|
88
116
|
writeFileSync(
|
|
89
117
|
path,
|
|
90
118
|
safeJsonLine({
|
|
@@ -102,7 +130,8 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
102
130
|
pending = pending
|
|
103
131
|
.catch(() => undefined)
|
|
104
132
|
.then(async () => {
|
|
105
|
-
|
|
133
|
+
const path = resolvePath();
|
|
134
|
+
ensureParent(path);
|
|
106
135
|
if (existsSync(path) && statSync(path).size > maxBytes) {
|
|
107
136
|
writeReset("max-bytes", { maxBytes });
|
|
108
137
|
}
|
|
@@ -116,9 +145,10 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
116
145
|
};
|
|
117
146
|
|
|
118
147
|
return {
|
|
119
|
-
|
|
148
|
+
getPath: resolvePath,
|
|
120
149
|
reset(reason, scope) {
|
|
121
|
-
|
|
150
|
+
const path = resolvePath();
|
|
151
|
+
scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
|
|
122
152
|
try {
|
|
123
153
|
writeReset(reason, scope);
|
|
124
154
|
} catch {
|
|
@@ -126,8 +156,9 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
126
156
|
}
|
|
127
157
|
},
|
|
128
158
|
resetIfScopeChanged(nextScopeKey, reason, scope) {
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
const path = resolvePath();
|
|
160
|
+
if (scopeKeys.get(path) === nextScopeKey) return;
|
|
161
|
+
scopeKeys.set(path, nextScopeKey);
|
|
131
162
|
try {
|
|
132
163
|
writeReset(reason, scope);
|
|
133
164
|
} catch {
|
package/lib/media.ts
CHANGED
|
@@ -33,11 +33,36 @@ export interface TelegramRichMessage {
|
|
|
33
33
|
blocks?: unknown[];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface TelegramMessageUser {
|
|
37
|
+
id?: number;
|
|
38
|
+
is_bot?: boolean;
|
|
39
|
+
first_name?: string;
|
|
40
|
+
last_name?: string;
|
|
41
|
+
username?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TelegramMessageForwardOrigin {
|
|
45
|
+
type?: string;
|
|
46
|
+
sender_user?: TelegramMessageUser;
|
|
47
|
+
sender_user_name?: string;
|
|
48
|
+
sender_chat?: { title?: string; username?: string; id?: number };
|
|
49
|
+
chat?: { title?: string; username?: string; id?: number };
|
|
50
|
+
author_signature?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
export interface TelegramReplyToMessage {
|
|
37
54
|
message_id?: number;
|
|
55
|
+
from?: TelegramMessageUser;
|
|
38
56
|
text?: string;
|
|
39
57
|
caption?: string;
|
|
40
58
|
rich_message?: TelegramRichMessage;
|
|
59
|
+
photo?: TelegramPhotoSize[];
|
|
60
|
+
document?: TelegramDocument;
|
|
61
|
+
video?: TelegramVideo;
|
|
62
|
+
audio?: TelegramAudio;
|
|
63
|
+
voice?: TelegramVoice;
|
|
64
|
+
animation?: TelegramAnimation;
|
|
65
|
+
sticker?: TelegramSticker;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
export interface TelegramSticker {
|
|
@@ -46,6 +71,10 @@ export interface TelegramSticker {
|
|
|
46
71
|
|
|
47
72
|
export interface TelegramMediaMessage {
|
|
48
73
|
message_id: number;
|
|
74
|
+
from?: TelegramMessageUser;
|
|
75
|
+
forward_origin?: TelegramMessageForwardOrigin;
|
|
76
|
+
forward_from?: TelegramMessageUser;
|
|
77
|
+
forward_sender_name?: string;
|
|
49
78
|
text?: string;
|
|
50
79
|
caption?: string;
|
|
51
80
|
rich_message?: TelegramRichMessage;
|
|
@@ -277,6 +306,39 @@ function truncateTelegramReplyContextText(text: string): string {
|
|
|
277
306
|
return `${text.slice(0, TELEGRAM_REPLY_CONTEXT_MAX_LENGTH).trimEnd()}…`;
|
|
278
307
|
}
|
|
279
308
|
|
|
309
|
+
function formatTelegramUser(user: TelegramMessageUser | undefined): string | undefined {
|
|
310
|
+
if (!user) return undefined;
|
|
311
|
+
if (user.username) return user.username;
|
|
312
|
+
if (typeof user.id === "number") return String(user.id);
|
|
313
|
+
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
|
314
|
+
return name || undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatTelegramForwardOriginIdentifier(
|
|
318
|
+
message: TelegramMediaMessage,
|
|
319
|
+
): string | undefined {
|
|
320
|
+
const origin = message.forward_origin;
|
|
321
|
+
const user = origin?.sender_user ?? message.forward_from;
|
|
322
|
+
if (user?.username) return user.username;
|
|
323
|
+
if (typeof user?.id === "number") return String(user.id);
|
|
324
|
+
const chat = origin?.sender_chat ?? origin?.chat;
|
|
325
|
+
if (chat?.username) return chat.username;
|
|
326
|
+
if (typeof chat?.id === "number") return String(chat.id);
|
|
327
|
+
return origin?.sender_user_name ?? message.forward_sender_name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function extractTelegramForwardContextText(
|
|
331
|
+
message: TelegramMediaMessage,
|
|
332
|
+
allowedUserId?: number,
|
|
333
|
+
): string {
|
|
334
|
+
const originUser = message.forward_origin?.sender_user ?? message.forward_from;
|
|
335
|
+
const isOwnerOrigin =
|
|
336
|
+
typeof allowedUserId === "number" && originUser?.id === allowedUserId;
|
|
337
|
+
const origin = formatTelegramForwardOriginIdentifier(message);
|
|
338
|
+
if (!origin || isOwnerOrigin) return "";
|
|
339
|
+
return `from: ${origin}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
280
342
|
export function extractTelegramReplyContextText(
|
|
281
343
|
message: TelegramMediaMessage,
|
|
282
344
|
): string {
|
|
@@ -289,13 +351,44 @@ export function extractTelegramReplyContextText(
|
|
|
289
351
|
return quoted ? truncateTelegramReplyContextText(quoted) : "";
|
|
290
352
|
}
|
|
291
353
|
|
|
354
|
+
export function buildTelegramReplyContextBlock(
|
|
355
|
+
message: TelegramMediaMessage,
|
|
356
|
+
replyFiles: Pick<DownloadedTelegramFile, "path">[] = [],
|
|
357
|
+
): string {
|
|
358
|
+
const from = formatTelegramUser(message.reply_to_message?.from);
|
|
359
|
+
const header = from ? `[reply|from:${from}]` : "[reply]";
|
|
360
|
+
const text = extractTelegramReplyContextText(message);
|
|
361
|
+
const dirs = [...new Set(replyFiles.map((file) => dirname(file.path)))];
|
|
362
|
+
const sameDir = dirs.length === 1;
|
|
363
|
+
const attachmentHeader = sameDir
|
|
364
|
+
? `[attachments${from ? `|from:${from}` : ""}] ${dirs[0]}`
|
|
365
|
+
: `[attachments${from ? `|from:${from}` : ""}]`;
|
|
366
|
+
const fileLines = sameDir
|
|
367
|
+
? replyFiles.map((file) => `- /${basename(file.path)}`)
|
|
368
|
+
: replyFiles.map((file) => `- ${file.path}`);
|
|
369
|
+
const replyBlock = text ? `${header} ${text}` : header;
|
|
370
|
+
if (fileLines.length > 0) {
|
|
371
|
+
return `${replyBlock}\n\n${attachmentHeader}\n${fileLines.join("\n")}`;
|
|
372
|
+
}
|
|
373
|
+
if (text) return replyBlock;
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
|
|
292
377
|
export function appendTelegramReplyContext(
|
|
293
378
|
text: string,
|
|
294
379
|
replyContext: string,
|
|
295
380
|
): string {
|
|
296
381
|
if (!replyContext) return text;
|
|
297
|
-
|
|
298
|
-
|
|
382
|
+
return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function appendTelegramForwardContext(
|
|
386
|
+
text: string,
|
|
387
|
+
forwardContext: string,
|
|
388
|
+
): string {
|
|
389
|
+
if (!forwardContext) return text;
|
|
390
|
+
const forwardBlock = `[forward|${forwardContext.replace(/:\s+/g, ":")}]`;
|
|
391
|
+
return text ? `\n\n${forwardBlock} ${text}` : `\n\n${forwardBlock}`;
|
|
299
392
|
}
|
|
300
393
|
|
|
301
394
|
export function extractTelegramMessagePromptText(
|
|
@@ -303,7 +396,7 @@ export function extractTelegramMessagePromptText(
|
|
|
303
396
|
): string {
|
|
304
397
|
return appendTelegramReplyContext(
|
|
305
398
|
extractTelegramMessageText(message),
|
|
306
|
-
|
|
399
|
+
buildTelegramReplyContextBlock(message),
|
|
307
400
|
);
|
|
308
401
|
}
|
|
309
402
|
|
|
@@ -321,7 +414,7 @@ export function extractTelegramMessagesPromptText(
|
|
|
321
414
|
if (!firstMessage) return text;
|
|
322
415
|
return appendTelegramReplyContext(
|
|
323
416
|
text,
|
|
324
|
-
|
|
417
|
+
buildTelegramReplyContextBlock(firstMessage),
|
|
325
418
|
);
|
|
326
419
|
}
|
|
327
420
|
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -192,7 +192,7 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
192
192
|
replyToMessageId: options.replyToMessageId,
|
|
193
193
|
sourceMessageIds: [options.replyToMessageId],
|
|
194
194
|
queueOrder: options.queueOrder,
|
|
195
|
-
queueLane: "
|
|
195
|
+
queueLane: "priority",
|
|
196
196
|
laneOrder: options.queueOrder,
|
|
197
197
|
queuedAttachments: [],
|
|
198
198
|
content: [{ type: "text", text: prompt }],
|
package/lib/outbound.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { resolveTelegramTempDir } from "./paths.ts";
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
planTelegramButtonReply,
|
|
@@ -66,8 +67,7 @@ export function recordTelegramRuntimeEvent(
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export type TelegramOutboundCommandTemplateConfig =
|
|
69
|
-
|
|
|
70
|
-
| CommandTemplateObjectConfig;
|
|
70
|
+
string | CommandTemplateObjectConfig;
|
|
71
71
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
72
72
|
type?: string;
|
|
73
73
|
match?: string | string[];
|
|
@@ -448,10 +448,7 @@ function getVoiceReplyTemplateValues(
|
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
function getDefaultTelegramVoiceTempDir(): string {
|
|
451
|
-
|
|
452
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
453
|
-
: join(homedir(), ".pi", "agent");
|
|
454
|
-
return join(agentDir, "tmp", "telegram");
|
|
451
|
+
return resolveTelegramTempDir();
|
|
455
452
|
}
|
|
456
453
|
|
|
457
454
|
async function generateTelegramVoiceReplyFileWithHandler(
|
package/lib/paths.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bridge path resolution for Pi-compatible runtimes
|
|
3
|
+
* Zones: telemetry paths, filesystem, runtime identity
|
|
4
|
+
* Owns agent-dir detection and extension-local path derivation
|
|
5
|
+
*
|
|
6
|
+
* This domain is pure/path-only: it resolves directories and file paths
|
|
7
|
+
* from environment and runtime identity. It does not read config, manage
|
|
8
|
+
* state, or import broader Telegram domains.
|
|
9
|
+
*/
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
export interface TelegramAgentDirResolutionInput {
|
|
14
|
+
env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
|
|
15
|
+
execPath?: string;
|
|
16
|
+
argv?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the agent data directory for the current Pi-compatible runtime.
|
|
21
|
+
*
|
|
22
|
+
* Precedence:
|
|
23
|
+
* 1. `PI_CODING_AGENT_DIR` env variable, when explicitly set.
|
|
24
|
+
* 2. Detect Pi-compatible runtime identity from the executable or argv[1]
|
|
25
|
+
* (e.g. OMP vs standard Pi agent).
|
|
26
|
+
* 3. Fallback: `~/.pi/agent`.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveAgentDir(
|
|
29
|
+
input: TelegramAgentDirResolutionInput = {},
|
|
30
|
+
): string {
|
|
31
|
+
const env = input.env ?? process.env;
|
|
32
|
+
if (env.PI_CODING_AGENT_DIR) return resolve(env.PI_CODING_AGENT_DIR);
|
|
33
|
+
const execPath = input.execPath ?? process.execPath;
|
|
34
|
+
const argv = input.argv ?? process.argv;
|
|
35
|
+
const execBasename = execPath.toLowerCase().split(/[\\/]/u).pop() ?? "";
|
|
36
|
+
const argv1Last = (argv[1] ?? "").toLowerCase().split(/[\\/]/u).pop() ?? "";
|
|
37
|
+
if (execBasename.startsWith("omp") || argv1Last.startsWith("omp")) {
|
|
38
|
+
return join(homedir(), ".omp", "agent");
|
|
39
|
+
}
|
|
40
|
+
return join(homedir(), ".pi", "agent");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
44
|
+
export function resolveTelegramConfigPath(): string {
|
|
45
|
+
return join(resolveAgentDir(), "telegram.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Telegram singleton lock file (<agentDir>/locks.json). */
|
|
49
|
+
export function resolveTelegramLocksPath(): string {
|
|
50
|
+
return join(resolveAgentDir(), "locks.json");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
|
|
54
|
+
export function resolveTelegramTempDir(agentDir = resolveAgentDir()): string {
|
|
55
|
+
return join(agentDir, "tmp", "telegram");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getTelegramProfilePathSuffix(profileName?: string): string {
|
|
59
|
+
return profileName ? `.${profileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}` : "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveTelegramProfileTempFilePath(
|
|
63
|
+
baseName: string,
|
|
64
|
+
extension: string,
|
|
65
|
+
agentDir = resolveAgentDir(),
|
|
66
|
+
profileName?: string,
|
|
67
|
+
): string {
|
|
68
|
+
return join(
|
|
69
|
+
resolveTelegramTempDir(agentDir),
|
|
70
|
+
`${baseName}${getTelegramProfilePathSuffix(profileName)}.${extension}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
75
|
+
export function resolveTelegramRuntimeLogPath(): string {
|
|
76
|
+
return resolveTelegramProfileTempFilePath("logs", "jsonl");
|
|
77
|
+
}
|
package/lib/prompt-templates.ts
CHANGED
|
@@ -116,11 +116,13 @@ export function getTelegramPromptTemplateCommands(
|
|
|
116
116
|
if (!telegramCommand) continue;
|
|
117
117
|
if (reservedNames.has(telegramCommand)) continue;
|
|
118
118
|
if (seen.has(telegramCommand)) continue;
|
|
119
|
+
const sourcePath = command.sourceInfo?.path;
|
|
120
|
+
if (!sourcePath) continue;
|
|
119
121
|
seen.add(telegramCommand);
|
|
120
122
|
promptCommands.push({
|
|
121
123
|
command: telegramCommand,
|
|
122
124
|
description: command.description,
|
|
123
|
-
path:
|
|
125
|
+
path: sourcePath,
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
return promptCommands.sort((a, b) => a.command.localeCompare(b.command));
|
package/lib/prompts.ts
CHANGED
|
@@ -15,7 +15,7 @@ Telegram bridge available. Do not use it from local/TUI prompts unless explicitl
|
|
|
15
15
|
|
|
16
16
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
17
17
|
|
|
18
|
-
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help
|
|
18
|
+
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
19
|
|
|
20
20
|
const TELEGRAM_HELP_TEXT = `--- TELEGRAM BRIDGE HELP ---
|
|
21
21
|
|
package/lib/queue.ts
CHANGED
|
@@ -273,6 +273,37 @@ export function appendTelegramQueueItem<
|
|
|
273
273
|
return [...items, item];
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
|
|
277
|
+
return item.content
|
|
278
|
+
.filter((entry): entry is TelegramPromptTextContent => entry.type === "text")
|
|
279
|
+
.map((entry) => entry.text)
|
|
280
|
+
.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isDuplicateTelegramPromptTurn(
|
|
284
|
+
left: PendingTelegramTurn,
|
|
285
|
+
right: PendingTelegramTurn,
|
|
286
|
+
): boolean {
|
|
287
|
+
return (
|
|
288
|
+
left.chatId === right.chatId &&
|
|
289
|
+
left.target?.threadId === right.target?.threadId &&
|
|
290
|
+
left.replyToMessageId === right.replyToMessageId &&
|
|
291
|
+
getTelegramPromptTextSignature(left) === getTelegramPromptTextSignature(right)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function appendTelegramPromptTurnOnce<TContext = unknown>(
|
|
296
|
+
items: TelegramQueueItem<TContext>[],
|
|
297
|
+
turn: PendingTelegramTurn,
|
|
298
|
+
): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
|
|
299
|
+
assertTelegramQueueItemAdmissionValid(turn);
|
|
300
|
+
const duplicate = items.some(
|
|
301
|
+
(item) => isPendingTelegramTurn(item) && isDuplicateTelegramPromptTurn(item, turn),
|
|
302
|
+
);
|
|
303
|
+
if (duplicate) return { items, appended: false };
|
|
304
|
+
return { items: [...items, turn], appended: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
276
307
|
export function compareTelegramQueueItems<TContext = unknown>(
|
|
277
308
|
left: TelegramQueueItem<TContext>,
|
|
278
309
|
right: TelegramQueueItem<TContext>,
|
|
@@ -2051,7 +2082,7 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
|
2051
2082
|
}
|
|
2052
2083
|
deps.onPromptDispatchStart(plan.item.chatId);
|
|
2053
2084
|
try {
|
|
2054
|
-
deps.sendUserMessage(plan.item.content
|
|
2085
|
+
deps.sendUserMessage(plan.item.content);
|
|
2055
2086
|
} catch (error) {
|
|
2056
2087
|
const message = getTelegramQueueErrorMessage(error);
|
|
2057
2088
|
deps.onPromptDispatchFailure(message);
|