@llblab/pi-telegram 0.53.2 → 0.54.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 +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/lib/bus-follower.d.ts +4 -0
- package/dist/lib/bus-follower.js +7 -0
- package/dist/lib/bus-leader.js +8 -0
- package/dist/lib/bus.d.ts +35 -0
- package/dist/lib/bus.js +81 -3
- package/dist/lib/config.d.ts +0 -6
- package/dist/lib/config.js +0 -1
- package/dist/lib/extension.js +9 -2
- package/dist/lib/journal.d.ts +8 -0
- package/dist/lib/journal.js +18 -6
- package/dist/lib/locks.d.ts +7 -0
- package/dist/lib/locks.js +72 -35
- package/dist/lib/status.js +9 -0
- package/dist/package.json +1 -1
- package/docs/architecture.md +12 -12
- package/docs/multi-instance-bus.md +15 -15
- package/lib/bus-follower.ts +11 -0
- package/lib/bus-leader.ts +8 -0
- package/lib/bus.ts +124 -3
- package/lib/config.ts +0 -8
- package/lib/extension.ts +11 -2
- package/lib/journal.ts +26 -5
- package/lib/locks.ts +97 -38
- package/lib/status.ts +9 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -76,7 +76,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
76
76
|
- Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
|
|
77
77
|
- Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
|
|
78
78
|
- Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
|
|
79
|
-
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register
|
|
79
|
+
- Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
|
|
80
80
|
- Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
|
|
81
81
|
- Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
|
|
82
82
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.54.0: No periodic disk writes while idle
|
|
8
|
+
|
|
9
|
+
- `Journal serialization`: The update journal no longer borrows the `telegram.json` lock. Journal operations serialize on their own `tmp/pi-telegram/runtime/journals.transaction`, so receiving messages and repairing journals never touch the config file or its guard; only config writes and sender admission still take it. Bus protocol moves to v3 so 0.53.x peers, which serialize on the old lock, cannot register alongside 0.54 writers.
|
|
10
|
+
- `Idle status writes`: The persisted runtime status no longer stores poll-cycle timestamps, so an idle connected session stops rewriting `state.json` after every long-poll response.
|
|
11
|
+
- `Leader liveness`: The leader no longer rewrites `state.json` every two seconds. A dead PID is taken over at once; a live but frozen Threaded Mode leader is replaced only after an authenticated `bus.probe` proves it unresponsive, bound to that exact owner. Classic mode never takes over a live PID silently: `/telegram-connect` asks first. An idle connected session now makes no periodic disk writes.
|
|
12
|
+
|
|
7
13
|
## 0.53.2: Idle runtime without lock churn
|
|
8
14
|
|
|
9
15
|
- `Idle disk load`: Polling-cursor and status reads of the update journal no longer open config and journal transactions, so a connected idle session stops creating `telegram.json.transaction.staged.*` and `inbox.json.transaction.staged.*` guard folders; only reads that need repair take the serialized path. Unchanged runtime status snapshots are compared before locking and skip their `state.json` transaction.
|
|
@@ -179,6 +179,8 @@ export declare function createTelegramManualFollowerProfileKeyResolver(input: {
|
|
|
179
179
|
}): () => string;
|
|
180
180
|
export interface TelegramBusFollowerElection {
|
|
181
181
|
expectedOwner?: TelegramLockEntry;
|
|
182
|
+
/** The expected owner is bus-proven unresponsive; the lock CAS still requires it to be the current owner. */
|
|
183
|
+
unresponsive?: boolean;
|
|
182
184
|
}
|
|
183
185
|
export type TelegramBusFollowerPromotionHandler<TContext> = (ctx: TContext, binding: TelegramBusFollowerPromotedBinding, election: TelegramBusFollowerElection) => Promise<boolean>;
|
|
184
186
|
type TelegramBusFollowerWorkspaceAdmissionDeps = {
|
|
@@ -227,6 +229,8 @@ export interface TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext> {
|
|
|
227
229
|
scheduleRetry?: (retry: () => void, delayMs: number) => void;
|
|
228
230
|
getActiveContext?: () => TContext | undefined;
|
|
229
231
|
promotionGraceMs?: number;
|
|
232
|
+
/** Bus liveness proof for a live-PID leader that no longer answers; replaces the retired file heartbeat. */
|
|
233
|
+
proveLeaderUnresponsive?: (owner: TelegramLockEntry) => Promise<boolean>;
|
|
230
234
|
recordRuntimeEvent: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
231
235
|
}
|
|
232
236
|
export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
package/dist/lib/bus-follower.js
CHANGED
|
@@ -1357,6 +1357,13 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
1357
1357
|
if (await tryRegisterWithLeader(ctx, graceState.lock, "follower-register-restore-grace", initialBinding)) {
|
|
1358
1358
|
return;
|
|
1359
1359
|
}
|
|
1360
|
+
if (await deps.proveLeaderUnresponsive?.(graceState.lock)) {
|
|
1361
|
+
await promoteToLeader(error, ctx, initialBinding, {
|
|
1362
|
+
expectedOwner: graceState.lock,
|
|
1363
|
+
unresponsive: true,
|
|
1364
|
+
});
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1360
1367
|
deps.setLifecyclePhase(undefined);
|
|
1361
1368
|
safeUpdateStatus(ctx);
|
|
1362
1369
|
deps.recordRuntimeEvent("bus", "Telegram follower promotion blocked by live leader lease", {
|
package/dist/lib/bus-leader.js
CHANGED
|
@@ -2184,6 +2184,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2184
2184
|
return createUnauthorizedBusAck(envelope.requestId);
|
|
2185
2185
|
}
|
|
2186
2186
|
switch (envelope.kind) {
|
|
2187
|
+
case "bus.probe":
|
|
2188
|
+
// Liveness only: no registry, routing, journal or Bot API effect.
|
|
2189
|
+
return {
|
|
2190
|
+
kind: "bus.ack",
|
|
2191
|
+
requestId: envelope.requestId,
|
|
2192
|
+
ok: true,
|
|
2193
|
+
protocol: deps.protocolIdentity,
|
|
2194
|
+
};
|
|
2187
2195
|
case "follower.register":
|
|
2188
2196
|
case "follower.restoreWorkspace": {
|
|
2189
2197
|
const compatibility = getTelegramBusProtocolCompatibility({
|
package/dist/lib/bus.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type TelegramBusEndpointLayout, type TelegramBusTransportEventRecorder, type TelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
8
8
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
9
|
+
import { type TelegramLockEntry, type TelegramLockState } from "./locks.ts";
|
|
9
10
|
import { type TelegramQueueHandoffPayload } from "./queue.ts";
|
|
10
11
|
import { type TelegramTarget } from "./target.ts";
|
|
11
12
|
import { type TelegramSessionReplacementIntent } from "./threads.ts";
|
|
@@ -525,6 +526,10 @@ export type TelegramBusEnvelope = ({
|
|
|
525
526
|
method: string;
|
|
526
527
|
args: unknown[];
|
|
527
528
|
sentAtMs: number;
|
|
529
|
+
} | {
|
|
530
|
+
/** Side-effect-free leader liveness probe; any acknowledgement proves a responsive event loop. */
|
|
531
|
+
kind: "bus.probe";
|
|
532
|
+
requestId: string;
|
|
528
533
|
} | {
|
|
529
534
|
kind: "bus.ack";
|
|
530
535
|
requestId: string;
|
|
@@ -558,6 +563,36 @@ export declare function createTelegramBusRequestId(input: {
|
|
|
558
563
|
export declare function createTelegramBusRequestIdFactory(instanceId: string): () => string;
|
|
559
564
|
export declare function encodeTelegramBusEnvelope(envelope: TelegramBusEnvelope): string;
|
|
560
565
|
export declare function parseTelegramBusEnvelope(line: string): TelegramBusEnvelope | undefined;
|
|
566
|
+
/**
|
|
567
|
+
* One leader liveness observation. `silent` (connected or connecting, no answer within the window) and
|
|
568
|
+
* `unreachable` (an endpoint that refuses connections: nobody listens) can support a takeover. A missing endpoint is
|
|
569
|
+
* `unknown`: a live classic-mode leader has no socket at all. Any bytes or an orderly close prove the peer's event
|
|
570
|
+
* loop handled the connection, so even an older leader rejecting the unknown kind is `responsive`.
|
|
571
|
+
*/
|
|
572
|
+
export type TelegramBusLeaderProbeResult = "responsive" | "silent" | "unreachable" | "unknown";
|
|
573
|
+
export declare function probeTelegramBusLeader(input: {
|
|
574
|
+
socketPath: TelegramBusSocketPathSource;
|
|
575
|
+
secret?: string;
|
|
576
|
+
timeoutMs?: number;
|
|
577
|
+
}): Promise<TelegramBusLeaderProbeResult>;
|
|
578
|
+
/**
|
|
579
|
+
* Takeover evidence replacing the retired file heartbeat: a full silent window, or an unreachable endpoint
|
|
580
|
+
* confirmed again after one more window (a just-started leader binds its socket well within it). The owner must
|
|
581
|
+
* stay the same throughout; the lock acquisition then CAS-checks that exact owner.
|
|
582
|
+
*/
|
|
583
|
+
export declare function proveTelegramBusLeaderUnresponsive(input: {
|
|
584
|
+
probe: () => Promise<TelegramBusLeaderProbeResult>;
|
|
585
|
+
isSameOwner: () => boolean;
|
|
586
|
+
sleep: (ms: number) => Promise<void>;
|
|
587
|
+
windowMs?: number;
|
|
588
|
+
}): Promise<boolean>;
|
|
589
|
+
/** Composition-ready takeover proof for one profile's leader lock and endpoint. */
|
|
590
|
+
export declare function createTelegramBusLeaderUnresponsivenessProof(deps: {
|
|
591
|
+
getLeaderState: () => TelegramLockState;
|
|
592
|
+
getLeaderSocketPath: () => string;
|
|
593
|
+
probe?: typeof probeTelegramBusLeader;
|
|
594
|
+
sleep?: (ms: number) => Promise<void>;
|
|
595
|
+
}): (owner: TelegramLockEntry) => Promise<boolean>;
|
|
561
596
|
interface TelegramBusLocalServer {
|
|
562
597
|
start: () => Promise<void>;
|
|
563
598
|
stop: () => Promise<void>;
|
package/dist/lib/bus.js
CHANGED
|
@@ -12,7 +12,7 @@ import { platform as getPlatform, tmpdir } from "node:os";
|
|
|
12
12
|
import { basename, dirname, join } from "node:path";
|
|
13
13
|
import { isDeepStrictEqual } from "node:util";
|
|
14
14
|
import { classifyTelegramBusTransportError, createTelegramBusTransportTimeoutError, delayTelegramBusTransportRetry, getTelegramBusEndpointDiagnostics, getTelegramBusFollowerEndpoint, getTelegramBusLeaderEndpoint, getTelegramBusPipePath, getTelegramBusTransportRetryPolicy, isRetryableTelegramBusTransportError, isTelegramBusPipePath, probeTelegramBusEndpoint, TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES, } from "./bus-transport.js";
|
|
15
|
-
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS } from "./locks.js";
|
|
15
|
+
import { isSameTelegramLockOwner, TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
16
16
|
import { resolveAgentDir } from "./paths.js";
|
|
17
17
|
import { getTelegramProcessBirthIdentity } from "./process-identity.js";
|
|
18
18
|
import { parseTelegramQueueHandoffPayload, TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES, } from "./queue.js";
|
|
@@ -46,7 +46,8 @@ export function createTelegramBusProcessRuntime(input) {
|
|
|
46
46
|
export function createTelegramBusAuthSecret() {
|
|
47
47
|
return randomBytes(32).toString("base64url");
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// v3: journal source serialization left the config transaction; mixed v2/v3 peers would not exclude each other.
|
|
50
|
+
const TELEGRAM_BUS_PROTOCOL_VERSION = 3;
|
|
50
51
|
export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION = "durable-follower-admission-v1";
|
|
51
52
|
export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF = "queue-handoff-v1";
|
|
52
53
|
export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE = "input-custody-reference-v1";
|
|
@@ -466,7 +467,8 @@ export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input)
|
|
|
466
467
|
}
|
|
467
468
|
export function getTelegramBusEnvelopeTrafficClass(envelope) {
|
|
468
469
|
if (envelope.kind === "follower.register" ||
|
|
469
|
-
envelope.kind === "follower.restoreWorkspace"
|
|
470
|
+
envelope.kind === "follower.restoreWorkspace" ||
|
|
471
|
+
envelope.kind === "bus.probe")
|
|
470
472
|
return "bootstrap";
|
|
471
473
|
if (envelope.kind === "bus.ack")
|
|
472
474
|
return "response";
|
|
@@ -718,6 +720,9 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
718
720
|
case "follower.callApi":
|
|
719
721
|
envelope = parseCallApiEnvelope(value, requestId);
|
|
720
722
|
break;
|
|
723
|
+
case "bus.probe":
|
|
724
|
+
envelope = { kind, requestId };
|
|
725
|
+
break;
|
|
721
726
|
case "bus.ack":
|
|
722
727
|
envelope = parseAckEnvelope(value, requestId);
|
|
723
728
|
break;
|
|
@@ -729,6 +734,79 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
729
734
|
envelope.auth = auth;
|
|
730
735
|
return envelope;
|
|
731
736
|
}
|
|
737
|
+
export function probeTelegramBusLeader(input) {
|
|
738
|
+
const timeoutMs = input.timeoutMs ?? TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
739
|
+
return new Promise((resolve) => {
|
|
740
|
+
let settled = false;
|
|
741
|
+
let socket;
|
|
742
|
+
const finish = (result) => {
|
|
743
|
+
if (settled)
|
|
744
|
+
return;
|
|
745
|
+
settled = true;
|
|
746
|
+
clearTimeout(timer);
|
|
747
|
+
socket?.destroy();
|
|
748
|
+
resolve(result);
|
|
749
|
+
};
|
|
750
|
+
const timer = setTimeout(() => finish("silent"), timeoutMs);
|
|
751
|
+
let connected = false;
|
|
752
|
+
try {
|
|
753
|
+
socket = createConnection(resolveTelegramBusSocketPath(input.socketPath));
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
finish("unknown");
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
socket.once("connect", () => {
|
|
760
|
+
connected = true;
|
|
761
|
+
socket.write(encodeTelegramBusEnvelope({
|
|
762
|
+
kind: "bus.probe",
|
|
763
|
+
requestId: `probe:${randomBytes(8).toString("hex")}`,
|
|
764
|
+
...(input.secret ? { auth: input.secret } : {}),
|
|
765
|
+
}));
|
|
766
|
+
});
|
|
767
|
+
socket.once("data", () => finish("responsive"));
|
|
768
|
+
socket.once("end", () => finish("responsive"));
|
|
769
|
+
socket.once("error", (error) => {
|
|
770
|
+
const code = error.code;
|
|
771
|
+
finish(!connected && code === "ECONNREFUSED" ? "unreachable" : "unknown");
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Takeover evidence replacing the retired file heartbeat: a full silent window, or an unreachable endpoint
|
|
777
|
+
* confirmed again after one more window (a just-started leader binds its socket well within it). The owner must
|
|
778
|
+
* stay the same throughout; the lock acquisition then CAS-checks that exact owner.
|
|
779
|
+
*/
|
|
780
|
+
export async function proveTelegramBusLeaderUnresponsive(input) {
|
|
781
|
+
const first = await input.probe();
|
|
782
|
+
if (first === "silent")
|
|
783
|
+
return input.isSameOwner();
|
|
784
|
+
if (first !== "unreachable")
|
|
785
|
+
return false;
|
|
786
|
+
await input.sleep(input.windowMs ?? TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS);
|
|
787
|
+
if (!input.isSameOwner())
|
|
788
|
+
return false;
|
|
789
|
+
const second = await input.probe();
|
|
790
|
+
return (second === "silent" || second === "unreachable") && input.isSameOwner();
|
|
791
|
+
}
|
|
792
|
+
/** Composition-ready takeover proof for one profile's leader lock and endpoint. */
|
|
793
|
+
export function createTelegramBusLeaderUnresponsivenessProof(deps) {
|
|
794
|
+
const probe = deps.probe ?? probeTelegramBusLeader;
|
|
795
|
+
const sleep = deps.sleep ??
|
|
796
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
797
|
+
return (owner) => proveTelegramBusLeaderUnresponsive({
|
|
798
|
+
probe: () => probe({
|
|
799
|
+
socketPath: owner.busSocketPath ?? deps.getLeaderSocketPath(),
|
|
800
|
+
secret: owner.busSecret,
|
|
801
|
+
}),
|
|
802
|
+
isSameOwner: () => {
|
|
803
|
+
const state = deps.getLeaderState();
|
|
804
|
+
return (state.kind === "active-elsewhere" &&
|
|
805
|
+
isSameTelegramLockOwner(state.lock, owner));
|
|
806
|
+
},
|
|
807
|
+
sleep,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
732
810
|
const TELEGRAM_ACTIVE_LOCAL_SERVERS = Symbol.for("@llblab/pi-telegram/active-local-servers");
|
|
733
811
|
function getActiveTelegramBusLocalServers() {
|
|
734
812
|
const root = globalThis;
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -116,12 +116,6 @@ export interface TelegramConfigStore {
|
|
|
116
116
|
setAllowedUserId: (userId: number) => void;
|
|
117
117
|
/** Publish an unpaired profile owner atomically; true only for the resulting exact owner. */
|
|
118
118
|
persistAllowedUserId: (userId: number, assertExecutionCurrent?: () => void, commitIfOwned?: (commit: () => void) => boolean) => Promise<boolean>;
|
|
119
|
-
/** Lock-only serialization for trusted synchronous source operations, not authorization.
|
|
120
|
-
* Does not read/adopt config. Acquire required Workspace admission first; never
|
|
121
|
-
* acquire owners or nest config admission here. Do not pass async callbacks:
|
|
122
|
-
* returned promises are not protected after their synchronous prefix.
|
|
123
|
-
*/
|
|
124
|
-
withSourceSerialization: <T>(operation: () => T) => T;
|
|
125
119
|
/** Trusted synchronous publication only; caller acquires Workspace admission before this config transaction. */
|
|
126
120
|
withPairingAdmission: <T>(profileName: string, tokenSha256: string, publish: (preApprovalExcluded: boolean) => T) => T;
|
|
127
121
|
/** Observe an existing exact owner and refresh an unpaired cache; never create an owner. Callback must be synchronous. */
|
package/dist/lib/config.js
CHANGED
|
@@ -449,7 +449,6 @@ export function createTelegramConfigStore(options = {}) {
|
|
|
449
449
|
nextConfig.allowedUserId = userId;
|
|
450
450
|
setEffectiveConfig(nextConfig);
|
|
451
451
|
},
|
|
452
|
-
withSourceSerialization: (operation) => withTelegramFileTransaction(`${configPath}.transaction`, operation),
|
|
453
452
|
withPairingAdmission: (profileName, tokenSha256, publish) => withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) => publish(profile.allowedUserId === undefined)),
|
|
454
453
|
withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
|
|
455
454
|
if (!Number.isSafeInteger(userId) || userId <= 0)
|
package/dist/lib/extension.js
CHANGED
|
@@ -142,12 +142,13 @@ export default function (pi) {
|
|
|
142
142
|
},
|
|
143
143
|
getProfileName: configStore.getActiveProfileName,
|
|
144
144
|
});
|
|
145
|
+
const withTelegramJournalSourceSerialization = Journal.createTelegramJournalSourceSerialization();
|
|
145
146
|
const telegramJournalBindingRuntime = Journal.createTelegramUpdateJournalBindingRuntime({
|
|
146
147
|
base: {
|
|
147
148
|
getProfileName: configStore.getActiveProfileName,
|
|
148
149
|
getBotToken: configStore.getBotToken,
|
|
149
150
|
getBotId: getTelegramBotId,
|
|
150
|
-
withSourceSerialization:
|
|
151
|
+
withSourceSerialization: withTelegramJournalSourceSerialization,
|
|
151
152
|
onRecovery(event) {
|
|
152
153
|
recordRuntimeEvent("recovery", event.kind === "repaired"
|
|
153
154
|
? "Telegram update journal was repaired automatically."
|
|
@@ -199,6 +200,10 @@ export default function (pi) {
|
|
|
199
200
|
return telegramLeaderJournalPath.createJournalPath();
|
|
200
201
|
},
|
|
201
202
|
});
|
|
203
|
+
const proveTelegramLeaderUnresponsive = Bus.createTelegramBusLeaderUnresponsivenessProof({
|
|
204
|
+
getLeaderState: lockRuntime.getState,
|
|
205
|
+
getLeaderSocketPath: getTelegramBusSocketPath,
|
|
206
|
+
});
|
|
202
207
|
const telegramSessionContextStore = Lifecycle.createTelegramSessionContextStore({
|
|
203
208
|
getIdentity(ctx) {
|
|
204
209
|
return ctx.sessionManager ?? ctx.cwd;
|
|
@@ -1069,7 +1074,7 @@ export default function (pi) {
|
|
|
1069
1074
|
botToken,
|
|
1070
1075
|
botId: getTelegramBotId(),
|
|
1071
1076
|
});
|
|
1072
|
-
return
|
|
1077
|
+
return withTelegramJournalSourceSerialization(function () {
|
|
1073
1078
|
return Journal.isTelegramThreadCleanupJournalNamespaceClear({
|
|
1074
1079
|
directory: Paths.resolveTelegramTempDir(),
|
|
1075
1080
|
profile,
|
|
@@ -1554,6 +1559,7 @@ export default function (pi) {
|
|
|
1554
1559
|
},
|
|
1555
1560
|
recovery: {
|
|
1556
1561
|
getLeaderState: lockRuntime.getState,
|
|
1562
|
+
proveLeaderUnresponsive: proveTelegramLeaderUnresponsive,
|
|
1557
1563
|
setLifecyclePhase: telegramBusFollowerControlState.setLifecyclePhase,
|
|
1558
1564
|
updateStatus,
|
|
1559
1565
|
promoteToLeader: promoteTelegramBusFollowerToLeader,
|
|
@@ -1860,6 +1866,7 @@ export default function (pi) {
|
|
|
1860
1866
|
const threadAwarePollingPorts = telegramThreadCapabilityRuntime.pollingPorts;
|
|
1861
1867
|
const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
|
|
1862
1868
|
lock: lockRuntime,
|
|
1869
|
+
proveOwnerUnresponsive: proveTelegramLeaderUnresponsive,
|
|
1863
1870
|
resetDamagedState() {
|
|
1864
1871
|
return Locks.resetDamagedTelegramRuntimeState(Paths.resolveTelegramStatePath(), function (profile, sections) {
|
|
1865
1872
|
Threads.parseTelegramWorkspaceStateSection(sections.workspace, profile);
|
package/dist/lib/journal.d.ts
CHANGED
|
@@ -860,6 +860,14 @@ export declare function createTelegramUpdateJournalBindingRuntime(deps: {
|
|
|
860
860
|
getActiveFollowerSessionId?: () => string | undefined;
|
|
861
861
|
isFollowerRegistered: () => boolean;
|
|
862
862
|
}): TelegramUpdateJournalBindingRuntime;
|
|
863
|
+
/**
|
|
864
|
+
* Journal-owned cross-family source serializer, lock-only and never authorization. Its guard lives
|
|
865
|
+
* in the runtime service directory, so journal work never touches `telegram.json` or its
|
|
866
|
+
* transaction. Acquire Workspace admission and any sender (config) admission first; never acquire
|
|
867
|
+
* owners inside it. Callbacks must be synchronous: a returned promise is unprotected after its
|
|
868
|
+
* synchronous prefix.
|
|
869
|
+
*/
|
|
870
|
+
export declare function createTelegramJournalSourceSerialization(getTransactionPath?: () => string): <T>(operation: () => T) => T;
|
|
863
871
|
export declare function createTelegramUpdateJournalStore(options: TelegramUpdateJournalStoreOptions): TelegramUpdateJournalStore;
|
|
864
872
|
/** Opt-in v3 only; does not migrate old files or expose legacy unowned mutation ports. */
|
|
865
873
|
export declare function createTelegramInputJournalStore(options: TelegramInputJournalStoreOptions): TelegramInputJournalStore;
|
package/dist/lib/journal.js
CHANGED
|
@@ -10,7 +10,7 @@ import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, opendirSync, ope
|
|
|
10
10
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
11
11
|
import { isDeepStrictEqual } from "node:util";
|
|
12
12
|
import { TELEGRAM_STRICT_READ_FLAGS, renameTelegramPathWithRetry, withTelegramFileTransaction, } from "./locks.js";
|
|
13
|
-
import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./paths.js";
|
|
13
|
+
import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix, resolveTelegramRuntimeDir, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./paths.js";
|
|
14
14
|
import { getTelegramProcessLiveness, } from "./process-identity.js";
|
|
15
15
|
import { hasOnlyWireKeys as hasOnlyKeys, isNonEmptyWireString as isNonEmptyString, isWireRecord as isRecord, isNonNegativeWireInteger as isSafeNonNegativeInteger, } from "./wire.js";
|
|
16
16
|
import { getTelegramWorkspaceAdmissionScopeKey, runWithTelegramWorkspaceAdmissions, } from "./workspace-admission.js";
|
|
@@ -2735,6 +2735,16 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2735
2735
|
};
|
|
2736
2736
|
return runtime;
|
|
2737
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Journal-owned cross-family source serializer, lock-only and never authorization. Its guard lives
|
|
2740
|
+
* in the runtime service directory, so journal work never touches `telegram.json` or its
|
|
2741
|
+
* transaction. Acquire Workspace admission and any sender (config) admission first; never acquire
|
|
2742
|
+
* owners inside it. Callbacks must be synchronous: a returned promise is unprotected after its
|
|
2743
|
+
* synchronous prefix.
|
|
2744
|
+
*/
|
|
2745
|
+
export function createTelegramJournalSourceSerialization(getTransactionPath = () => join(resolveTelegramRuntimeDir(), "journals.transaction")) {
|
|
2746
|
+
return (operation) => withTelegramFileTransaction(getTransactionPath(), operation);
|
|
2747
|
+
}
|
|
2738
2748
|
export function createTelegramUpdateJournalStore(options) {
|
|
2739
2749
|
return createJournalStoreCore(options).journal;
|
|
2740
2750
|
}
|
|
@@ -4219,18 +4229,20 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4219
4229
|
serializedBytes: published.serializedBytes,
|
|
4220
4230
|
};
|
|
4221
4231
|
});
|
|
4232
|
+
// Sender admission is the outer config authority; journal serialization nests inside it.
|
|
4233
|
+
const serializedAppend = (preApprovalExcluded) => withSourceSerialization
|
|
4234
|
+
? withSourceSerialization(() => appendWithEvidence(preApprovalExcluded))
|
|
4235
|
+
: appendWithEvidence(preApprovalExcluded);
|
|
4222
4236
|
const append = () => {
|
|
4223
4237
|
if (withPairedAdmission) {
|
|
4224
|
-
const result = withPairedAdmission(normalizedUpdates, () =>
|
|
4238
|
+
const result = withPairedAdmission(normalizedUpdates, () => serializedAppend());
|
|
4225
4239
|
if (!result.admitted)
|
|
4226
4240
|
throw createJournalError("sender-denied", path, "refused paired-only sender admission");
|
|
4227
4241
|
return result.value;
|
|
4228
4242
|
}
|
|
4229
4243
|
if (withPairingAdmission)
|
|
4230
|
-
return withPairingAdmission(
|
|
4231
|
-
return
|
|
4232
|
-
? withSourceSerialization(() => appendWithEvidence())
|
|
4233
|
-
: appendWithEvidence();
|
|
4244
|
+
return withPairingAdmission(serializedAppend);
|
|
4245
|
+
return serializedAppend();
|
|
4234
4246
|
};
|
|
4235
4247
|
const admittedAppend = () => {
|
|
4236
4248
|
if (!workspaceAdmission)
|
package/dist/lib/locks.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ interface TelegramLockAcquireOptions {
|
|
|
60
60
|
force?: boolean;
|
|
61
61
|
expectedOwner?: TelegramLockEntry;
|
|
62
62
|
election?: boolean;
|
|
63
|
+
/** Bus-proven unresponsive owner: treated as stale only while it is still exactly the current owner. */
|
|
64
|
+
unresponsiveOwner?: TelegramLockEntry;
|
|
63
65
|
}
|
|
64
66
|
type TelegramLockAcquireResult = {
|
|
65
67
|
ok: true;
|
|
@@ -213,6 +215,8 @@ export interface TelegramOwnedStatePublicationOptions extends TelegramRuntimeSta
|
|
|
213
215
|
export declare function mutateTelegramRuntimeStateSection<T>(path: string, profile: string, section: TelegramRuntimeStateSection, mutate: TelegramRuntimeStateSectionReducer<T>, options: TelegramRuntimeStatePublicationOptions): T;
|
|
214
216
|
export declare function writeLocks(path: string, locks: Record<string, unknown>): void;
|
|
215
217
|
export declare function parseTelegramLockEntry(value: unknown): TelegramLockEntry | undefined;
|
|
218
|
+
/** Exact owner identity (pid, cwd, instance, leader epoch, runtime generation); absent entries never match. */
|
|
219
|
+
export declare function isSameTelegramLockOwner(current: TelegramLockEntry | undefined, expected: TelegramLockEntry | undefined): boolean;
|
|
216
220
|
export declare function createTelegramLockRuntime<TContext extends TelegramLockContext>(options?: TelegramLockRuntimeOptions): TelegramLockRuntime<TContext>;
|
|
217
221
|
export declare function createTelegramLockOwnershipGuard<TContext extends TelegramLockContext>(lock: TelegramLockRuntime<TContext>): TelegramLockOwnershipGuard<TContext>;
|
|
218
222
|
export declare function createTelegramDirectDeliveryOwnershipChecker<TContext extends TelegramLockContext>(deps: {
|
|
@@ -225,6 +229,7 @@ interface TelegramLockedPollingStartOptions {
|
|
|
225
229
|
requestedThreadName?: string;
|
|
226
230
|
election?: {
|
|
227
231
|
expectedOwner?: TelegramLockEntry;
|
|
232
|
+
unresponsive?: boolean;
|
|
228
233
|
};
|
|
229
234
|
onAcquired?: () => Promise<void> | void;
|
|
230
235
|
}
|
|
@@ -270,6 +275,8 @@ interface TelegramLockedPollingRuntimeDeps<TContext extends TelegramLockContext>
|
|
|
270
275
|
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
|
271
276
|
restoreFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
|
272
277
|
stopFollowerRegistration?: () => void;
|
|
278
|
+
/** Threaded Mode takeover evidence after failed follower registration; replaces the retired file heartbeat. */
|
|
279
|
+
proveOwnerUnresponsive?: (owner: TelegramLockEntry) => Promise<boolean>;
|
|
273
280
|
onTransportAvailabilityChanged?: () => void;
|
|
274
281
|
transportMonitor?: {
|
|
275
282
|
start: (ctx: TContext) => void;
|
package/dist/lib/locks.js
CHANGED
|
@@ -959,6 +959,10 @@ function hasSameLockOwner(current, expected) {
|
|
|
959
959
|
current.leaderEpoch === expected.leaderEpoch &&
|
|
960
960
|
current.runtimeGeneration === expected.runtimeGeneration);
|
|
961
961
|
}
|
|
962
|
+
/** Exact owner identity (pid, cwd, instance, leader epoch, runtime generation); absent entries never match. */
|
|
963
|
+
export function isSameTelegramLockOwner(current, expected) {
|
|
964
|
+
return hasSameLockOwner(current, expected);
|
|
965
|
+
}
|
|
962
966
|
function canSupersedeSameProcessOwner(current, pid, ctx, instanceId, runtimeGeneration) {
|
|
963
967
|
if (current.pid !== pid ||
|
|
964
968
|
(current.cwd !== undefined && current.cwd !== ctx.cwd) ||
|
|
@@ -973,9 +977,7 @@ function createLockEntry(pid, ctx, options) {
|
|
|
973
977
|
if (options.journalPath)
|
|
974
978
|
lock.journalPath = options.journalPath;
|
|
975
979
|
if (options.instanceId) {
|
|
976
|
-
const nowMs = options.getNowMs?.();
|
|
977
980
|
lock.instanceId = options.instanceId;
|
|
978
|
-
lock.heartbeatMs = nowMs;
|
|
979
981
|
lock.leaderEpoch = options.mintLeaderEpoch?.() ?? randomUUID();
|
|
980
982
|
lock.runtimeGeneration = options.runtimeGeneration;
|
|
981
983
|
}
|
|
@@ -1144,7 +1146,11 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1144
1146
|
changed: false,
|
|
1145
1147
|
};
|
|
1146
1148
|
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1147
|
-
const
|
|
1149
|
+
const observedState = getLockState(current, pid, isAlive, stateOptions());
|
|
1150
|
+
const state = observedState.kind === "active-elsewhere" &&
|
|
1151
|
+
hasSameLockOwner(current, acquireOptions.unresponsiveOwner)
|
|
1152
|
+
? { kind: "stale", lock: observedState.lock }
|
|
1153
|
+
: observedState;
|
|
1148
1154
|
const expectedOwned = adoptCompatibleOwnedLock(effectiveKey, current, ctx);
|
|
1149
1155
|
if (state.kind === "active-here" &&
|
|
1150
1156
|
hasSameLockOwner(current, expectedOwned) &&
|
|
@@ -1191,7 +1197,6 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1191
1197
|
instanceId: options.instanceId,
|
|
1192
1198
|
busSocketPath: options.busSocketPath,
|
|
1193
1199
|
busSecret: options.busSecret,
|
|
1194
|
-
getNowMs,
|
|
1195
1200
|
mintLeaderEpoch: options.mintLeaderEpoch,
|
|
1196
1201
|
runtimeGeneration,
|
|
1197
1202
|
});
|
|
@@ -1299,9 +1304,36 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1299
1304
|
};
|
|
1300
1305
|
}, { ...publication, isCurrent });
|
|
1301
1306
|
},
|
|
1302
|
-
refresh: (ctx) =>
|
|
1303
|
-
|
|
1304
|
-
|
|
1307
|
+
refresh: (ctx) => {
|
|
1308
|
+
if (deliveryRevoked)
|
|
1309
|
+
return false;
|
|
1310
|
+
// Owner fields only, never a timestamp: liveness is proven over the bus, so an exact owner needs no write.
|
|
1311
|
+
const refreshedEntry = (lock) => {
|
|
1312
|
+
const busSecret = options.busSecret ?? lock.busSecret;
|
|
1313
|
+
return {
|
|
1314
|
+
pid: lock.pid,
|
|
1315
|
+
...(lock.cwd ? { cwd: lock.cwd } : {}),
|
|
1316
|
+
instanceId: options.instanceId,
|
|
1317
|
+
leaderEpoch: lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
|
|
1318
|
+
runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
|
|
1319
|
+
...(options.busSocketPath
|
|
1320
|
+
? { busSocketPath: options.busSocketPath }
|
|
1321
|
+
: {}),
|
|
1322
|
+
...(busSecret !== undefined ? { busSecret } : {}),
|
|
1323
|
+
...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
// Parsed entries carry explicit `undefined` fields; compare only the published ones.
|
|
1327
|
+
const isCurrentEntry = (lock) => lock.leaderEpoch !== undefined &&
|
|
1328
|
+
isDeepStrictEqual(Object.fromEntries(Object.entries(lock).filter(([, value]) => value !== undefined)), refreshedEntry(lock));
|
|
1329
|
+
const effectiveKey = resolveEffectiveKey();
|
|
1330
|
+
const observed = readLock();
|
|
1331
|
+
if (observed &&
|
|
1332
|
+
hasSameLockOwner(observed, adoptCompatibleOwnedLock(effectiveKey, observed, ctx)) &&
|
|
1333
|
+
(!options.instanceId || isCurrentEntry(observed))) {
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
return transactOwners((locks) => {
|
|
1305
1337
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1306
1338
|
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
1307
1339
|
if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
|
|
@@ -1313,24 +1345,15 @@ export function createTelegramLockRuntime(options = {}) {
|
|
|
1313
1345
|
}
|
|
1314
1346
|
if (!options.instanceId)
|
|
1315
1347
|
return { result: true, changed: false };
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
};
|
|
1348
|
+
if (isCurrentEntry(lock))
|
|
1349
|
+
return { result: true, changed: false };
|
|
1350
|
+
const refreshedLock = refreshedEntry(lock);
|
|
1329
1351
|
locks[effectiveKey] = refreshedLock;
|
|
1330
1352
|
ownedLockKey = effectiveKey;
|
|
1331
1353
|
ownedLock = refreshedLock;
|
|
1332
1354
|
return { result: true, changed: true };
|
|
1333
|
-
})
|
|
1355
|
+
});
|
|
1356
|
+
},
|
|
1334
1357
|
};
|
|
1335
1358
|
}
|
|
1336
1359
|
export function createTelegramLockOwnershipGuard(lock) {
|
|
@@ -1551,6 +1574,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1551
1574
|
expectedOwner: options.election?.expectedOwner ??
|
|
1552
1575
|
(options.force ? takeoverCandidate : undefined),
|
|
1553
1576
|
election: options.election !== undefined,
|
|
1577
|
+
unresponsiveOwner: options.election?.unresponsive
|
|
1578
|
+
? options.election.expectedOwner
|
|
1579
|
+
: undefined,
|
|
1554
1580
|
});
|
|
1555
1581
|
if (!acquired.ok && !options.election) {
|
|
1556
1582
|
const currentState = deps.lock.getState();
|
|
@@ -1593,22 +1619,33 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1593
1619
|
});
|
|
1594
1620
|
}
|
|
1595
1621
|
if (failureMessage) {
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
}
|
|
1622
|
+
const unresponsiveOwner = acquired.lock;
|
|
1623
|
+
if (deps.proveOwnerUnresponsive &&
|
|
1624
|
+
(await deps.proveOwnerUnresponsive(unresponsiveOwner))) {
|
|
1625
|
+
if (!isCurrent())
|
|
1626
|
+
return cancelled;
|
|
1627
|
+
acquired = deps.lock.acquire(ctx, { unresponsiveOwner });
|
|
1628
|
+
}
|
|
1629
|
+
if (!acquired.ok) {
|
|
1630
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1631
|
+
return {
|
|
1632
|
+
ok: false,
|
|
1633
|
+
canTakeover: false,
|
|
1634
|
+
owner,
|
|
1635
|
+
message: `Telegram bridge is active in another Pi instance (${owner}); follower registration failed: ${formatTelegramFollowerRegistrationFailure(failureMessage)}.`,
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1603
1638
|
}
|
|
1604
1639
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1640
|
+
if (!acquired.ok) {
|
|
1641
|
+
const owner = formatTelegramLockEntry(acquired.lock);
|
|
1642
|
+
return {
|
|
1643
|
+
ok: false,
|
|
1644
|
+
canTakeover: true,
|
|
1645
|
+
owner,
|
|
1646
|
+
message: `Telegram bridge is active in another Pi instance (${owner}).`,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1612
1649
|
}
|
|
1613
1650
|
takeoverCandidate = undefined;
|
|
1614
1651
|
if (!(await runOwnedPollingStart(ctx, options, isCurrent))) {
|
package/dist/lib/status.js
CHANGED
|
@@ -433,6 +433,15 @@ export function createTelegramRuntimeProjectionStore(options) {
|
|
|
433
433
|
if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 }))
|
|
434
434
|
throw new Error("Telegram runtime projection is malformed.");
|
|
435
435
|
delete semantic.diagnostics.recentRuntimeEvents;
|
|
436
|
+
// Poll-cycle timestamps change on every response; live status renders them from memory,
|
|
437
|
+
// so persisting them would rewrite state.json each poll for no reader.
|
|
438
|
+
const polling = semantic.runtime.polling;
|
|
439
|
+
if (polling && typeof polling === "object" && !Array.isArray(polling)) {
|
|
440
|
+
const volatile = polling;
|
|
441
|
+
delete volatile.phaseStartedAtMs;
|
|
442
|
+
delete volatile.lastSuccessfulResponseAtMs;
|
|
443
|
+
delete volatile.lastSuccessfulResponseUpdateCount;
|
|
444
|
+
}
|
|
436
445
|
const isCurrent = () => getPath() === path && getProfile() === profile && authority() === true;
|
|
437
446
|
const isUnchanged = (current) => {
|
|
438
447
|
if (!isRuntimeProjection(current))
|