@llblab/pi-telegram 0.45.11 → 0.47.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +6 -6
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1608 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +116 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1006 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +17 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +11 -3
- package/lib/bus-leader.ts +57 -20
- package/lib/bus.ts +12 -2
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +26 -1
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +21 -5
- package/lib/outbound-markup.ts +16 -7
- package/lib/pi.ts +4 -0
- package/lib/sync.ts +3 -0
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-cleanup-manager.ts +36 -6
- package/lib/thread-display.ts +96 -18
- package/lib/threads.ts +150 -30
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram singleton lock helpers
|
|
3
|
+
* Zones: telegram ownership, filesystem, transport authority
|
|
4
|
+
* Owns extension-local owners.json access and Telegram bridge ownership semantics
|
|
5
|
+
*/
|
|
6
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { resolveTelegramOwnersPath } from "./paths.js";
|
|
10
|
+
export const TELEGRAM_LOCK_KEY = "default";
|
|
11
|
+
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
12
|
+
export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
|
|
13
|
+
export const TELEGRAM_OWNERSHIP_REFRESH_MS = 2_000;
|
|
14
|
+
const TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS = 5;
|
|
15
|
+
const TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS = 25;
|
|
16
|
+
const TELEGRAM_LOCK_TRANSACTION_ATTEMPTS = 80;
|
|
17
|
+
const TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS = 25;
|
|
18
|
+
const TELEGRAM_LOCK_RUNTIME_GENERATION_KEY = "__piTelegramLockRuntimeGeneration__";
|
|
19
|
+
function allocateTelegramLockRuntimeGeneration() {
|
|
20
|
+
const globals = globalThis;
|
|
21
|
+
const previous = globals[TELEGRAM_LOCK_RUNTIME_GENERATION_KEY];
|
|
22
|
+
const previousGeneration = typeof previous === "number" && Number.isSafeInteger(previous)
|
|
23
|
+
? previous
|
|
24
|
+
: 0;
|
|
25
|
+
const generation = Math.max(Date.now(), previousGeneration + 1);
|
|
26
|
+
globals[TELEGRAM_LOCK_RUNTIME_GENERATION_KEY] = generation;
|
|
27
|
+
return generation;
|
|
28
|
+
}
|
|
29
|
+
function getOwnersPath() {
|
|
30
|
+
return resolveTelegramOwnersPath();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the extension-local owner slot for the active Telegram profile.
|
|
34
|
+
* Default profile → default
|
|
35
|
+
* Named profile → the validated profile name
|
|
36
|
+
*/
|
|
37
|
+
export function resolveTelegramLockKey(activeProfile) {
|
|
38
|
+
return activeProfile || TELEGRAM_LOCK_KEY;
|
|
39
|
+
}
|
|
40
|
+
export function createTelegramLockKeyResolver(activeProfile) {
|
|
41
|
+
return function getTelegramLockKey() {
|
|
42
|
+
return resolveTelegramLockKey(activeProfile.getActiveProfileName());
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function readLocks(path = getOwnersPath()) {
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
51
|
+
? value
|
|
52
|
+
: {};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readLocksForTransaction(path) {
|
|
59
|
+
let source;
|
|
60
|
+
try {
|
|
61
|
+
source = readFileSync(path, "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error?.code === "ENOENT")
|
|
65
|
+
return {};
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
const value = JSON.parse(source);
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
throw new Error(`Invalid Telegram owner store: ${path}`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function isRetryableLockWriteError(error) {
|
|
75
|
+
const code = error?.code;
|
|
76
|
+
return code === "EPERM" || code === "EBUSY" || code === "EACCES";
|
|
77
|
+
}
|
|
78
|
+
function sleepSync(ms) {
|
|
79
|
+
const buffer = new SharedArrayBuffer(4);
|
|
80
|
+
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
81
|
+
}
|
|
82
|
+
/** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
|
|
83
|
+
export function renameTelegramPathWithRetry(sourcePath, destinationPath, options = {}) {
|
|
84
|
+
const rename = options.rename ?? renameSync;
|
|
85
|
+
const attempts = Math.max(1, options.attempts ?? TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS);
|
|
86
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS);
|
|
87
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
88
|
+
try {
|
|
89
|
+
rename(sourcePath, destinationPath);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error?.code === "ENOENT")
|
|
94
|
+
return false;
|
|
95
|
+
if (!isRetryableLockWriteError(error) || attempt === attempts - 1) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
sleepSync(retryDelayMs * (attempt + 1));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const TELEGRAM_TRANSACTION_OWNER_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
|
|
104
|
+
function getLockTransactionOwnerFile(generation) {
|
|
105
|
+
return `owner.${generation}.json`;
|
|
106
|
+
}
|
|
107
|
+
function getLockTransactionOwnerPath(path) {
|
|
108
|
+
const stat = lstatSync(path);
|
|
109
|
+
if (stat.isDirectory()) {
|
|
110
|
+
const entries = readdirSync(path);
|
|
111
|
+
if (entries.length === 1 &&
|
|
112
|
+
(TELEGRAM_TRANSACTION_OWNER_PATTERN.test(entries[0]) ||
|
|
113
|
+
TELEGRAM_TRANSACTION_RECLAIM_PATTERN.test(entries[0]))) {
|
|
114
|
+
return join(path, entries[0]);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unverifiable Telegram lock transaction guard: ${path}`);
|
|
117
|
+
}
|
|
118
|
+
if (stat.isFile())
|
|
119
|
+
return path;
|
|
120
|
+
throw new Error(`Unsupported Telegram lock transaction guard: ${path}`);
|
|
121
|
+
}
|
|
122
|
+
function readLockTransactionOwner(path) {
|
|
123
|
+
try {
|
|
124
|
+
const value = JSON.parse(readFileSync(getLockTransactionOwnerPath(path), "utf8"));
|
|
125
|
+
if (typeof value.pid !== "number" ||
|
|
126
|
+
typeof value.acquiredAtMs !== "number" ||
|
|
127
|
+
typeof value.generation !== "string") {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const ownerMatch = TELEGRAM_TRANSACTION_OWNER_PATTERN.exec(basename(getLockTransactionOwnerPath(path)));
|
|
131
|
+
if (ownerMatch && ownerMatch[1] !== value.generation)
|
|
132
|
+
return undefined;
|
|
133
|
+
return {
|
|
134
|
+
pid: value.pid,
|
|
135
|
+
acquiredAtMs: value.acquiredAtMs,
|
|
136
|
+
generation: value.generation,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function createLockTransactionContentionError(path) {
|
|
144
|
+
return Object.assign(new Error(`Telegram lock transaction guard already exists: ${path}`), { code: "EEXIST" });
|
|
145
|
+
}
|
|
146
|
+
function isLockTransactionContentionError(error) {
|
|
147
|
+
const code = error?.code;
|
|
148
|
+
return (code === "EEXIST" ||
|
|
149
|
+
code === "ENOTEMPTY" ||
|
|
150
|
+
code === "ENOTDIR" ||
|
|
151
|
+
code === "EISDIR" ||
|
|
152
|
+
isRetryableLockWriteError(error));
|
|
153
|
+
}
|
|
154
|
+
function removeLockTransactionGuard(path) {
|
|
155
|
+
rmSync(path, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
function createLockTransactionGuard(path, options = {}) {
|
|
158
|
+
const owner = {
|
|
159
|
+
pid: process.pid,
|
|
160
|
+
acquiredAtMs: Date.now(),
|
|
161
|
+
generation: randomUUID(),
|
|
162
|
+
};
|
|
163
|
+
const stagedPath = mkdtempSync(`${path}.staged.`);
|
|
164
|
+
try {
|
|
165
|
+
chmodSync(stagedPath, 0o700);
|
|
166
|
+
writeFileSync(join(stagedPath, getLockTransactionOwnerFile(owner.generation)), `${JSON.stringify(owner)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
167
|
+
if (existsSync(path))
|
|
168
|
+
throw createLockTransactionContentionError(path);
|
|
169
|
+
(options.publishRename ?? renameSync)(stagedPath, path);
|
|
170
|
+
return owner;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
try {
|
|
174
|
+
removeLockTransactionGuard(stagedPath);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* best effort */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function releaseLockTransactionGuard(path, owner) {
|
|
182
|
+
const current = readLockTransactionOwner(path);
|
|
183
|
+
if (!current) {
|
|
184
|
+
if (!existsSync(path))
|
|
185
|
+
return;
|
|
186
|
+
throw new Error(`Cannot verify Telegram lock transaction guard: ${path}`);
|
|
187
|
+
}
|
|
188
|
+
if (current.pid !== owner.pid ||
|
|
189
|
+
current.generation !== owner.generation ||
|
|
190
|
+
current.acquiredAtMs !== owner.acquiredAtMs) {
|
|
191
|
+
throw new Error(`Telegram lock transaction guard changed ownership: ${path}`);
|
|
192
|
+
}
|
|
193
|
+
const releasedPath = `${path}.released.${randomUUID()}`;
|
|
194
|
+
if (!renameTelegramPathWithRetry(path, releasedPath))
|
|
195
|
+
return;
|
|
196
|
+
try {
|
|
197
|
+
removeLockTransactionGuard(releasedPath);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* released debris cannot retain transaction authority */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function isAbandonedLockTransaction(path) {
|
|
204
|
+
const owner = readLockTransactionOwner(path);
|
|
205
|
+
return owner ? !isProcessAlive(owner.pid) : false;
|
|
206
|
+
}
|
|
207
|
+
const TELEGRAM_TRANSACTION_RECLAIM_PATTERN = /^owner\.reclaim\.(\d+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json$/u;
|
|
208
|
+
const TELEGRAM_ACTIVE_TRANSACTION_RECLAIMS = Symbol.for("@llblab/pi-telegram/active-transaction-reclaims");
|
|
209
|
+
function getActiveTransactionReclaims() {
|
|
210
|
+
const root = globalThis;
|
|
211
|
+
return (root[TELEGRAM_ACTIVE_TRANSACTION_RECLAIMS] ??= new Set());
|
|
212
|
+
}
|
|
213
|
+
function reclaimAbandonedDirectoryGuard(path, options = {}) {
|
|
214
|
+
try {
|
|
215
|
+
if (!lstatSync(path).isDirectory())
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
let entries;
|
|
222
|
+
try {
|
|
223
|
+
entries = readdirSync(path);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Another recovery candidate may remove the observed guard after lstat.
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (entries.length !== 1)
|
|
230
|
+
return false;
|
|
231
|
+
const entry = entries[0];
|
|
232
|
+
let observedPid;
|
|
233
|
+
let observedReclaimGeneration;
|
|
234
|
+
if (TELEGRAM_TRANSACTION_OWNER_PATTERN.test(entry)) {
|
|
235
|
+
const owner = readLockTransactionOwner(path);
|
|
236
|
+
if (!owner)
|
|
237
|
+
return false;
|
|
238
|
+
observedPid = owner.pid;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const match = TELEGRAM_TRANSACTION_RECLAIM_PATTERN.exec(entry);
|
|
242
|
+
if (!match)
|
|
243
|
+
return false;
|
|
244
|
+
observedPid = Number.parseInt(match[1], 10);
|
|
245
|
+
observedReclaimGeneration = match[2];
|
|
246
|
+
}
|
|
247
|
+
const activeReclaims = getActiveTransactionReclaims();
|
|
248
|
+
if (observedPid === process.pid && observedReclaimGeneration !== undefined) {
|
|
249
|
+
if (activeReclaims.has(observedReclaimGeneration))
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
else if (isProcessAlive(observedPid)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const renameRecovery = options.recoveryRename ?? renameSync;
|
|
256
|
+
const sourcePath = join(path, entry);
|
|
257
|
+
const reclaimGeneration = randomUUID();
|
|
258
|
+
const reclaimPath = join(path, `owner.reclaim.${process.pid}.${reclaimGeneration}.json`);
|
|
259
|
+
try {
|
|
260
|
+
// Claim inside the still-occupied guard before making its stable path free.
|
|
261
|
+
renameRecovery(sourcePath, reclaimPath);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const code = error?.code;
|
|
265
|
+
if (code === "ENOENT")
|
|
266
|
+
return false;
|
|
267
|
+
// macOS may report EINVAL instead of ENOENT when another process wins
|
|
268
|
+
// the same source rename. Only classify it as contention once the
|
|
269
|
+
// observed source is actually gone; preserve unrelated EINVAL failures.
|
|
270
|
+
if (code === "EINVAL" && !existsSync(sourcePath))
|
|
271
|
+
return false;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const renameWithRetry = (fromPath, toPath) => renameTelegramPathWithRetry(fromPath, toPath, { rename: renameRecovery });
|
|
275
|
+
activeReclaims.add(reclaimGeneration);
|
|
276
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
277
|
+
try {
|
|
278
|
+
try {
|
|
279
|
+
if (!renameWithRetry(path, stalePath))
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
catch (renameError) {
|
|
283
|
+
try {
|
|
284
|
+
if (!renameWithRetry(reclaimPath, sourcePath))
|
|
285
|
+
throw renameError;
|
|
286
|
+
}
|
|
287
|
+
catch (rollbackError) {
|
|
288
|
+
throw new AggregateError([renameError, rollbackError], `Failed to reclaim or restore Telegram lock transaction guard: ${path}`);
|
|
289
|
+
}
|
|
290
|
+
throw renameError;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
activeReclaims.delete(reclaimGeneration);
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
removeLockTransactionGuard(stalePath);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* stale debris cannot retain transaction authority */
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
function acquireRecoverableDirectoryGuard(path, options = {}) {
|
|
305
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
306
|
+
try {
|
|
307
|
+
return createLockTransactionGuard(path, options);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (!isLockTransactionContentionError(error))
|
|
311
|
+
throw error;
|
|
312
|
+
if (!reclaimAbandonedDirectoryGuard(path, options))
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
function removeAbandonedLegacyRecoveryGuard(path, options = {}) {
|
|
319
|
+
try {
|
|
320
|
+
if (!lstatSync(path).isFile() || !isAbandonedLockTransaction(path))
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const migrationGuardPath = `${path}.migration`;
|
|
327
|
+
const migrationOwner = acquireRecoverableDirectoryGuard(migrationGuardPath, options);
|
|
328
|
+
if (!migrationOwner)
|
|
329
|
+
return false;
|
|
330
|
+
try {
|
|
331
|
+
try {
|
|
332
|
+
if (!lstatSync(path).isFile() || !isAbandonedLockTransaction(path))
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
339
|
+
try {
|
|
340
|
+
renameSync(path, stalePath);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error?.code === "ENOENT")
|
|
344
|
+
return false;
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
removeLockTransactionGuard(stalePath);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
/* stale debris cannot retain transaction authority */
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
releaseLockTransactionGuard(migrationGuardPath, migrationOwner);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function acquireLegacyRecoveryGuard(path, options = {}) {
|
|
360
|
+
let owner = acquireRecoverableDirectoryGuard(path, options);
|
|
361
|
+
if (owner)
|
|
362
|
+
return owner;
|
|
363
|
+
if (!removeAbandonedLegacyRecoveryGuard(path, options))
|
|
364
|
+
return undefined;
|
|
365
|
+
owner = acquireRecoverableDirectoryGuard(path, options);
|
|
366
|
+
return owner;
|
|
367
|
+
}
|
|
368
|
+
function createRecoveredLockTransactionGuard(path, options = {}) {
|
|
369
|
+
try {
|
|
370
|
+
return createLockTransactionGuard(path, options);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (isLockTransactionContentionError(error))
|
|
374
|
+
return undefined;
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function recoverAbandonedLockTransaction(path, options = {}) {
|
|
379
|
+
if (!isAbandonedLockTransaction(path))
|
|
380
|
+
return undefined;
|
|
381
|
+
let isDirectory;
|
|
382
|
+
try {
|
|
383
|
+
isDirectory = lstatSync(path).isDirectory();
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
if (isDirectory) {
|
|
389
|
+
if (!reclaimAbandonedDirectoryGuard(path, options))
|
|
390
|
+
return undefined;
|
|
391
|
+
const recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
392
|
+
try {
|
|
393
|
+
reclaimAbandonedDirectoryGuard(`${path}.recovery`, options);
|
|
394
|
+
return recoveredOwner;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (recoveredOwner) {
|
|
398
|
+
try {
|
|
399
|
+
releaseLockTransactionGuard(path, recoveredOwner);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
/* preserve the recovery cleanup failure */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const recoveryGuardPath = `${path}.recovery`;
|
|
409
|
+
const recoveryOwner = acquireLegacyRecoveryGuard(recoveryGuardPath, options);
|
|
410
|
+
if (!recoveryOwner)
|
|
411
|
+
return undefined;
|
|
412
|
+
let recoveredOwner;
|
|
413
|
+
try {
|
|
414
|
+
if (!isAbandonedLockTransaction(path))
|
|
415
|
+
return undefined;
|
|
416
|
+
const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
|
|
417
|
+
try {
|
|
418
|
+
renameSync(path, stalePath);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (error?.code === "ENOENT")
|
|
422
|
+
return undefined;
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
removeLockTransactionGuard(stalePath);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
/* stale debris cannot retain transaction authority */
|
|
430
|
+
}
|
|
431
|
+
recoveredOwner = createRecoveredLockTransactionGuard(path, options);
|
|
432
|
+
return recoveredOwner;
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
try {
|
|
436
|
+
releaseLockTransactionGuard(recoveryGuardPath, recoveryOwner);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (recoveredOwner) {
|
|
440
|
+
try {
|
|
441
|
+
releaseLockTransactionGuard(path, recoveredOwner);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
/* preserve the recovery cleanup failure */
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function acquireLockTransaction(path, options = {}) {
|
|
452
|
+
const attempts = Math.max(1, options.attempts ?? TELEGRAM_LOCK_TRANSACTION_ATTEMPTS);
|
|
453
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? TELEGRAM_LOCK_TRANSACTION_RETRY_DELAY_MS);
|
|
454
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
455
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
456
|
+
try {
|
|
457
|
+
return createLockTransactionGuard(path, options);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (!isLockTransactionContentionError(error))
|
|
461
|
+
throw error;
|
|
462
|
+
const recoveredOwner = recoverAbandonedLockTransaction(path, options);
|
|
463
|
+
if (recoveredOwner !== undefined)
|
|
464
|
+
return recoveredOwner;
|
|
465
|
+
if (attempt === attempts - 1) {
|
|
466
|
+
throw new Error(`Timed out acquiring Telegram lock transaction: ${path}`);
|
|
467
|
+
}
|
|
468
|
+
sleepSync(retryDelayMs);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
throw new Error(`Failed to acquire Telegram lock transaction: ${path}`);
|
|
472
|
+
}
|
|
473
|
+
export function withTelegramFileTransaction(transactionPath, operation, options = {}) {
|
|
474
|
+
const owner = acquireLockTransaction(transactionPath, options);
|
|
475
|
+
try {
|
|
476
|
+
return operation();
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
releaseLockTransactionGuard(transactionPath, owner);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function withLockTransaction(locksPath, mutate) {
|
|
483
|
+
return withTelegramFileTransaction(`${locksPath}.transaction`, () => {
|
|
484
|
+
const locks = readLocksForTransaction(locksPath);
|
|
485
|
+
const outcome = mutate(locks);
|
|
486
|
+
if (outcome.changed)
|
|
487
|
+
writeLocks(locksPath, locks);
|
|
488
|
+
return outcome.result;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
export function writeLocks(path, locks) {
|
|
492
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
493
|
+
const payload = `${JSON.stringify(locks, null, 2)}\n`;
|
|
494
|
+
let lastError;
|
|
495
|
+
for (let attempt = 0; attempt < TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS; attempt += 1) {
|
|
496
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${attempt}.tmp`;
|
|
497
|
+
try {
|
|
498
|
+
writeFileSync(tempPath, payload, {
|
|
499
|
+
encoding: "utf8",
|
|
500
|
+
mode: 0o600,
|
|
501
|
+
});
|
|
502
|
+
renameSync(tempPath, path);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
lastError = error;
|
|
507
|
+
try {
|
|
508
|
+
unlinkSync(tempPath);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
/* best effort */
|
|
512
|
+
}
|
|
513
|
+
if (!isRetryableLockWriteError(error) ||
|
|
514
|
+
attempt === TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS - 1) {
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
sleepSync(TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS * (attempt + 1));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
throw lastError;
|
|
521
|
+
}
|
|
522
|
+
export function parseTelegramLockEntry(value) {
|
|
523
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
524
|
+
return undefined;
|
|
525
|
+
const record = value;
|
|
526
|
+
if (typeof record.pid !== "number")
|
|
527
|
+
return undefined;
|
|
528
|
+
return {
|
|
529
|
+
pid: record.pid,
|
|
530
|
+
cwd: typeof record.cwd === "string" ? record.cwd : undefined,
|
|
531
|
+
instanceId: typeof record.instanceId === "string" ? record.instanceId : undefined,
|
|
532
|
+
heartbeatMs: typeof record.heartbeatMs === "number" ? record.heartbeatMs : undefined,
|
|
533
|
+
leaderEpoch: typeof record.leaderEpoch === "number" ||
|
|
534
|
+
typeof record.leaderEpoch === "string"
|
|
535
|
+
? record.leaderEpoch
|
|
536
|
+
: undefined,
|
|
537
|
+
runtimeGeneration: typeof record.runtimeGeneration === "number"
|
|
538
|
+
? record.runtimeGeneration
|
|
539
|
+
: undefined,
|
|
540
|
+
busSocketPath: typeof record.busSocketPath === "string"
|
|
541
|
+
? record.busSocketPath
|
|
542
|
+
: undefined,
|
|
543
|
+
busSecret: typeof record.busSecret === "string" ? record.busSecret : undefined,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
export function isProcessAlive(pid) {
|
|
547
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
548
|
+
return false;
|
|
549
|
+
try {
|
|
550
|
+
process.kill(pid, 0);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
return error.code === "EPERM";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
export function formatTelegramLockEntry(lock) {
|
|
558
|
+
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
559
|
+
}
|
|
560
|
+
function formatTelegramFollowerRegistrationFailure(message) {
|
|
561
|
+
if (/\b(?:ENOENT|ECONNREFUSED|ETIMEDOUT)\b/u.test(message)) {
|
|
562
|
+
return (`live owner / unreachable bus endpoint after bounded retries (${message}); ` +
|
|
563
|
+
"wait briefly for owner recovery, then retry /telegram-connect. " +
|
|
564
|
+
"Do not force takeover while the owner remains live");
|
|
565
|
+
}
|
|
566
|
+
return message;
|
|
567
|
+
}
|
|
568
|
+
function getLockState(lock, pid, isAlive, options = {}) {
|
|
569
|
+
if (!lock)
|
|
570
|
+
return { kind: "inactive" };
|
|
571
|
+
if (lock.pid === pid)
|
|
572
|
+
return { kind: "active-here", lock };
|
|
573
|
+
if (typeof lock.heartbeatMs === "number" &&
|
|
574
|
+
typeof options.nowMs === "number" &&
|
|
575
|
+
typeof options.staleHeartbeatMs === "number" &&
|
|
576
|
+
options.nowMs - lock.heartbeatMs > options.staleHeartbeatMs) {
|
|
577
|
+
return { kind: "stale", lock };
|
|
578
|
+
}
|
|
579
|
+
if (isAlive(lock.pid))
|
|
580
|
+
return { kind: "active-elsewhere", lock };
|
|
581
|
+
return { kind: "stale", lock };
|
|
582
|
+
}
|
|
583
|
+
function ownsLockContext(lock, pid, ctx) {
|
|
584
|
+
if (!lock || lock.pid !== pid)
|
|
585
|
+
return false;
|
|
586
|
+
return !lock.cwd || !ctx || lock.cwd === ctx.cwd;
|
|
587
|
+
}
|
|
588
|
+
function hasSameLockOwner(current, expected) {
|
|
589
|
+
if (!current || !expected)
|
|
590
|
+
return false;
|
|
591
|
+
return (current.pid === expected.pid &&
|
|
592
|
+
current.cwd === expected.cwd &&
|
|
593
|
+
current.instanceId === expected.instanceId &&
|
|
594
|
+
current.leaderEpoch === expected.leaderEpoch &&
|
|
595
|
+
current.runtimeGeneration === expected.runtimeGeneration);
|
|
596
|
+
}
|
|
597
|
+
function canSupersedeSameProcessOwner(current, pid, ctx, instanceId, runtimeGeneration) {
|
|
598
|
+
if (current.pid !== pid ||
|
|
599
|
+
(current.cwd !== undefined && current.cwd !== ctx.cwd) ||
|
|
600
|
+
!instanceId) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
return (current.runtimeGeneration === undefined ||
|
|
604
|
+
runtimeGeneration > current.runtimeGeneration);
|
|
605
|
+
}
|
|
606
|
+
function createLockEntry(pid, ctx, options) {
|
|
607
|
+
const lock = { pid, cwd: ctx.cwd };
|
|
608
|
+
if (options.instanceId) {
|
|
609
|
+
const nowMs = options.getNowMs?.();
|
|
610
|
+
lock.instanceId = options.instanceId;
|
|
611
|
+
lock.heartbeatMs = nowMs;
|
|
612
|
+
lock.leaderEpoch = options.mintLeaderEpoch?.() ?? randomUUID();
|
|
613
|
+
lock.runtimeGeneration = options.runtimeGeneration;
|
|
614
|
+
}
|
|
615
|
+
if (options.busSocketPath)
|
|
616
|
+
lock.busSocketPath = options.busSocketPath;
|
|
617
|
+
if (options.busSecret)
|
|
618
|
+
lock.busSecret = options.busSecret;
|
|
619
|
+
return lock;
|
|
620
|
+
}
|
|
621
|
+
function formatLockState(state) {
|
|
622
|
+
switch (state.kind) {
|
|
623
|
+
case "inactive":
|
|
624
|
+
return "inactive";
|
|
625
|
+
case "active-here":
|
|
626
|
+
return "active here";
|
|
627
|
+
case "active-elsewhere":
|
|
628
|
+
return `active elsewhere (${formatTelegramLockEntry(state.lock)})`;
|
|
629
|
+
case "stale":
|
|
630
|
+
return `stale (${formatTelegramLockEntry(state.lock)})`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
export function createTelegramLockRuntime(options = {}) {
|
|
634
|
+
const key = options.key ?? TELEGRAM_LOCK_KEY;
|
|
635
|
+
const locksPath = options.locksPath ?? getOwnersPath();
|
|
636
|
+
const pid = options.pid ?? process.pid;
|
|
637
|
+
const isAlive = options.isProcessAlive ?? isProcessAlive;
|
|
638
|
+
const getNowMs = options.getNowMs ?? Date.now;
|
|
639
|
+
const runtimeGeneration = options.runtimeGeneration ?? allocateTelegramLockRuntimeGeneration();
|
|
640
|
+
let ownedLockKey;
|
|
641
|
+
let ownedLock;
|
|
642
|
+
let deliveryRevoked = false;
|
|
643
|
+
const stateOptions = () => ({
|
|
644
|
+
nowMs: getNowMs(),
|
|
645
|
+
staleHeartbeatMs: options.staleHeartbeatMs,
|
|
646
|
+
});
|
|
647
|
+
const resolveEffectiveKey = () => {
|
|
648
|
+
if (typeof key === "function")
|
|
649
|
+
return key() || TELEGRAM_LOCK_KEY;
|
|
650
|
+
return key;
|
|
651
|
+
};
|
|
652
|
+
const readLock = () => {
|
|
653
|
+
const effectiveKey = resolveEffectiveKey();
|
|
654
|
+
return parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
655
|
+
};
|
|
656
|
+
const adoptCompatibleOwnedLock = (effectiveKey, lock, ctx) => {
|
|
657
|
+
if (ownedLock) {
|
|
658
|
+
return ownedLockKey === effectiveKey ? ownedLock : undefined;
|
|
659
|
+
}
|
|
660
|
+
if (!ownsLockContext(lock, pid, ctx))
|
|
661
|
+
return undefined;
|
|
662
|
+
if ((lock?.instanceId !== undefined &&
|
|
663
|
+
lock.instanceId !== options.instanceId) ||
|
|
664
|
+
(lock?.runtimeGeneration !== undefined &&
|
|
665
|
+
lock.runtimeGeneration !== runtimeGeneration)) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
ownedLockKey = effectiveKey;
|
|
669
|
+
ownedLock = lock;
|
|
670
|
+
return ownedLock;
|
|
671
|
+
};
|
|
672
|
+
return {
|
|
673
|
+
acquire: (ctx, acquireOptions = {}) => withLockTransaction(locksPath, (locks) => {
|
|
674
|
+
const effectiveKey = resolveEffectiveKey();
|
|
675
|
+
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
676
|
+
const state = getLockState(current, pid, isAlive, stateOptions());
|
|
677
|
+
const expectedOwned = adoptCompatibleOwnedLock(effectiveKey, current, ctx);
|
|
678
|
+
if (state.kind === "active-here" &&
|
|
679
|
+
hasSameLockOwner(current, expectedOwned) &&
|
|
680
|
+
!deliveryRevoked) {
|
|
681
|
+
return {
|
|
682
|
+
result: {
|
|
683
|
+
ok: true,
|
|
684
|
+
lock: current,
|
|
685
|
+
replacedStale: false,
|
|
686
|
+
},
|
|
687
|
+
changed: false,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
if (acquireOptions.election && current) {
|
|
691
|
+
if (state.kind !== "stale" ||
|
|
692
|
+
!hasSameLockOwner(current, acquireOptions.expectedOwner)) {
|
|
693
|
+
return {
|
|
694
|
+
result: { ok: false, lock: current },
|
|
695
|
+
changed: false,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const expectedReplacementMatches = hasSameLockOwner(state.kind === "active-here" || state.kind === "active-elsewhere"
|
|
700
|
+
? state.lock
|
|
701
|
+
: undefined, acquireOptions.expectedOwner);
|
|
702
|
+
const canReplaceCurrent = state.kind === "active-elsewhere" ||
|
|
703
|
+
(state.kind === "active-here" &&
|
|
704
|
+
canSupersedeSameProcessOwner(state.lock, pid, ctx, options.instanceId, runtimeGeneration));
|
|
705
|
+
if (!acquireOptions.election &&
|
|
706
|
+
(state.kind === "active-here" || state.kind === "active-elsewhere") &&
|
|
707
|
+
!(deliveryRevoked && hasSameLockOwner(current, expectedOwned)) &&
|
|
708
|
+
(!acquireOptions.force ||
|
|
709
|
+
!expectedReplacementMatches ||
|
|
710
|
+
!canReplaceCurrent)) {
|
|
711
|
+
return {
|
|
712
|
+
result: { ok: false, lock: state.lock },
|
|
713
|
+
changed: false,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const lock = createLockEntry(pid, ctx, {
|
|
717
|
+
instanceId: options.instanceId,
|
|
718
|
+
busSocketPath: options.busSocketPath,
|
|
719
|
+
busSecret: options.busSecret,
|
|
720
|
+
getNowMs,
|
|
721
|
+
mintLeaderEpoch: options.mintLeaderEpoch,
|
|
722
|
+
runtimeGeneration,
|
|
723
|
+
});
|
|
724
|
+
locks[effectiveKey] = lock;
|
|
725
|
+
ownedLockKey = effectiveKey;
|
|
726
|
+
ownedLock = lock;
|
|
727
|
+
deliveryRevoked = false;
|
|
728
|
+
return {
|
|
729
|
+
result: {
|
|
730
|
+
ok: true,
|
|
731
|
+
lock,
|
|
732
|
+
replacedStale: state.kind === "stale",
|
|
733
|
+
},
|
|
734
|
+
changed: true,
|
|
735
|
+
};
|
|
736
|
+
}),
|
|
737
|
+
release: () => {
|
|
738
|
+
// Withdraw local send authority even if the durable release fails.
|
|
739
|
+
deliveryRevoked = true;
|
|
740
|
+
return withLockTransaction(locksPath, (locks) => {
|
|
741
|
+
const effectiveKey = resolveEffectiveKey();
|
|
742
|
+
const state = getLockState(parseTelegramLockEntry(locks[effectiveKey]), pid, isAlive, stateOptions());
|
|
743
|
+
const changed = ownedLockKey === effectiveKey &&
|
|
744
|
+
hasSameLockOwner(parseTelegramLockEntry(locks[effectiveKey]), ownedLock);
|
|
745
|
+
if (changed) {
|
|
746
|
+
delete locks[effectiveKey];
|
|
747
|
+
ownedLockKey = undefined;
|
|
748
|
+
ownedLock = undefined;
|
|
749
|
+
}
|
|
750
|
+
return { result: state, changed };
|
|
751
|
+
});
|
|
752
|
+
},
|
|
753
|
+
getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
|
|
754
|
+
getStatusLabel: () => formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
|
|
755
|
+
getOwnedLeaderEpoch: () => {
|
|
756
|
+
if (deliveryRevoked)
|
|
757
|
+
return undefined;
|
|
758
|
+
const effectiveKey = resolveEffectiveKey();
|
|
759
|
+
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
760
|
+
const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
|
|
761
|
+
return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
|
|
762
|
+
},
|
|
763
|
+
owns: (ctx) => {
|
|
764
|
+
if (deliveryRevoked)
|
|
765
|
+
return false;
|
|
766
|
+
const effectiveKey = resolveEffectiveKey();
|
|
767
|
+
const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
|
|
768
|
+
return hasSameLockOwner(lock, adoptCompatibleOwnedLock(effectiveKey, lock, ctx));
|
|
769
|
+
},
|
|
770
|
+
commitIfOwned: (commit) => !deliveryRevoked && withLockTransaction(locksPath, (locks) => {
|
|
771
|
+
const effectiveKey = resolveEffectiveKey();
|
|
772
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
773
|
+
const exactOwner = ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
|
|
774
|
+
if (!exactOwner) {
|
|
775
|
+
if (ownedLockKey === effectiveKey) {
|
|
776
|
+
ownedLockKey = undefined;
|
|
777
|
+
ownedLock = undefined;
|
|
778
|
+
}
|
|
779
|
+
return { result: false, changed: false };
|
|
780
|
+
}
|
|
781
|
+
commit();
|
|
782
|
+
return { result: true, changed: false };
|
|
783
|
+
}),
|
|
784
|
+
refresh: (ctx) => !deliveryRevoked && withLockTransaction(locksPath, (locks) => {
|
|
785
|
+
const effectiveKey = resolveEffectiveKey();
|
|
786
|
+
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
787
|
+
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
788
|
+
if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
|
|
789
|
+
if (ownedLockKey === effectiveKey) {
|
|
790
|
+
ownedLockKey = undefined;
|
|
791
|
+
ownedLock = undefined;
|
|
792
|
+
}
|
|
793
|
+
return { result: false, changed: false };
|
|
794
|
+
}
|
|
795
|
+
if (!options.instanceId)
|
|
796
|
+
return { result: true, changed: false };
|
|
797
|
+
const refreshedLock = {
|
|
798
|
+
pid: lock.pid,
|
|
799
|
+
...(lock.cwd ? { cwd: lock.cwd } : {}),
|
|
800
|
+
instanceId: options.instanceId,
|
|
801
|
+
heartbeatMs: getNowMs(),
|
|
802
|
+
leaderEpoch: lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
|
|
803
|
+
runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
|
|
804
|
+
...(options.busSocketPath
|
|
805
|
+
? { busSocketPath: options.busSocketPath }
|
|
806
|
+
: {}),
|
|
807
|
+
busSecret: options.busSecret ?? lock.busSecret,
|
|
808
|
+
};
|
|
809
|
+
locks[effectiveKey] = refreshedLock;
|
|
810
|
+
ownedLockKey = effectiveKey;
|
|
811
|
+
ownedLock = refreshedLock;
|
|
812
|
+
return { result: true, changed: true };
|
|
813
|
+
}),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
export function createTelegramLockOwnershipGuard(lock) {
|
|
817
|
+
return {
|
|
818
|
+
ownsContext: (ctx) => lock.owns(ctx),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
export function createTelegramDirectDeliveryOwnershipChecker(deps) {
|
|
822
|
+
return () => {
|
|
823
|
+
const ctx = deps.contextStore.get();
|
|
824
|
+
return ctx ? deps.lock.owns(ctx) : false;
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function snapshotLockContext(ctx) {
|
|
828
|
+
return { cwd: ctx.cwd };
|
|
829
|
+
}
|
|
830
|
+
export function createTelegramLockedPollingRuntime(deps) {
|
|
831
|
+
let ownershipCheckInterval;
|
|
832
|
+
let ownershipRefreshInterval;
|
|
833
|
+
let ownershipStop;
|
|
834
|
+
let activeContext;
|
|
835
|
+
let takeoverCandidate;
|
|
836
|
+
let sessionAutoStartRun;
|
|
837
|
+
let pollingGeneration = 0;
|
|
838
|
+
const ownershipCheckMs = deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
839
|
+
const ownershipRefreshMs = deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
840
|
+
const stopOwnershipWatcher = () => {
|
|
841
|
+
if (ownershipCheckInterval)
|
|
842
|
+
clearInterval(ownershipCheckInterval);
|
|
843
|
+
if (ownershipRefreshInterval)
|
|
844
|
+
clearInterval(ownershipRefreshInterval);
|
|
845
|
+
ownershipCheckInterval = undefined;
|
|
846
|
+
ownershipRefreshInterval = undefined;
|
|
847
|
+
};
|
|
848
|
+
const suspendPolling = async () => {
|
|
849
|
+
pollingGeneration += 1;
|
|
850
|
+
activeContext = undefined;
|
|
851
|
+
deps.transportMonitor?.stop();
|
|
852
|
+
deps.stopFollowerRegistration?.();
|
|
853
|
+
stopOwnershipWatcher();
|
|
854
|
+
if (sessionAutoStartRun) {
|
|
855
|
+
await sessionAutoStartRun;
|
|
856
|
+
deps.stopFollowerRegistration?.();
|
|
857
|
+
}
|
|
858
|
+
if (ownershipStop) {
|
|
859
|
+
await ownershipStop;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
await deps.stopPolling();
|
|
863
|
+
};
|
|
864
|
+
const stopAfterOwnershipLoss = () => {
|
|
865
|
+
if (ownershipStop)
|
|
866
|
+
return;
|
|
867
|
+
activeContext = undefined;
|
|
868
|
+
deps.transportMonitor?.stop();
|
|
869
|
+
stopOwnershipWatcher();
|
|
870
|
+
deps.onTransportAvailabilityChanged?.();
|
|
871
|
+
ownershipStop = deps
|
|
872
|
+
.stopPolling()
|
|
873
|
+
.catch((error) => deps.recordRuntimeEvent?.("lock", error, { phase: "ownership-loss" }))
|
|
874
|
+
.finally(() => {
|
|
875
|
+
ownershipStop = undefined;
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
const startOwnershipWatcher = (ctx) => {
|
|
879
|
+
const owner = snapshotLockContext(ctx);
|
|
880
|
+
stopOwnershipWatcher();
|
|
881
|
+
ownershipCheckInterval = setInterval(() => {
|
|
882
|
+
try {
|
|
883
|
+
if (deps.lock.owns(owner))
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "check" });
|
|
888
|
+
}
|
|
889
|
+
stopAfterOwnershipLoss();
|
|
890
|
+
}, ownershipCheckMs);
|
|
891
|
+
ownershipRefreshInterval = setInterval(() => {
|
|
892
|
+
try {
|
|
893
|
+
if (deps.lock.refresh(owner))
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "refresh" });
|
|
898
|
+
}
|
|
899
|
+
stopAfterOwnershipLoss();
|
|
900
|
+
}, ownershipRefreshMs);
|
|
901
|
+
ownershipCheckInterval.unref?.();
|
|
902
|
+
ownershipRefreshInterval.unref?.();
|
|
903
|
+
};
|
|
904
|
+
const runOwnedPollingStart = async (ctx, options, isCurrent) => {
|
|
905
|
+
if (!isCurrent())
|
|
906
|
+
return false;
|
|
907
|
+
activeContext = ctx;
|
|
908
|
+
startOwnershipWatcher(ctx);
|
|
909
|
+
try {
|
|
910
|
+
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
911
|
+
stopOwnershipWatcher();
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
await options.onAcquired?.();
|
|
915
|
+
if (!isCurrent())
|
|
916
|
+
return false;
|
|
917
|
+
await deps.startPolling(ctx, options);
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
if (!isCurrent())
|
|
921
|
+
return false;
|
|
922
|
+
stopOwnershipWatcher();
|
|
923
|
+
try {
|
|
924
|
+
await deps.stopPolling();
|
|
925
|
+
}
|
|
926
|
+
catch (stopError) {
|
|
927
|
+
deps.recordRuntimeEvent?.("lock", stopError, {
|
|
928
|
+
phase: "startup-rollback",
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (!isCurrent())
|
|
932
|
+
return false;
|
|
933
|
+
deps.lock.release();
|
|
934
|
+
deps.onTransportAvailabilityChanged?.();
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
if (!isCurrent())
|
|
938
|
+
return false;
|
|
939
|
+
if (deps.lock.owns(ctx)) {
|
|
940
|
+
if (activeContext !== ctx)
|
|
941
|
+
return false;
|
|
942
|
+
deps.transportMonitor?.start(ctx);
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
stopOwnershipWatcher();
|
|
946
|
+
if (ownershipStop)
|
|
947
|
+
await ownershipStop;
|
|
948
|
+
if (!isCurrent())
|
|
949
|
+
return false;
|
|
950
|
+
await deps.stopPolling();
|
|
951
|
+
if (!isCurrent())
|
|
952
|
+
return false;
|
|
953
|
+
deps.onTransportAvailabilityChanged?.();
|
|
954
|
+
return false;
|
|
955
|
+
};
|
|
956
|
+
const canStartPolling = (ctx) => deps.canStartPolling?.(ctx) ?? true;
|
|
957
|
+
const formatStartBlockedMessage = (ctx) => deps.formatStartBlockedMessage?.(ctx) ??
|
|
958
|
+
"Telegram polling is unavailable in this Pi run mode.";
|
|
959
|
+
return {
|
|
960
|
+
start: async (ctx, options = {}) => {
|
|
961
|
+
if (!deps.hasBotToken()) {
|
|
962
|
+
return {
|
|
963
|
+
ok: false,
|
|
964
|
+
message: deps.getBotTokenDiagnostic?.() ?? "Telegram bot is not configured.",
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (!canStartPolling(ctx)) {
|
|
968
|
+
return { ok: false, message: formatStartBlockedMessage(ctx) };
|
|
969
|
+
}
|
|
970
|
+
const cancelled = {
|
|
971
|
+
ok: false,
|
|
972
|
+
canTakeover: false,
|
|
973
|
+
message: "Telegram polling startup was cancelled or superseded.",
|
|
974
|
+
};
|
|
975
|
+
if (deps.isContextCurrent?.(ctx) === false)
|
|
976
|
+
return cancelled;
|
|
977
|
+
const generation = ++pollingGeneration;
|
|
978
|
+
const isCurrent = () => generation === pollingGeneration &&
|
|
979
|
+
(deps.isContextCurrent?.(ctx) ?? true);
|
|
980
|
+
if (ownershipStop)
|
|
981
|
+
await ownershipStop;
|
|
982
|
+
if (!isCurrent())
|
|
983
|
+
return cancelled;
|
|
984
|
+
let acquired = deps.lock.acquire(ctx, {
|
|
985
|
+
force: options.force,
|
|
986
|
+
expectedOwner: options.election?.expectedOwner ??
|
|
987
|
+
(options.force ? takeoverCandidate : undefined),
|
|
988
|
+
election: options.election !== undefined,
|
|
989
|
+
});
|
|
990
|
+
if (!acquired.ok && !options.election) {
|
|
991
|
+
const currentState = deps.lock.getState();
|
|
992
|
+
if (currentState.kind === "active-here" &&
|
|
993
|
+
hasSameLockOwner(currentState.lock, acquired.lock)) {
|
|
994
|
+
acquired = deps.lock.acquire(ctx, {
|
|
995
|
+
force: true,
|
|
996
|
+
expectedOwner: acquired.lock,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (!acquired.ok) {
|
|
1001
|
+
takeoverCandidate = acquired.lock;
|
|
1002
|
+
if (options.election) {
|
|
1003
|
+
return {
|
|
1004
|
+
ok: false,
|
|
1005
|
+
canTakeover: false,
|
|
1006
|
+
owner: formatTelegramLockEntry(acquired.lock),
|
|
1007
|
+
message: "Telegram leadership election lost to another live owner.",
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (deps.registerFollowerWithOwner) {
|
|
1011
|
+
let failureMessage;
|
|
1012
|
+
try {
|
|
1013
|
+
const registered = await deps.registerFollowerWithOwner(ctx, acquired.lock);
|
|
1014
|
+
if (!isCurrent())
|
|
1015
|
+
return cancelled;
|
|
1016
|
+
if (registered) {
|
|
1017
|
+
deps.updateStatus(ctx);
|
|
1018
|
+
return { ok: true, canTakeover: false };
|
|
1019
|
+
}
|
|
1020
|
+
if (registered === false)
|
|
1021
|
+
failureMessage = "not registered";
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
failureMessage =
|
|
1025
|
+
error instanceof Error ? error.message : String(error);
|
|
1026
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1027
|
+
phase: "follower-register",
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (failureMessage) {
|
|
1031
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1032
|
+
return {
|
|
1033
|
+
ok: false,
|
|
1034
|
+
canTakeover: false,
|
|
1035
|
+
owner,
|
|
1036
|
+
message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${formatTelegramFollowerRegistrationFailure(failureMessage)}.`,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1041
|
+
return {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
canTakeover: true,
|
|
1044
|
+
owner,
|
|
1045
|
+
message: `Telegram bridge is active in another Pi instance (${owner}).`,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
takeoverCandidate = undefined;
|
|
1049
|
+
if (!(await runOwnedPollingStart(ctx, options, isCurrent))) {
|
|
1050
|
+
if (!isCurrent())
|
|
1051
|
+
return cancelled;
|
|
1052
|
+
return {
|
|
1053
|
+
ok: false,
|
|
1054
|
+
canTakeover: false,
|
|
1055
|
+
message: "Telegram leadership changed during polling startup.",
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (!isCurrent())
|
|
1059
|
+
return cancelled;
|
|
1060
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1061
|
+
deps.updateStatus(ctx);
|
|
1062
|
+
const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
|
|
1063
|
+
return { ok: true, message: `Telegram bridge connected.${staleSuffix}` };
|
|
1064
|
+
},
|
|
1065
|
+
stop: async () => {
|
|
1066
|
+
await suspendPolling();
|
|
1067
|
+
const state = deps.lock.release();
|
|
1068
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1069
|
+
if (state.kind === "active-elsewhere") {
|
|
1070
|
+
return `Telegram bridge is active in another Pi instance (${formatTelegramLockEntry(state.lock)}).`;
|
|
1071
|
+
}
|
|
1072
|
+
if (state.kind === "stale") {
|
|
1073
|
+
return `Removed stale Telegram bridge lock (${formatTelegramLockEntry(state.lock)}).`;
|
|
1074
|
+
}
|
|
1075
|
+
return "Telegram bridge disconnected.";
|
|
1076
|
+
},
|
|
1077
|
+
suspend: suspendPolling,
|
|
1078
|
+
onPersistentConflict: async (ctx, count) => {
|
|
1079
|
+
if (activeContext === undefined || ownershipStop)
|
|
1080
|
+
return;
|
|
1081
|
+
if (!(deps.isContextCurrent?.(ctx) ?? activeContext === ctx))
|
|
1082
|
+
return;
|
|
1083
|
+
activeContext = undefined;
|
|
1084
|
+
pollingGeneration += 1;
|
|
1085
|
+
stopOwnershipWatcher();
|
|
1086
|
+
deps.transportMonitor?.stop();
|
|
1087
|
+
let ownership = "unverifiable";
|
|
1088
|
+
const cleanupErrors = [];
|
|
1089
|
+
try {
|
|
1090
|
+
ownership = deps.lock.owns(snapshotLockContext(ctx)) ? "owned" : "lost";
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
cleanupErrors.push(String(error));
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
deps.lock.release();
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
ownership = "unverifiable";
|
|
1100
|
+
cleanupErrors.push(String(error));
|
|
1101
|
+
}
|
|
1102
|
+
ownershipStop = Promise.resolve()
|
|
1103
|
+
.then(() => deps.stopPolling())
|
|
1104
|
+
.catch((error) => { cleanupErrors.push(String(error)); })
|
|
1105
|
+
.finally(() => {
|
|
1106
|
+
ownershipStop = undefined;
|
|
1107
|
+
deps.recordRuntimeEvent?.("polling", ownership === "lost"
|
|
1108
|
+
? "Telegram transport stopped: local ownership lost; check for another Pi instance."
|
|
1109
|
+
: "Telegram transport stopped: competing getUpdates client or ownership mismatch.", {
|
|
1110
|
+
phase: "persistent-conflict", count, ownership,
|
|
1111
|
+
...(cleanupErrors.length ? { cleanupErrors } : {}),
|
|
1112
|
+
});
|
|
1113
|
+
deps.updateStatus(ctx);
|
|
1114
|
+
});
|
|
1115
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1116
|
+
await ownershipStop;
|
|
1117
|
+
},
|
|
1118
|
+
onSessionStart: async (_event, ctx) => {
|
|
1119
|
+
if (!deps.hasBotToken())
|
|
1120
|
+
return;
|
|
1121
|
+
if (!canStartPolling(ctx))
|
|
1122
|
+
return;
|
|
1123
|
+
const ownsCurrentLock = deps.lock.owns(ctx);
|
|
1124
|
+
const state = ownsCurrentLock ? undefined : deps.lock.getState();
|
|
1125
|
+
const canResumeStaleSameCwd = state?.kind === "stale" && state.lock.cwd === ctx.cwd;
|
|
1126
|
+
const canHandoffSameProcess = state?.kind === "active-here" &&
|
|
1127
|
+
(!state.lock.cwd || state.lock.cwd === ctx.cwd);
|
|
1128
|
+
const canRestoreRememberedFollower = state?.kind === "active-elsewhere" &&
|
|
1129
|
+
deps.restoreFollowerWithOwner !== undefined;
|
|
1130
|
+
if (!ownsCurrentLock &&
|
|
1131
|
+
!canResumeStaleSameCwd &&
|
|
1132
|
+
!canHandoffSameProcess &&
|
|
1133
|
+
!canRestoreRememberedFollower) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (deps.isContextCurrent?.(ctx) === false)
|
|
1137
|
+
return;
|
|
1138
|
+
const generation = ++pollingGeneration;
|
|
1139
|
+
const isCurrent = () => generation === pollingGeneration &&
|
|
1140
|
+
(deps.isContextCurrent?.(ctx) ?? true);
|
|
1141
|
+
const startedAtMs = Date.now();
|
|
1142
|
+
deps.recordRuntimeEvent?.("lock", "Telegram auto-start scheduled", {
|
|
1143
|
+
phase: "auto-start-scheduled",
|
|
1144
|
+
mode: canRestoreRememberedFollower ? "follower-restore" : "leader",
|
|
1145
|
+
});
|
|
1146
|
+
const run = (async () => {
|
|
1147
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1148
|
+
if (ownershipStop)
|
|
1149
|
+
await ownershipStop;
|
|
1150
|
+
if (!isCurrent())
|
|
1151
|
+
return;
|
|
1152
|
+
if (canRestoreRememberedFollower && state?.kind === "active-elsewhere") {
|
|
1153
|
+
const restored = await deps.restoreFollowerWithOwner?.(ctx, state.lock);
|
|
1154
|
+
if (!isCurrent() || !restored)
|
|
1155
|
+
return;
|
|
1156
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1157
|
+
deps.updateStatus(ctx);
|
|
1158
|
+
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect completed", { phase: "follower-auto-connect" });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (canResumeStaleSameCwd || canHandoffSameProcess) {
|
|
1162
|
+
const acquired = deps.lock.acquire(ctx, canHandoffSameProcess
|
|
1163
|
+
? { force: true, expectedOwner: state?.lock }
|
|
1164
|
+
: undefined);
|
|
1165
|
+
if (!acquired.ok)
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (!isCurrent())
|
|
1169
|
+
return;
|
|
1170
|
+
if (!(await runOwnedPollingStart(ctx, {}, isCurrent)))
|
|
1171
|
+
return;
|
|
1172
|
+
if (!isCurrent())
|
|
1173
|
+
return;
|
|
1174
|
+
deps.onTransportAvailabilityChanged?.();
|
|
1175
|
+
deps.updateStatus(ctx);
|
|
1176
|
+
deps.recordRuntimeEvent?.("lock", "Telegram auto-start completed", {
|
|
1177
|
+
phase: "auto-start-complete",
|
|
1178
|
+
durationMs: Date.now() - startedAtMs,
|
|
1179
|
+
});
|
|
1180
|
+
})()
|
|
1181
|
+
.catch((error) => {
|
|
1182
|
+
deps.recordRuntimeEvent?.("lock", error, { phase: "auto-start" });
|
|
1183
|
+
})
|
|
1184
|
+
.finally(() => {
|
|
1185
|
+
if (sessionAutoStartRun === run)
|
|
1186
|
+
sessionAutoStartRun = undefined;
|
|
1187
|
+
});
|
|
1188
|
+
sessionAutoStartRun = run;
|
|
1189
|
+
},
|
|
1190
|
+
registerFollowerWithOwner: deps.registerFollowerWithOwner
|
|
1191
|
+
? async (ctx, owner) => {
|
|
1192
|
+
const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
|
|
1193
|
+
if (registered)
|
|
1194
|
+
deps.updateStatus(ctx);
|
|
1195
|
+
return registered === true;
|
|
1196
|
+
}
|
|
1197
|
+
: undefined,
|
|
1198
|
+
restoreFollowerWithOwner: deps.restoreFollowerWithOwner
|
|
1199
|
+
? async (ctx, owner) => {
|
|
1200
|
+
const restored = await deps.restoreFollowerWithOwner?.(ctx, owner);
|
|
1201
|
+
if (restored)
|
|
1202
|
+
deps.updateStatus(ctx);
|
|
1203
|
+
return restored === true;
|
|
1204
|
+
}
|
|
1205
|
+
: undefined,
|
|
1206
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
1207
|
+
};
|
|
1208
|
+
}
|