@llblab/pi-telegram 0.26.5 → 0.26.7
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 +1 -1
- package/BACKLOG.md +27 -2
- package/CHANGELOG.md +10 -0
- package/docs/multi-instance-bus.md +1 -1
- package/lib/bus-follower.ts +28 -3
- package/lib/bus-leader.ts +22 -5
- package/lib/bus.ts +53 -11
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
84
84
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
85
85
|
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission by itself to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for confirmed manual disconnect, graceful Pi quit, or confirmed reconciliation cases. A stale follower may become a confirmed reconciliation case only when Thread cleanup is enabled, the OS reports the exact registered PID absent, and cleanup serializes ahead of replacement registration; live/unknown PID state remains non-destructive. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` retains destructive confirmation; graceful Pi `quit` invokes the same teardown without prompting when top-level `threads.automaticCleanup` is enabled (default), while disabling it and `reload`, `new`, `resume`, or `fork` preserve the binding through safe handoff/suspension. A leader deletes its own bound thread before releasing transport ownership, while a follower sends an authenticated exact-registration-generation disconnect envelope and waits for the leader to delete its assigned thread, mark the durable binding offline, and remove live routing authority before unregistering. Cleanup must be confirmed by successful deletion or explicit already-gone evidence. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before destructive API work; interruption preserves that intent for the current or successor leader to replay under its own epoch, while leader startup must first adopt a still-active same-profile binding and cancel its superseded cleanup before any destructive API call. Confirmation removes both the binding and intent atomically from the next persisted snapshot. Silence, heartbeat expiry, process absence, IPC failure, and auth mismatch create no cleanup intent by themselves; only stale heartbeat combined with enabled cleanup and OS-confirmed absence of the exact registered follower PID may authorize fallback intent. Incomplete manual cleanup preserves binding/routing state, keeps the session open, and returns actionable retry guidance. A promoted leader authorizes inherited follower-thread cleanup by its current leader epoch rather than the historical owner label. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
|
|
86
86
|
- Runtime routing binds to a live `instanceId` plus its exact follower registration generation; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Persisted manual-follower records may act as stable restart hints, but only an authenticated live registry entry authorizes inbound forwarding or outbound transport; stale session records must not be treated as live owners. A live instance owns at most one active bound thread; provisioning a new target for the same `instanceId` must remove older current-state bindings and close duplicate Telegram threads when possible. A reload creates a new runtime `instanceId`; if the previous leader slot is still known/alive, keep it occupied in reservations and assign the new leader the next free slot instead of creating a duplicate same-letter tab or blocking startup on Telegram UI convergence.
|
|
87
|
-
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Registration requires a present exact generation. Cross-session target reuse and any carried target absent from current bindings require one synchronous visibility probe: explicit stale evidence may provision a replacement, while ambiguous/non-stale failure must reject registration without replay, binding invalidation, or speculative replacement. Persist an ambiguously observed absent target only as non-routable `probe-required` restoration evidence so targetless retries and leader reloads must probe the exact target again before activation or replacement. Registration and explicit disconnect mutations for one durable follower profile must serialize across awaited cleanup so an old runtime's disconnect cannot delete or unregister a replacement generation. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread;
|
|
87
|
+
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Registration requires a present exact generation. Cross-session target reuse and any carried target absent from current bindings require one synchronous visibility probe: explicit stale evidence may provision a replacement, while ambiguous/non-stale failure must reject registration without replay, binding invalidation, or speculative replacement. Persist an ambiguously observed absent target only as non-routable `probe-required` restoration evidence so targetless retries and leader reloads must probe the exact target again before activation or replacement. Registration and explicit disconnect mutations for one durable follower profile must serialize across awaited cleanup so an old runtime's disconnect cannot delete or unregister a replacement generation. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread; derive stable process-birth identity from OS metadata where available, and carry a fresh authenticated previous-runtime-plus-exact-target handoff through initial registration so a changed identity migrates rather than provisions. Do not key durable follower ownership only by the short-lived Pi process id. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
|
|
88
88
|
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints unless enabled Thread cleanup turns stale heartbeat plus exact-PID death confirmation into fenced cleanup authority; otherwise only explicit stale/deleted/offline evidence invalidates them. Successful target reuse must refresh the binding timestamp and replacement runtime identity; generic leader startup or heartbeat pruning alone must not erase the target merely because no follower registration is currently live. An authenticated live follower whose carried exact target is missing from current persisted bindings should recover that target instead of creating another Telegram thread; preserve a carried slot only when it does not collide with another current record. Preserve a cursor that belongs to a live or recoverable binding and continue skipping retained restart hints plus unexpired pending/reserved slots. Explicit stale/deleted/offline reconciliation may release that slot; generic process absence may not. The alphabet cap is a feature — compact single-letter tabs are scannable.
|
|
89
89
|
- Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status, live diagnostics, and `[telegram|thread:name]` through one target-aware current-instance identity resolver. Registered follower/leader metadata takes precedence over a stale shared record for the same target, with the record used only as fallback; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
|
|
90
90
|
- Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread or process launcher, and leader activation proactively creates or reuses its private-DM thread. When the owner writes in an unknown thread, the leader reclaims and routes the first one only if it lacks an active binding; later unknown threads remain intact and receive an in-thread chooser to reroute the captured prompt or restore a stale binding. Destructive cleanup requires explicit confirmation through `thread-reconciler`; another Pi instance still requires a manually started process followed by `/telegram-connect`. Without Threaded Mode, process the message through classic routing.
|
package/BACKLOG.md
CHANGED
|
@@ -2,7 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
_This backlog tracks only open release-relevant work: hotfixes, bounded maintenance, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
|
|
4
4
|
|
|
5
|
-
## P0 —
|
|
5
|
+
## P0 — macOS Follower Reload Identity Hotfix
|
|
6
|
+
|
|
7
|
+
Context: macOS lacks Linux `/proc` start ticks, so a follower previously received a generation-based profile identity on each extension reload. Initial registration could also race ahead of session refresh before the old target was restored, causing a duplicate Telegram thread.
|
|
8
|
+
|
|
9
|
+
Open work:
|
|
10
|
+
|
|
11
|
+
- [x] Derive stable Darwin process-birth identity from `/bin/ps` while preserving Linux start ticks and generation fallback.
|
|
12
|
+
- [x] Carry a fresh previous runtime identity and exact target through initial follower registration, migrate the old binding after a visibility probe, and consume the handoff only after acknowledgement.
|
|
13
|
+
- [x] Replace the helper-only regression with a runtime-level registration test covering failed-attempt retention, envelope evidence, acknowledgement, and handoff consumption.
|
|
14
|
+
- [x] Update durable context, multi-instance documentation, changelog, and package metadata.
|
|
15
|
+
- [ ] Validate on Linux, macOS, and Windows; publish and verify `0.26.7`.
|
|
16
|
+
|
|
17
|
+
Done when: same-process follower reload preserves one thread and binding, the race is covered at the runtime boundary, all hosted platforms pass, and the hotfix is published.
|
|
18
|
+
|
|
19
|
+
## P1 — Windows Poll-Wait Boundary Release
|
|
20
|
+
|
|
21
|
+
Context: a Windows process-lock regression reached its expected `parent:getUpdates` marker at the polling deadline, but the shared wait helper failed immediately after its final read without applying the predicate to that last value. This produces a false timeout despite successful behavior.
|
|
22
|
+
|
|
23
|
+
Open work:
|
|
24
|
+
|
|
25
|
+
- [x] Reapply the predicate after the final file read before reporting a timeout.
|
|
26
|
+
- [x] Validate the process-lock regression across hosted platforms and publish `0.26.6`.
|
|
27
|
+
|
|
28
|
+
Done when: deadline-edge marker arrival succeeds, genuine missing markers still fail with evidence, and CI passes on Linux, macOS, and Windows.
|
|
29
|
+
|
|
30
|
+
## P1 — Scoped Model Thinking Label Release
|
|
6
31
|
|
|
7
32
|
Context: scoped model buttons still separate their optional thinking level with a middle dot. Parentheses match the quieter subordinate-metadata convention now used by Activity update omission labels.
|
|
8
33
|
|
|
@@ -10,7 +35,7 @@ Open work:
|
|
|
10
35
|
|
|
11
36
|
- [x] Render scoped model buttons as `provider/model (level)` when a thinking level is attached.
|
|
12
37
|
- [x] Update focused regression, changelog, and package metadata.
|
|
13
|
-
- [
|
|
38
|
+
- [x] Publish and verify GitHub/npm release evidence for `0.26.5`.
|
|
14
39
|
|
|
15
40
|
Done when: optional thinking metadata uses parentheses consistently, validation passes, and the hotfix is published.
|
|
16
41
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.7: macOS Follower Reload Identity
|
|
4
|
+
|
|
5
|
+
- `macOS Identity`: Derived manual-follower process-birth identity from the parent process start time on Darwin while preserving Linux `/proc` start ticks and the generation fallback. Impact: extension reloads in one macOS Pi process retain a stable durable profile instead of looking like new followers.
|
|
6
|
+
- `Follower Handoff`: Carried the previous runtime identity and exact target through initial registration, then migrated the existing binding to the current profile identity after the required visibility probe. The handoff remains available after failed registration and clears only after acknowledgement. Impact: initial registration can no longer race session refresh into provisioning a duplicate Telegram thread.
|
|
7
|
+
- `Regression Depth`: Replaced the helper-only handoff check with a runtime-level registration regression covering failed-attempt retention, serialized envelope evidence, successful acknowledgement, and handoff consumption. Impact: the test now exercises the race-fixing boundary rather than its projection helper.
|
|
8
|
+
|
|
9
|
+
## 0.26.6: Windows Poll-Wait Boundary Fix
|
|
10
|
+
|
|
11
|
+
- `Windows CI`: Reapplied the wait predicate to the final marker-file read before reporting a timeout. Impact: process-lock regressions no longer fail when Windows exposes the expected marker exactly at the polling deadline, while genuinely missing evidence still produces the same diagnostic failure.
|
|
12
|
+
|
|
3
13
|
## 0.26.5: Quieter Scoped Model Labels
|
|
4
14
|
|
|
5
15
|
- `Model Microcopy`: Replaced the middle-dot separator in scoped model buttons with parentheses, rendering optional thinking metadata as `provider/model (level)`. Impact: model identity stays primary while its attached thinking level reads as subordinate metadata consistently with Activity omission labels.
|
|
@@ -207,7 +207,7 @@ Current portability audit:
|
|
|
207
207
|
- Ownership/config/state/temp files: path construction uses `path.join`/`path.resolve` under the Pi agent directory. File permission calls remain best-effort private-mode hardening; native Windows may emulate POSIX modes, so broad Windows ACL auditing is outside this extension's current local-bus baseline.
|
|
208
208
|
- Process liveness: lock ownership uses `process.kill(pid, 0)`, which Node supports on Windows for existence checks. Cross-user permission failures are treated as alive, matching Unix semantics.
|
|
209
209
|
- Shell/provider commands: outbound handler command templates remain operator-configured and platform-dependent; Threaded Mode bus portability does not guarantee every configured STT/TTS/shell provider is Windows-native.
|
|
210
|
-
- Manual follower identity: process ids are
|
|
210
|
+
- Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time; inaccessible metadata falls back to the extension generation. A fresh authenticated session handoff carries the previous runtime identity and exact target through initial registration, allowing fallback identities to migrate without provisioning another thread.
|
|
211
211
|
|
|
212
212
|
Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.
|
|
213
213
|
|
package/lib/bus-follower.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
95
95
|
registerWithLeader: (
|
|
96
96
|
ctx: TContext,
|
|
97
97
|
leader: { busSocketPath?: string; busSecret?: string },
|
|
98
|
-
options?: { target?: TelegramTarget },
|
|
98
|
+
options?: { target?: TelegramTarget; previousInstanceId?: string },
|
|
99
99
|
) => Promise<boolean>;
|
|
100
100
|
setContext: (ctx: TContext) => void;
|
|
101
101
|
disconnectFromLeader?: () => Promise<boolean>;
|
|
@@ -815,7 +815,10 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
|
|
|
815
815
|
const restored = await deps.registrationRuntime.registerWithLeader(
|
|
816
816
|
ctx,
|
|
817
817
|
lockState.lock,
|
|
818
|
-
{
|
|
818
|
+
{
|
|
819
|
+
target: handoff.target,
|
|
820
|
+
previousInstanceId: handoff.instanceId,
|
|
821
|
+
},
|
|
819
822
|
);
|
|
820
823
|
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
821
824
|
if (restored) {
|
|
@@ -1240,6 +1243,18 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1240
1243
|
};
|
|
1241
1244
|
return {
|
|
1242
1245
|
registerWithLeader: async (ctx, leader, options) => {
|
|
1246
|
+
const pendingHandoff = options
|
|
1247
|
+
? undefined
|
|
1248
|
+
: getTelegramFollowerSessionHandoff();
|
|
1249
|
+
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
|
|
1250
|
+
pendingHandoff,
|
|
1251
|
+
)
|
|
1252
|
+
? {
|
|
1253
|
+
target: pendingHandoff.target,
|
|
1254
|
+
previousInstanceId: pendingHandoff.instanceId,
|
|
1255
|
+
}
|
|
1256
|
+
: undefined;
|
|
1257
|
+
const registrationOptions = options ?? pendingHandoffOptions;
|
|
1243
1258
|
const leaderSocketPath =
|
|
1244
1259
|
leader.busSocketPath ??
|
|
1245
1260
|
deps.getLeaderSocketPath?.() ??
|
|
@@ -1257,6 +1272,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1257
1272
|
auth: activeAuthSecret,
|
|
1258
1273
|
registration: {
|
|
1259
1274
|
instanceId: deps.instanceId,
|
|
1275
|
+
...(registrationOptions?.previousInstanceId
|
|
1276
|
+
? { previousInstanceId: registrationOptions.previousInstanceId }
|
|
1277
|
+
: {}),
|
|
1260
1278
|
profileKey:
|
|
1261
1279
|
deps.getProfileKey?.(ctx) ??
|
|
1262
1280
|
(ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
|
|
@@ -1271,7 +1289,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1271
1289
|
cwd: ctx.cwd,
|
|
1272
1290
|
pid: getPid(),
|
|
1273
1291
|
target:
|
|
1274
|
-
|
|
1292
|
+
registrationOptions?.target ??
|
|
1275
1293
|
deps.registrationState?.getTarget() ??
|
|
1276
1294
|
lastKnownTarget,
|
|
1277
1295
|
busSocketPath:
|
|
@@ -1344,6 +1362,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1344
1362
|
activeContext = ctx;
|
|
1345
1363
|
await sendHeartbeat();
|
|
1346
1364
|
startHeartbeat(leaderSocketPath);
|
|
1365
|
+
if (
|
|
1366
|
+
pendingHandoffOptions &&
|
|
1367
|
+
getTelegramFollowerSessionHandoff()?.instanceId ===
|
|
1368
|
+
pendingHandoffOptions.previousInstanceId
|
|
1369
|
+
) {
|
|
1370
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
1371
|
+
}
|
|
1347
1372
|
return true;
|
|
1348
1373
|
}
|
|
1349
1374
|
stopHeartbeat();
|
package/lib/bus-leader.ts
CHANGED
|
@@ -366,13 +366,22 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
366
366
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
367
367
|
const requestedTarget = registration.target;
|
|
368
368
|
const reconnectRecord = recordsBeforeProvision.find((record) => {
|
|
369
|
+
const matchesRequestedTarget =
|
|
370
|
+
!requestedTarget ||
|
|
371
|
+
(record.target.chatId === requestedTarget.chatId &&
|
|
372
|
+
record.target.threadId === requestedTarget.threadId);
|
|
373
|
+
const matchesCurrentIdentity =
|
|
374
|
+
record.instanceId === registration.instanceId ||
|
|
375
|
+
record.profileKey === followerProfileKey;
|
|
376
|
+
const matchesSessionHandoff =
|
|
377
|
+
!!requestedTarget &&
|
|
378
|
+
!!registration.previousInstanceId &&
|
|
379
|
+
record.instanceId === registration.previousInstanceId &&
|
|
380
|
+
matchesRequestedTarget;
|
|
369
381
|
return (
|
|
370
382
|
record.owner?.kind === "manual-follower" &&
|
|
371
|
-
(
|
|
372
|
-
|
|
373
|
-
(!requestedTarget ||
|
|
374
|
-
(record.target.chatId === requestedTarget.chatId &&
|
|
375
|
-
record.target.threadId === requestedTarget.threadId))
|
|
383
|
+
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
384
|
+
matchesSessionHandoff)
|
|
376
385
|
);
|
|
377
386
|
});
|
|
378
387
|
const followerOwner =
|
|
@@ -539,6 +548,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
539
548
|
const nowMs = getNowMs();
|
|
540
549
|
const transferredRecord = deps.topicTargetStore.upsert({
|
|
541
550
|
...reconnectRecord,
|
|
551
|
+
profileKey: followerProfileKey,
|
|
552
|
+
owner:
|
|
553
|
+
followerOwner.kind === "manual-follower"
|
|
554
|
+
? followerOwner
|
|
555
|
+
: {
|
|
556
|
+
kind: "manual-follower",
|
|
557
|
+
instanceId: registration.instanceId,
|
|
558
|
+
},
|
|
542
559
|
instanceId: registration.instanceId,
|
|
543
560
|
updatedAtMs: nowMs,
|
|
544
561
|
lastSyncObservedAtMs: nowMs,
|
package/lib/bus.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* cross-instance forwarding helpers, and the live follower registry model.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
10
|
import {
|
|
10
11
|
chmodSync,
|
|
@@ -51,22 +52,59 @@ export interface TelegramBusProcessRuntime {
|
|
|
51
52
|
getFollowerSocketPath: () => string;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export interface TelegramProcessBirthIdentityOptions {
|
|
56
|
+
platform?: NodeJS.Platform;
|
|
57
|
+
readProcStat?: (pid: number) => string;
|
|
58
|
+
readDarwinProcessStart?: (pid: number) => string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readDarwinProcessStart(pid: number): string {
|
|
62
|
+
return execFileSync(
|
|
63
|
+
"/bin/ps",
|
|
64
|
+
["-o", "lstart=", "-p", String(pid)],
|
|
65
|
+
{
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
},
|
|
69
|
+
).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
export function getTelegramProcessBirthIdentity(
|
|
55
73
|
pid: number,
|
|
56
74
|
fallbackGeneration: number | string,
|
|
75
|
+
options: TelegramProcessBirthIdentityOptions = {},
|
|
57
76
|
): string {
|
|
58
77
|
if (pid > 0) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
const platform = options.platform ?? getPlatform();
|
|
79
|
+
if (platform === "linux") {
|
|
80
|
+
try {
|
|
81
|
+
const stat = (options.readProcStat ?? ((targetPid) =>
|
|
82
|
+
readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
|
|
83
|
+
const closeParen = stat.lastIndexOf(")");
|
|
84
|
+
const fields = stat
|
|
85
|
+
.slice(closeParen + 2)
|
|
86
|
+
.trim()
|
|
87
|
+
.split(/\s+/u);
|
|
88
|
+
const startTicks = fields[19];
|
|
89
|
+
if (startTicks) return `${pid}:start:${startTicks}`;
|
|
90
|
+
} catch {
|
|
91
|
+
/* inaccessible process metadata */
|
|
92
|
+
}
|
|
93
|
+
} else if (platform === "darwin") {
|
|
94
|
+
try {
|
|
95
|
+
const startedAt = (
|
|
96
|
+
options.readDarwinProcessStart ?? readDarwinProcessStart
|
|
97
|
+
)(pid);
|
|
98
|
+
if (startedAt) {
|
|
99
|
+
const fingerprint = createHash("sha256")
|
|
100
|
+
.update(startedAt)
|
|
101
|
+
.digest("hex")
|
|
102
|
+
.slice(0, 16);
|
|
103
|
+
return `${pid}:start:${fingerprint}`;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
/* inaccessible process metadata */
|
|
107
|
+
}
|
|
70
108
|
}
|
|
71
109
|
}
|
|
72
110
|
return `${pid}:generation:${fallbackGeneration}`;
|
|
@@ -145,6 +183,7 @@ export function getTelegramBusFollowerSocketPath(
|
|
|
145
183
|
|
|
146
184
|
export interface TelegramBusInstanceRegistration {
|
|
147
185
|
instanceId: string;
|
|
186
|
+
previousInstanceId?: string;
|
|
148
187
|
profileKey?: string;
|
|
149
188
|
threadName?: string;
|
|
150
189
|
slot?: string;
|
|
@@ -1561,6 +1600,9 @@ function parseRegistration(
|
|
|
1561
1600
|
instanceId: value.instanceId,
|
|
1562
1601
|
connectedAtMs: value.connectedAtMs,
|
|
1563
1602
|
};
|
|
1603
|
+
if (typeof value.previousInstanceId === "string") {
|
|
1604
|
+
registration.previousInstanceId = value.previousInstanceId;
|
|
1605
|
+
}
|
|
1564
1606
|
if (typeof value.profileKey === "string")
|
|
1565
1607
|
registration.profileKey = value.profileKey;
|
|
1566
1608
|
if (typeof value.threadName === "string")
|