@llblab/pi-telegram 0.19.2 → 0.20.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 +5 -3
- package/BACKLOG.md +15 -2
- package/CHANGELOG.md +10 -0
- package/README.md +57 -57
- package/docs/architecture.md +4 -1
- package/docs/public-api.md +1 -1
- package/index.ts +67 -12
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +27 -8
- package/lib/bus-leader.ts +26 -42
- package/lib/bus.ts +19 -22
- 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 +2 -2
- /package/{banner.png → screenshot.png} +0 -0
package/lib/config.ts
CHANGED
|
@@ -6,27 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
9
|
+
import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
|
|
11
10
|
|
|
12
11
|
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
13
12
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
14
13
|
|
|
15
14
|
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
16
15
|
|
|
17
|
-
function getAgentDir(): string {
|
|
18
|
-
return process.env.PI_CODING_AGENT_DIR
|
|
19
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
20
|
-
: join(homedir(), ".pi", "agent");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function getConfigPath(): string {
|
|
24
|
-
return
|
|
17
|
+
return resolveTelegramConfigPath();
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export type TelegramOutboundCommandTemplateConfig =
|
|
28
|
-
|
|
|
29
|
-
| CommandTemplateObjectConfig;
|
|
21
|
+
string | CommandTemplateObjectConfig;
|
|
30
22
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
31
23
|
type?: string;
|
|
32
24
|
match?: string | string[];
|
|
@@ -75,12 +67,88 @@ export interface TelegramConfig {
|
|
|
75
67
|
sendTranscript?: boolean;
|
|
76
68
|
};
|
|
77
69
|
time?: TelegramTimeConfig;
|
|
70
|
+
/** Named bot/session profiles (e.g. "work", "omp"). */
|
|
71
|
+
profiles?: Record<string, TelegramBotProfile>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Per-profile bot/session identity fields.
|
|
76
|
+
* Stored under `profiles.<name>` in telegram.json.
|
|
77
|
+
* Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
|
|
78
|
+
* assistant, proactivePush) stay at the top level.
|
|
79
|
+
*/
|
|
80
|
+
export interface TelegramBotProfile {
|
|
81
|
+
botToken: string;
|
|
82
|
+
botUsername?: string;
|
|
83
|
+
botId?: number;
|
|
84
|
+
allowedUserId?: number;
|
|
85
|
+
lastUpdateId?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Profile names must be lowercase letters, digits, hyphens, underscores; max 32 chars. */
|
|
89
|
+
const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
90
|
+
const TELEGRAM_RESERVED_PROFILE_NAMES: ReadonlySet<string> = new Set([
|
|
91
|
+
"default",
|
|
92
|
+
"main",
|
|
93
|
+
"active",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
export function isValidTelegramProfileName(name: string): boolean {
|
|
97
|
+
return (
|
|
98
|
+
TELEGRAM_PROFILE_NAME_PATTERN.test(name) &&
|
|
99
|
+
!TELEGRAM_RESERVED_PROFILE_NAMES.has(name)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the effective config for a named (or default) profile.
|
|
105
|
+
* Returns bot/session fields from the named profile, falling back to
|
|
106
|
+
* top-level fields for the default profile. Shared bridge settings
|
|
107
|
+
* always come from the top level.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveTelegramActiveProfile(
|
|
110
|
+
config: TelegramConfig,
|
|
111
|
+
profileName?: string,
|
|
112
|
+
): {
|
|
113
|
+
botToken?: string;
|
|
114
|
+
botUsername?: string;
|
|
115
|
+
botId?: number;
|
|
116
|
+
allowedUserId?: number;
|
|
117
|
+
lastUpdateId?: number;
|
|
118
|
+
} {
|
|
119
|
+
if (!profileName || !config.profiles?.[profileName]) {
|
|
120
|
+
return {
|
|
121
|
+
botToken: config.botToken,
|
|
122
|
+
botUsername: config.botUsername,
|
|
123
|
+
botId: config.botId,
|
|
124
|
+
allowedUserId: config.allowedUserId,
|
|
125
|
+
lastUpdateId: config.lastUpdateId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const profile = config.profiles[profileName];
|
|
129
|
+
return {
|
|
130
|
+
botToken: profile.botToken,
|
|
131
|
+
botUsername: profile.botUsername,
|
|
132
|
+
botId: profile.botId,
|
|
133
|
+
allowedUserId: profile.allowedUserId,
|
|
134
|
+
lastUpdateId: profile.lastUpdateId,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** List defined profile names. */
|
|
139
|
+
export function getTelegramProfileNames(
|
|
140
|
+
config: TelegramConfig,
|
|
141
|
+
): string[] {
|
|
142
|
+
return Object.keys(config.profiles ?? {}).sort();
|
|
78
143
|
}
|
|
79
144
|
|
|
80
145
|
export interface TelegramConfigStore {
|
|
81
146
|
get: () => TelegramConfig;
|
|
147
|
+
getStoredConfig: () => TelegramConfig;
|
|
82
148
|
set: (config: TelegramConfig) => void;
|
|
83
149
|
update: (mutate: (config: TelegramConfig) => void) => void;
|
|
150
|
+
activateProfile: (profileName: string | undefined) => boolean;
|
|
151
|
+
getActiveProfileName: () => string | undefined;
|
|
84
152
|
getBotToken: () => string | undefined;
|
|
85
153
|
hasBotToken: () => boolean;
|
|
86
154
|
getAllowedUserId: () => number | undefined;
|
|
@@ -211,23 +279,85 @@ export async function writeTelegramConfig(
|
|
|
211
279
|
await chmod(configPath, 0o600);
|
|
212
280
|
}
|
|
213
281
|
|
|
282
|
+
function getTelegramProfileFields(config: TelegramConfig): TelegramBotProfile | undefined {
|
|
283
|
+
const token = config.botToken?.trim();
|
|
284
|
+
if (!token) return undefined;
|
|
285
|
+
return {
|
|
286
|
+
botToken: token,
|
|
287
|
+
botUsername: config.botUsername,
|
|
288
|
+
botId: config.botId,
|
|
289
|
+
allowedUserId: config.allowedUserId,
|
|
290
|
+
lastUpdateId: config.lastUpdateId,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function applyTelegramProfile(
|
|
295
|
+
config: TelegramConfig,
|
|
296
|
+
profileName: string | undefined,
|
|
297
|
+
): TelegramConfig {
|
|
298
|
+
if (!profileName) return config;
|
|
299
|
+
const profile = config.profiles?.[profileName];
|
|
300
|
+
if (!profile) return config;
|
|
301
|
+
return {
|
|
302
|
+
...config,
|
|
303
|
+
botToken: profile.botToken,
|
|
304
|
+
botUsername: profile.botUsername,
|
|
305
|
+
botId: profile.botId,
|
|
306
|
+
allowedUserId: profile.allowedUserId,
|
|
307
|
+
lastUpdateId: profile.lastUpdateId,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function storeTelegramEffectiveConfig(
|
|
312
|
+
baseConfig: TelegramConfig,
|
|
313
|
+
nextConfig: TelegramConfig,
|
|
314
|
+
profileName: string | undefined,
|
|
315
|
+
): TelegramConfig {
|
|
316
|
+
if (!profileName) return nextConfig;
|
|
317
|
+
const profile = getTelegramProfileFields(nextConfig);
|
|
318
|
+
const profiles = { ...(baseConfig.profiles ?? {}) };
|
|
319
|
+
if (profile) profiles[profileName] = profile;
|
|
320
|
+
else delete profiles[profileName];
|
|
321
|
+
return {
|
|
322
|
+
...nextConfig,
|
|
323
|
+
botToken: baseConfig.botToken,
|
|
324
|
+
botUsername: baseConfig.botUsername,
|
|
325
|
+
botId: baseConfig.botId,
|
|
326
|
+
allowedUserId: baseConfig.allowedUserId,
|
|
327
|
+
lastUpdateId: baseConfig.lastUpdateId,
|
|
328
|
+
profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
214
332
|
export function createTelegramConfigStore(
|
|
215
333
|
options: TelegramConfigStoreOptions = {},
|
|
216
334
|
): TelegramConfigStore {
|
|
217
335
|
let config: TelegramConfig = options.initialConfig ?? {};
|
|
218
|
-
|
|
336
|
+
let activeProfileName: string | undefined;
|
|
337
|
+
const agentDir = options.agentDir ?? resolveAgentDir();
|
|
219
338
|
const configPath = options.configPath ?? getConfigPath();
|
|
339
|
+
const getEffectiveConfig = () => applyTelegramProfile(config, activeProfileName);
|
|
340
|
+
const setEffectiveConfig = (nextConfig: TelegramConfig) => {
|
|
341
|
+
config = storeTelegramEffectiveConfig(config, nextConfig, activeProfileName);
|
|
342
|
+
};
|
|
220
343
|
return {
|
|
221
|
-
get:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
},
|
|
344
|
+
get: getEffectiveConfig,
|
|
345
|
+
getStoredConfig: () => config,
|
|
346
|
+
set: setEffectiveConfig,
|
|
225
347
|
update: (mutate) => {
|
|
226
|
-
|
|
348
|
+
const nextConfig = getEffectiveConfig();
|
|
349
|
+
mutate(nextConfig);
|
|
350
|
+
setEffectiveConfig(nextConfig);
|
|
351
|
+
},
|
|
352
|
+
activateProfile: (profileName) => {
|
|
353
|
+
if (profileName && !config.profiles?.[profileName]) return false;
|
|
354
|
+
activeProfileName = profileName;
|
|
355
|
+
return true;
|
|
227
356
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
357
|
+
getActiveProfileName: () => activeProfileName,
|
|
358
|
+
getBotToken: () => getEffectiveConfig().botToken,
|
|
359
|
+
hasBotToken: () => !!getEffectiveConfig().botToken,
|
|
360
|
+
getAllowedUserId: () => getEffectiveConfig().allowedUserId,
|
|
231
361
|
getInboundHandlers: () => [
|
|
232
362
|
...(config.inboundHandlers ?? []),
|
|
233
363
|
...(config.attachmentHandlers ?? []),
|
|
@@ -235,7 +365,9 @@ export function createTelegramConfigStore(
|
|
|
235
365
|
getAttachmentHandlers: () => config.attachmentHandlers,
|
|
236
366
|
getOutboundHandlers: () => config.outboundHandlers,
|
|
237
367
|
setAllowedUserId: (userId) => {
|
|
238
|
-
|
|
368
|
+
const nextConfig = getEffectiveConfig();
|
|
369
|
+
nextConfig.allowedUserId = userId;
|
|
370
|
+
setEffectiveConfig(nextConfig);
|
|
239
371
|
},
|
|
240
372
|
load: async () => {
|
|
241
373
|
config = await readTelegramConfig(configPath, {
|
|
@@ -247,9 +379,18 @@ export function createTelegramConfigStore(
|
|
|
247
379
|
});
|
|
248
380
|
},
|
|
249
381
|
});
|
|
382
|
+
if (activeProfileName && !config.profiles?.[activeProfileName]) {
|
|
383
|
+
activeProfileName = undefined;
|
|
384
|
+
}
|
|
250
385
|
},
|
|
251
|
-
persist: async (nextConfig =
|
|
252
|
-
|
|
386
|
+
persist: async (nextConfig = getEffectiveConfig()) => {
|
|
387
|
+
const storedConfig = storeTelegramEffectiveConfig(
|
|
388
|
+
config,
|
|
389
|
+
nextConfig,
|
|
390
|
+
activeProfileName,
|
|
391
|
+
);
|
|
392
|
+
config = storedConfig;
|
|
393
|
+
await writeTelegramConfig(agentDir, configPath, storedConfig);
|
|
253
394
|
},
|
|
254
395
|
};
|
|
255
396
|
}
|
|
@@ -481,9 +622,7 @@ export function createTelegramConfigControls(
|
|
|
481
622
|
}
|
|
482
623
|
|
|
483
624
|
export type TelegramAuthorizationState =
|
|
484
|
-
|
|
485
|
-
| { kind: "allow" }
|
|
486
|
-
| { kind: "deny" };
|
|
625
|
+
{ kind: "pair"; userId: number } | { kind: "allow" } | { kind: "deny" };
|
|
487
626
|
|
|
488
627
|
export interface TelegramUserPairingDeps<TContext> {
|
|
489
628
|
allowedUserId?: number;
|
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 {
|