@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/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 { homedir } from "node:os";
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 join(getAgentDir(), "telegram.json");
17
+ return resolveTelegramConfigPath();
25
18
  }
26
19
 
27
20
  export type TelegramOutboundCommandTemplateConfig =
28
- | string
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
- const agentDir = options.agentDir ?? getAgentDir();
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: () => config,
222
- set: (nextConfig) => {
223
- config = nextConfig;
224
- },
344
+ get: getEffectiveConfig,
345
+ getStoredConfig: () => config,
346
+ set: setEffectiveConfig,
225
347
  update: (mutate) => {
226
- mutate(config);
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
- getBotToken: () => config.botToken,
229
- hasBotToken: () => !!config.botToken,
230
- getAllowedUserId: () => config.allowedUserId,
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
- config.allowedUserId = userId;
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 = config) => {
252
- await writeTelegramConfig(agentDir, configPath, nextConfig);
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
- | { kind: "pair"; userId: number }
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 { homedir } from "node:os";
16
- import { dirname, join, resolve } from "node:path";
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 getAgentDir(): string {
22
- return process.env.PI_CODING_AGENT_DIR
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
- function getLocksPath(): string {
28
- return join(getAgentDir(), "locks.json");
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 tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
110
- try {
111
- writeFileSync(tempPath, `${JSON.stringify(locks, null, 2)}\n`, {
112
- encoding: "utf8",
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
- unlinkSync(tempPath);
119
- } catch {
120
- /* best effort */
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 readLock = () => parseTelegramLockEntry(readLocks(locksPath)[key]);
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[key] = lock;
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[key];
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
- if (deps.lock.refresh(owner)) return;
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 runtime JSONL diagnostics log
2
+ * Telegram diagnostics logs
3
3
  * Zones: telegram diagnostics, filesystem, session observability
4
- * Owns session-local append-only runtime evidence for debugging without becoming routing state
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 { homedir } from "node:os";
16
- import { dirname, join, resolve } from "node:path";
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?: string;
27
- previousPath?: string;
31
+ path?: TelegramLogPathInput;
32
+ previousPath?: TelegramLogPathInput;
28
33
  maxBytes?: number;
29
34
  getNowMs?: () => number;
30
35
  }
31
36
 
32
37
  export interface TelegramRuntimeJsonlLog {
33
- path: string;
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 getAgentDir(): string {
46
- return process.env.PI_CODING_AGENT_DIR
47
- ? resolve(process.env.PI_CODING_AGENT_DIR)
48
- : join(homedir(), ".pi", "agent");
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 getTelegramRuntimeLogPath(agentDir = getAgentDir()): string {
52
- return join(agentDir, "tmp", "telegram", "logs.jsonl");
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") return undefined;
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 path = options.path ?? getTelegramRuntimeLogPath();
68
- const previousPath =
69
- options.previousPath ?? path.replace(/\.jsonl$/u, ".previous.jsonl");
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
- let scopeKey: string | undefined;
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
- ensureParent();
87
- preserveCurrentLog();
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
- ensureParent();
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
- path,
148
+ getPath: resolvePath,
120
149
  reset(reason, scope) {
121
- scopeKey = scope ? safeJsonLine(scope) : undefined;
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
- if (scopeKey === nextScopeKey) return;
130
- scopeKey = nextScopeKey;
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 {