@llblab/pi-telegram 0.52.2 → 0.53.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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/dist/lib/locks.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Zones: telegram ownership, filesystem, transport authority
|
|
4
4
|
* Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
|
|
5
5
|
*/
|
|
6
|
-
import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
7
|
+
import { chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
8
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
-
import {
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
11
10
|
import { isTelegramSessionPollingJournalPath, resolveTelegramOwnersPath, resolveTelegramSessionPollingJournalPath, resolveTelegramUpdateJournalPathForProfile, } from "./paths.js";
|
|
11
|
+
import { isProcessAlive } from "./process-identity.js";
|
|
12
|
+
import { isWireRecord as runtimeStateRecord } from "./wire.js";
|
|
12
13
|
export const TELEGRAM_LOCK_KEY = "default";
|
|
13
14
|
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
14
15
|
export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
|
|
@@ -56,7 +57,9 @@ export function createTelegramOwnedStateAuthorityCapture(lock, session) {
|
|
|
56
57
|
const epoch = lock.getOwnedLeaderEpoch();
|
|
57
58
|
if (epoch === undefined)
|
|
58
59
|
return undefined;
|
|
59
|
-
const current = () => session.isCurrent(ctx, generation) &&
|
|
60
|
+
const current = () => session.isCurrent(ctx, generation) &&
|
|
61
|
+
lock.owns(ctx) &&
|
|
62
|
+
lock.getOwnedLeaderEpoch() === epoch;
|
|
60
63
|
return current() ? current : undefined;
|
|
61
64
|
};
|
|
62
65
|
}
|
|
@@ -71,16 +74,20 @@ export function createTelegramLeaderJournalPathResolver(deps) {
|
|
|
71
74
|
if (existsSync(root))
|
|
72
75
|
return root;
|
|
73
76
|
const sessionId = deps.getSessionId();
|
|
74
|
-
return sessionId === undefined
|
|
77
|
+
return sessionId === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
|
|
75
80
|
};
|
|
76
81
|
return {
|
|
77
82
|
createJournalPath: () => createOwn(),
|
|
78
83
|
resolve(profileName) {
|
|
79
84
|
const named = deps.getNamedJournalPath();
|
|
80
|
-
if (named &&
|
|
81
|
-
named
|
|
85
|
+
if (named &&
|
|
86
|
+
(isTelegramSessionPollingJournalPath(named) ||
|
|
87
|
+
named === resolveTelegramUpdateJournalPathForProfile(profileName)))
|
|
82
88
|
return named;
|
|
83
|
-
return createOwn(profileName) ??
|
|
89
|
+
return (createOwn(profileName) ??
|
|
90
|
+
resolveTelegramUpdateJournalPathForProfile(profileName));
|
|
84
91
|
},
|
|
85
92
|
};
|
|
86
93
|
}
|
|
@@ -128,6 +135,93 @@ function sleepSync(ms) {
|
|
|
128
135
|
const buffer = new SharedArrayBuffer(4);
|
|
129
136
|
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
130
137
|
}
|
|
138
|
+
/** Typed private-file refusal; owners map the failure into their own error vocabulary. */
|
|
139
|
+
export class TelegramPrivateFileError extends Error {
|
|
140
|
+
failure;
|
|
141
|
+
constructor(failure, message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.failure = failure;
|
|
144
|
+
this.name = "TelegramPrivateFileError";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read flags for private runtime files. Where the platform offers them, no-follow and non-blocking opens refuse a
|
|
149
|
+
* swapped link or FIFO at open time. Windows has neither flag and no FIFOs in the file namespace; there the
|
|
150
|
+
* lstat-before/fstat-after identity binding every strict reader performs is the guard, so a link swapped in after
|
|
151
|
+
* inspection opens a different file and is refused as changed.
|
|
152
|
+
*/
|
|
153
|
+
export const TELEGRAM_STRICT_READ_FLAGS = constants.O_RDONLY |
|
|
154
|
+
(constants.O_NOFOLLOW ?? 0) |
|
|
155
|
+
(constants.O_NONBLOCK ?? 0);
|
|
156
|
+
/** POSIX owner and permission bits; Windows has no uid, so its ACL-protected agent directory carries privacy. */
|
|
157
|
+
export function isTelegramOwnerPrivate(stat) {
|
|
158
|
+
const uid = process.getuid?.();
|
|
159
|
+
return (uid === undefined ||
|
|
160
|
+
(stat.uid === BigInt(uid) && (stat.mode & 63n) === 0n));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read one owner-private, single-link, bounded regular file without following links. Absence returns undefined;
|
|
164
|
+
* an inode or metadata change between inspection and open refuses as `changed`.
|
|
165
|
+
*/
|
|
166
|
+
export function readTelegramPrivateFile(path, maxBytes) {
|
|
167
|
+
let before;
|
|
168
|
+
try {
|
|
169
|
+
before = lstatSync(path, { bigint: true });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error?.code === "ENOENT")
|
|
173
|
+
return undefined;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
if (!before.isFile() ||
|
|
177
|
+
before.isSymbolicLink() ||
|
|
178
|
+
!isTelegramOwnerPrivate(before) ||
|
|
179
|
+
before.nlink !== 1n ||
|
|
180
|
+
before.size > BigInt(maxBytes)) {
|
|
181
|
+
throw new TelegramPrivateFileError(before.size > BigInt(maxBytes) ? "capacity" : "unsafe", "Telegram private file is not a bounded no-follow regular file.");
|
|
182
|
+
}
|
|
183
|
+
const fd = openSync(path, TELEGRAM_STRICT_READ_FLAGS);
|
|
184
|
+
try {
|
|
185
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
186
|
+
if (!opened.isFile() ||
|
|
187
|
+
opened.dev !== before.dev ||
|
|
188
|
+
opened.ino !== before.ino ||
|
|
189
|
+
opened.uid !== before.uid ||
|
|
190
|
+
opened.nlink !== 1n ||
|
|
191
|
+
!isTelegramOwnerPrivate(opened) ||
|
|
192
|
+
opened.size !== before.size ||
|
|
193
|
+
opened.mtimeNs !== before.mtimeNs) {
|
|
194
|
+
throw new TelegramPrivateFileError("changed", "Telegram private file changed during inspection.");
|
|
195
|
+
}
|
|
196
|
+
return readFileSync(fd, "utf8");
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
closeSync(fd);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Atomically publish owner-private contents through a unique staging file; the rename consumes staging. */
|
|
203
|
+
export function publishTelegramPrivateFile(path, temporaryBasePath, contents, label, onBoundary) {
|
|
204
|
+
const temporaryPath = `${temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
205
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
206
|
+
mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
|
|
207
|
+
try {
|
|
208
|
+
writeFileSync(temporaryPath, contents, { encoding: "utf8", mode: 0o600 });
|
|
209
|
+
chmodSync(temporaryPath, 0o600);
|
|
210
|
+
onBoundary?.("after-write-before-rename");
|
|
211
|
+
if (!renameTelegramPathWithRetry(temporaryPath, path))
|
|
212
|
+
throw new Error(`${label} staging file disappeared before publication.`);
|
|
213
|
+
chmodSync(path, 0o600);
|
|
214
|
+
onBoundary?.("after-rename");
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
try {
|
|
218
|
+
unlinkSync(temporaryPath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* Atomic rename consumes the temporary path. */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
131
225
|
/** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
|
|
132
226
|
export function renameTelegramPathWithRetry(sourcePath, destinationPath, options = {}) {
|
|
133
227
|
const rename = options.rename ?? renameSync;
|
|
@@ -543,7 +637,10 @@ function requireRuntimeStatePath(path) {
|
|
|
543
637
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
|
|
544
638
|
}
|
|
545
639
|
function requireRuntimeStateProfile(profile) {
|
|
546
|
-
if (typeof profile !== "string" ||
|
|
640
|
+
if (typeof profile !== "string" ||
|
|
641
|
+
!profile.length ||
|
|
642
|
+
profile.length > 512 ||
|
|
643
|
+
/[\x00-\x1f]/u.test(profile))
|
|
547
644
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state profile is invalid.");
|
|
548
645
|
}
|
|
549
646
|
/** Private single-link regular file read with identity continuity; initial absence only is positive. */
|
|
@@ -554,13 +651,17 @@ function readTelegramRuntimeSource(path) {
|
|
|
554
651
|
try {
|
|
555
652
|
const stat = lstatSync(path);
|
|
556
653
|
observed = true;
|
|
557
|
-
if (!stat.isFile() ||
|
|
654
|
+
if (!stat.isFile() ||
|
|
655
|
+
stat.nlink !== 1 ||
|
|
558
656
|
(process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
559
657
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state must be a private regular file.");
|
|
560
658
|
const fd = openSync(path, "r");
|
|
561
659
|
try {
|
|
562
660
|
const opened = fstatSync(fd);
|
|
563
|
-
if (opened.dev !== stat.dev ||
|
|
661
|
+
if (opened.dev !== stat.dev ||
|
|
662
|
+
opened.ino !== stat.ino ||
|
|
663
|
+
opened.nlink !== 1 ||
|
|
664
|
+
!opened.isFile() ||
|
|
564
665
|
(process.platform !== "win32" && (opened.mode & 0o077) !== 0))
|
|
565
666
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state identity changed before reading.");
|
|
566
667
|
source = readFileSync(fd).toString("utf8");
|
|
@@ -569,7 +670,11 @@ function readTelegramRuntimeSource(path) {
|
|
|
569
670
|
closeSync(fd);
|
|
570
671
|
}
|
|
571
672
|
const after = lstatSync(path);
|
|
572
|
-
if (after.dev !== stat.dev ||
|
|
673
|
+
if (after.dev !== stat.dev ||
|
|
674
|
+
after.ino !== stat.ino ||
|
|
675
|
+
after.size !== stat.size ||
|
|
676
|
+
after.mtimeMs !== stat.mtimeMs ||
|
|
677
|
+
after.ctimeMs !== stat.ctimeMs)
|
|
573
678
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state changed during observation.");
|
|
574
679
|
}
|
|
575
680
|
catch (error) {
|
|
@@ -591,12 +696,15 @@ export function readTelegramRuntimeState(path) {
|
|
|
591
696
|
catch (error) {
|
|
592
697
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state is unreadable.", { cause: error });
|
|
593
698
|
}
|
|
594
|
-
if (!runtimeStateRecord(value) ||
|
|
595
|
-
|
|
699
|
+
if (!runtimeStateRecord(value) ||
|
|
700
|
+
value.version !== 2 ||
|
|
701
|
+
!runtimeStateRecord(value.profiles) ||
|
|
702
|
+
Object.keys(value).some((key) => key !== "version" && key !== "profiles"))
|
|
596
703
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state envelope is unsupported or malformed.");
|
|
597
704
|
for (const [profile, sections] of Object.entries(value.profiles)) {
|
|
598
705
|
requireRuntimeStateProfile(profile);
|
|
599
|
-
if (!runtimeStateRecord(sections) ||
|
|
706
|
+
if (!runtimeStateRecord(sections) ||
|
|
707
|
+
Object.keys(sections).some((key) => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key)))
|
|
600
708
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state sections are malformed.");
|
|
601
709
|
}
|
|
602
710
|
return value;
|
|
@@ -672,12 +780,19 @@ export function mutateTelegramRuntimeStateSection(path, profile, section, mutate
|
|
|
672
780
|
return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
|
|
673
781
|
assertCurrent();
|
|
674
782
|
const file = readTelegramRuntimeState(path);
|
|
675
|
-
const previous = Object.hasOwn(file.profiles, profile)
|
|
783
|
+
const previous = Object.hasOwn(file.profiles, profile)
|
|
784
|
+
? file.profiles[profile]
|
|
785
|
+
: {};
|
|
676
786
|
const outcome = mutate(structuredClone(previous[section]), structuredClone(previous));
|
|
677
|
-
if (!runtimeStateRecord(outcome) ||
|
|
787
|
+
if (!runtimeStateRecord(outcome) ||
|
|
788
|
+
!Object.hasOwn(outcome, "value") ||
|
|
789
|
+
!Object.hasOwn(outcome, "result") ||
|
|
790
|
+
"then" in outcome)
|
|
678
791
|
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state mutations must return a synchronous value and result.");
|
|
679
792
|
// Normalize intentional optional properties to their wire representation before comparison/publication.
|
|
680
|
-
const next = outcome.value === undefined
|
|
793
|
+
const next = outcome.value === undefined
|
|
794
|
+
? undefined
|
|
795
|
+
: JSON.parse(JSON.stringify(outcome.value));
|
|
681
796
|
assertCurrent();
|
|
682
797
|
if (isDeepStrictEqual(previous[section], next))
|
|
683
798
|
return outcome.result;
|
|
@@ -687,18 +802,32 @@ export function mutateTelegramRuntimeStateSection(path, profile, section, mutate
|
|
|
687
802
|
else
|
|
688
803
|
updated[section] = next;
|
|
689
804
|
if (Object.keys(updated).length)
|
|
690
|
-
Object.defineProperty(file.profiles, profile, {
|
|
805
|
+
Object.defineProperty(file.profiles, profile, {
|
|
806
|
+
value: updated,
|
|
807
|
+
enumerable: true,
|
|
808
|
+
configurable: true,
|
|
809
|
+
writable: true,
|
|
810
|
+
});
|
|
691
811
|
else
|
|
692
812
|
delete file.profiles[profile];
|
|
693
813
|
const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
694
814
|
try {
|
|
695
815
|
options.onPublicationBoundary?.("before-write");
|
|
696
816
|
assertCurrent();
|
|
697
|
-
writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
|
|
817
|
+
writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
|
|
818
|
+
encoding: "utf8",
|
|
819
|
+
flag: "wx",
|
|
820
|
+
mode: 0o600,
|
|
821
|
+
});
|
|
698
822
|
options.onPublicationBoundary?.("after-write-before-rename");
|
|
699
823
|
assertCurrent();
|
|
700
824
|
try {
|
|
701
|
-
if (!renameTelegramPathWithRetry(tempPath, path, {
|
|
825
|
+
if (!renameTelegramPathWithRetry(tempPath, path, {
|
|
826
|
+
rename(from, to) {
|
|
827
|
+
assertCurrent();
|
|
828
|
+
(options.publishRename ?? renameSync)(from, to);
|
|
829
|
+
},
|
|
830
|
+
}))
|
|
702
831
|
throw new Error("Telegram runtime state temporary publication disappeared.");
|
|
703
832
|
options.onPublicationBoundary?.("after-rename");
|
|
704
833
|
assertCurrent();
|
|
@@ -785,21 +914,11 @@ export function parseTelegramLockEntry(value) {
|
|
|
785
914
|
? record.busSocketPath
|
|
786
915
|
: undefined,
|
|
787
916
|
busSecret: typeof record.busSecret === "string" ? record.busSecret : undefined,
|
|
788
|
-
...(readTelegramLockJournalPath(record)
|
|
917
|
+
...(readTelegramLockJournalPath(record)
|
|
918
|
+
? { journalPath: readTelegramLockJournalPath(record) }
|
|
919
|
+
: {}),
|
|
789
920
|
};
|
|
790
921
|
}
|
|
791
|
-
export function isProcessAlive(pid) {
|
|
792
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
793
|
-
return false;
|
|
794
|
-
try {
|
|
795
|
-
process.kill(pid, 0);
|
|
796
|
-
return true;
|
|
797
|
-
}
|
|
798
|
-
catch (error) {
|
|
799
|
-
// Only an absent PID proves death; permission and unexpected failures do not.
|
|
800
|
-
return error.code !== "ESRCH";
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
922
|
function formatTelegramLockEntry(lock) {
|
|
804
923
|
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
805
924
|
}
|
|
@@ -883,14 +1002,29 @@ function assertTelegramStateTransport(value) {
|
|
|
883
1002
|
return;
|
|
884
1003
|
if (!runtimeStateRecord(value))
|
|
885
1004
|
throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
886
|
-
const keys = [
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1005
|
+
const keys = [
|
|
1006
|
+
"pid",
|
|
1007
|
+
"cwd",
|
|
1008
|
+
"instanceId",
|
|
1009
|
+
"heartbeatMs",
|
|
1010
|
+
"leaderEpoch",
|
|
1011
|
+
"runtimeGeneration",
|
|
1012
|
+
"busSocketPath",
|
|
1013
|
+
"busSecret",
|
|
1014
|
+
"journalPath",
|
|
1015
|
+
];
|
|
1016
|
+
if (Object.keys(value).some((key) => !keys.includes(key)) ||
|
|
1017
|
+
(value.pid === undefined
|
|
1018
|
+
? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value)
|
|
1019
|
+
: !Number.isSafeInteger(value.pid) || value.pid <= 0) ||
|
|
1020
|
+
["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some((key) => value[key] !== undefined && typeof value[key] !== "string") ||
|
|
1021
|
+
["heartbeatMs", "runtimeGeneration"].some((key) => value[key] !== undefined &&
|
|
1022
|
+
(!Number.isSafeInteger(value[key]) || value[key] < 0)) ||
|
|
892
1023
|
(value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
|
|
893
|
-
(value.leaderEpoch !== undefined &&
|
|
1024
|
+
(value.leaderEpoch !== undefined &&
|
|
1025
|
+
(typeof value.leaderEpoch === "string"
|
|
1026
|
+
? value.leaderEpoch.length === 0
|
|
1027
|
+
: !Number.isSafeInteger(value.leaderEpoch))))
|
|
894
1028
|
throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
895
1029
|
}
|
|
896
1030
|
export function createTelegramLockRuntime(options = {}) {
|
|
@@ -921,7 +1055,8 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
921
1055
|
// Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
|
|
922
1056
|
try {
|
|
923
1057
|
const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
|
|
924
|
-
.filter(([, value]) => Object.hasOwn(value, "transport"))
|
|
1058
|
+
.filter(([, value]) => Object.hasOwn(value, "transport"))
|
|
1059
|
+
.map(([profile, value]) => [profile, value.transport]));
|
|
925
1060
|
assertTelegramStateTransport(transports[resolveEffectiveKey()]);
|
|
926
1061
|
return transports;
|
|
927
1062
|
}
|
|
@@ -933,14 +1068,17 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
933
1068
|
if (!statePath)
|
|
934
1069
|
return withLockTransaction(locksPath, mutate);
|
|
935
1070
|
const profile = resolveEffectiveKey();
|
|
936
|
-
return mutateTelegramRuntimeStateSection(statePath, profile, "transport", current => {
|
|
1071
|
+
return mutateTelegramRuntimeStateSection(statePath, profile, "transport", (current) => {
|
|
937
1072
|
assertTelegramStateTransport(current);
|
|
938
1073
|
const locks = { [profile]: current };
|
|
939
1074
|
const outcome = mutate(locks);
|
|
940
1075
|
const value = outcome.changed ? locks[profile] : current;
|
|
941
1076
|
assertTelegramStateTransport(value);
|
|
942
1077
|
return { value, result: outcome.result };
|
|
943
|
-
}, {
|
|
1078
|
+
}, {
|
|
1079
|
+
...options.statePublication,
|
|
1080
|
+
isCurrent: () => resolveEffectiveKey() === profile,
|
|
1081
|
+
});
|
|
944
1082
|
};
|
|
945
1083
|
const readLock = () => {
|
|
946
1084
|
const effectiveKey = resolveEffectiveKey();
|
|
@@ -949,19 +1087,30 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
949
1087
|
// A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
|
|
950
1088
|
// endpoint, secret and epoch belong to another protocol and must never become a follower target.
|
|
951
1089
|
const readLegacyOwner = (effectiveKey) => {
|
|
952
|
-
if (!options.legacyLocksPath ||
|
|
1090
|
+
if (!options.legacyLocksPath ||
|
|
1091
|
+
options.legacyLocksPath === (statePath ?? locksPath))
|
|
953
1092
|
return undefined;
|
|
954
1093
|
try {
|
|
955
|
-
const records = statePath
|
|
1094
|
+
const records = statePath
|
|
1095
|
+
? readLocksForTransaction(options.legacyLocksPath)
|
|
1096
|
+
: readLocks(options.legacyLocksPath);
|
|
956
1097
|
if (statePath)
|
|
957
1098
|
for (const value of Object.values(records))
|
|
958
1099
|
assertTelegramStateTransport(value);
|
|
959
1100
|
const raw = records[effectiveKey];
|
|
960
1101
|
const legacy = parseTelegramLockEntry(raw);
|
|
961
|
-
if (!legacy ||
|
|
1102
|
+
if (!legacy ||
|
|
1103
|
+
getLockState(legacy, -1, isAlive, stateOptions()).kind !==
|
|
1104
|
+
"active-elsewhere")
|
|
962
1105
|
return undefined;
|
|
963
|
-
return {
|
|
964
|
-
|
|
1106
|
+
return {
|
|
1107
|
+
pid: legacy.pid,
|
|
1108
|
+
...(legacy.cwd ? { cwd: legacy.cwd } : {}),
|
|
1109
|
+
...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
|
|
1110
|
+
...(legacy.heartbeatMs !== undefined
|
|
1111
|
+
? { heartbeatMs: legacy.heartbeatMs }
|
|
1112
|
+
: {}),
|
|
1113
|
+
};
|
|
965
1114
|
}
|
|
966
1115
|
catch (error) {
|
|
967
1116
|
if (statePath)
|
|
@@ -990,7 +1139,10 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
990
1139
|
const effectiveKey = resolveEffectiveKey();
|
|
991
1140
|
const legacy = readLegacyOwner(effectiveKey);
|
|
992
1141
|
if (legacy)
|
|
993
|
-
return {
|
|
1142
|
+
return {
|
|
1143
|
+
result: { ok: false, lock: legacy },
|
|
1144
|
+
changed: false,
|
|
1145
|
+
};
|
|
994
1146
|
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
995
1147
|
const state = getLockState(current, pid, isAlive, stateOptions());
|
|
996
1148
|
const expectedOwned = adoptCompatibleOwnedLock(effectiveKey, current, ctx);
|
|
@@ -1032,7 +1184,8 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1032
1184
|
changed: false,
|
|
1033
1185
|
};
|
|
1034
1186
|
}
|
|
1035
|
-
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ??
|
|
1187
|
+
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ??
|
|
1188
|
+
options.createJournalPath?.(ctx);
|
|
1036
1189
|
const lock = createLockEntry(pid, ctx, {
|
|
1037
1190
|
journalPath,
|
|
1038
1191
|
instanceId: options.instanceId,
|
|
@@ -1093,34 +1246,45 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1093
1246
|
const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
1094
1247
|
return hasSameLockOwner(lock, adoptCompatibleOwnedLock(effectiveKey, lock, ctx));
|
|
1095
1248
|
},
|
|
1096
|
-
commitIfOwned: (commit) => !deliveryRevoked &&
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
if (
|
|
1102
|
-
ownedLockKey
|
|
1103
|
-
|
|
1249
|
+
commitIfOwned: (commit) => !deliveryRevoked &&
|
|
1250
|
+
transactOwners((locks) => {
|
|
1251
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1252
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1253
|
+
const exactOwner = ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
|
|
1254
|
+
if (!exactOwner) {
|
|
1255
|
+
if (ownedLockKey === effectiveKey) {
|
|
1256
|
+
ownedLockKey = undefined;
|
|
1257
|
+
ownedLock = undefined;
|
|
1258
|
+
}
|
|
1259
|
+
return { result: false, changed: false };
|
|
1104
1260
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1261
|
+
commit();
|
|
1262
|
+
// External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
|
|
1263
|
+
const current = !statePath ||
|
|
1264
|
+
(!deliveryRevoked &&
|
|
1265
|
+
hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
|
|
1266
|
+
return { result: current, changed: false };
|
|
1267
|
+
}),
|
|
1112
1268
|
publishStateSectionIfOwned(section, mutate, publication) {
|
|
1113
1269
|
if (section !== "workspace" && section !== "runtime")
|
|
1114
1270
|
throw new TelegramRuntimeStateError("invalid", "Telegram owner section publication is restricted to Workspace and runtime observations.");
|
|
1115
1271
|
if (!statePath)
|
|
1116
1272
|
throw new TelegramRuntimeStateError("invalid", "Consolidated transport storage is not selected.");
|
|
1117
1273
|
const profile = resolveEffectiveKey(), expected = ownedLock ? { ...ownedLock } : undefined;
|
|
1118
|
-
if (publication.expectedScope &&
|
|
1274
|
+
if (publication.expectedScope &&
|
|
1275
|
+
(publication.expectedScope.path !== statePath ||
|
|
1276
|
+
publication.expectedScope.profile !== profile))
|
|
1119
1277
|
return { committed: false };
|
|
1120
|
-
if (deliveryRevoked ||
|
|
1278
|
+
if (deliveryRevoked ||
|
|
1279
|
+
ownedLockKey !== profile ||
|
|
1280
|
+
!expected ||
|
|
1281
|
+
!publication.isCurrent())
|
|
1121
1282
|
return { committed: false };
|
|
1122
|
-
const isCurrent = () => !deliveryRevoked &&
|
|
1123
|
-
|
|
1283
|
+
const isCurrent = () => !deliveryRevoked &&
|
|
1284
|
+
resolveEffectiveKey() === profile &&
|
|
1285
|
+
ownedLockKey === profile &&
|
|
1286
|
+
hasSameLockOwner(ownedLock, expected) &&
|
|
1287
|
+
publication.isCurrent() &&
|
|
1124
1288
|
hasSameLockOwner(parseTelegramLockEntry(readOwners()[profile]), expected);
|
|
1125
1289
|
if (!isCurrent())
|
|
1126
1290
|
return { committed: false };
|
|
@@ -1129,40 +1293,44 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1129
1293
|
if (!hasSameLockOwner(parseTelegramLockEntry(observed.transport), expected))
|
|
1130
1294
|
return { value: current, result: { committed: false } };
|
|
1131
1295
|
const outcome = mutate(current, observed);
|
|
1132
|
-
return {
|
|
1296
|
+
return {
|
|
1297
|
+
value: outcome.value,
|
|
1298
|
+
result: { committed: true, result: outcome.result },
|
|
1299
|
+
};
|
|
1133
1300
|
}, { ...publication, isCurrent });
|
|
1134
1301
|
},
|
|
1135
|
-
refresh: (ctx) => !deliveryRevoked &&
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (
|
|
1141
|
-
ownedLockKey
|
|
1142
|
-
|
|
1302
|
+
refresh: (ctx) => !deliveryRevoked &&
|
|
1303
|
+
transactOwners((locks) => {
|
|
1304
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1305
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1306
|
+
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
1307
|
+
if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
|
|
1308
|
+
if (ownedLockKey === effectiveKey) {
|
|
1309
|
+
ownedLockKey = undefined;
|
|
1310
|
+
ownedLock = undefined;
|
|
1311
|
+
}
|
|
1312
|
+
return { result: false, changed: false };
|
|
1143
1313
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
: {}),
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
return { result: true, changed: true };
|
|
1165
|
-
}),
|
|
1314
|
+
if (!options.instanceId)
|
|
1315
|
+
return { result: true, changed: false };
|
|
1316
|
+
const refreshedLock = {
|
|
1317
|
+
pid: lock.pid,
|
|
1318
|
+
...(lock.cwd ? { cwd: lock.cwd } : {}),
|
|
1319
|
+
instanceId: options.instanceId,
|
|
1320
|
+
heartbeatMs: getNowMs(),
|
|
1321
|
+
leaderEpoch: lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
|
|
1322
|
+
runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
|
|
1323
|
+
...(options.busSocketPath
|
|
1324
|
+
? { busSocketPath: options.busSocketPath }
|
|
1325
|
+
: {}),
|
|
1326
|
+
busSecret: options.busSecret ?? lock.busSecret,
|
|
1327
|
+
...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
|
|
1328
|
+
};
|
|
1329
|
+
locks[effectiveKey] = refreshedLock;
|
|
1330
|
+
ownedLockKey = effectiveKey;
|
|
1331
|
+
ownedLock = refreshedLock;
|
|
1332
|
+
return { result: true, changed: true };
|
|
1333
|
+
}),
|
|
1166
1334
|
};
|
|
1167
1335
|
}
|
|
1168
1336
|
export function createTelegramLockOwnershipGuard(lock) {
|
|
@@ -1220,7 +1388,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1220
1388
|
}
|
|
1221
1389
|
await deps.stopPolling();
|
|
1222
1390
|
// Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
|
|
1223
|
-
if (generation === pollingGeneration &&
|
|
1391
|
+
if (generation === pollingGeneration &&
|
|
1392
|
+
suspensionsInFlight === 1 &&
|
|
1393
|
+
startupsInFlight === 0) {
|
|
1224
1394
|
suspendedGeneration = generation;
|
|
1225
1395
|
}
|
|
1226
1396
|
}
|
|
@@ -1473,12 +1643,18 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1473
1643
|
suspend: suspendPolling,
|
|
1474
1644
|
captureTransportAuthority(ctx) {
|
|
1475
1645
|
const generation = pollingGeneration;
|
|
1476
|
-
const current = () => generation === pollingGeneration &&
|
|
1477
|
-
|
|
1646
|
+
const current = () => generation === pollingGeneration &&
|
|
1647
|
+
activeContext === ctx &&
|
|
1648
|
+
!ownershipStop &&
|
|
1649
|
+
(deps.isContextCurrent?.(ctx) ?? true) &&
|
|
1650
|
+
deps.lock.owns(snapshotLockContext(ctx));
|
|
1478
1651
|
return current() ? current : undefined;
|
|
1479
1652
|
},
|
|
1480
1653
|
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1481
|
-
suspensionsInFlight === 0 &&
|
|
1654
|
+
suspensionsInFlight === 0 &&
|
|
1655
|
+
startupsInFlight === 0 &&
|
|
1656
|
+
!sessionAutoStartRun &&
|
|
1657
|
+
!ownershipStop,
|
|
1482
1658
|
onPersistentConflict: async (ctx, count) => {
|
|
1483
1659
|
if (activeContext === undefined || ownershipStop)
|
|
1484
1660
|
return;
|
|
@@ -1505,13 +1681,17 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1505
1681
|
}
|
|
1506
1682
|
ownershipStop = Promise.resolve()
|
|
1507
1683
|
.then(() => deps.stopPolling())
|
|
1508
|
-
.catch((error) => {
|
|
1684
|
+
.catch((error) => {
|
|
1685
|
+
cleanupErrors.push(String(error));
|
|
1686
|
+
})
|
|
1509
1687
|
.finally(() => {
|
|
1510
1688
|
ownershipStop = undefined;
|
|
1511
1689
|
deps.recordRuntimeEvent?.("polling", ownership === "lost"
|
|
1512
1690
|
? "Telegram transport stopped: local ownership lost; check for another Pi instance."
|
|
1513
1691
|
: "Telegram transport stopped: competing getUpdates client or ownership mismatch.", {
|
|
1514
|
-
phase: "persistent-conflict",
|
|
1692
|
+
phase: "persistent-conflict",
|
|
1693
|
+
count,
|
|
1694
|
+
ownership,
|
|
1515
1695
|
...(cleanupErrors.length ? { cleanupErrors } : {}),
|
|
1516
1696
|
});
|
|
1517
1697
|
deps.updateStatus(ctx);
|
|
@@ -1553,7 +1733,8 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1553
1733
|
await ownershipStop;
|
|
1554
1734
|
if (!isCurrent())
|
|
1555
1735
|
return;
|
|
1556
|
-
if (canRestoreRememberedFollower &&
|
|
1736
|
+
if (canRestoreRememberedFollower &&
|
|
1737
|
+
state?.kind === "active-elsewhere") {
|
|
1557
1738
|
const restored = await deps.restoreFollowerWithOwner?.(ctx, state.lock);
|
|
1558
1739
|
if (!isCurrent())
|
|
1559
1740
|
return;
|