@llblab/pi-kit 0.27.6 → 0.28.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 +2 -5
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +2 -2
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram singleton lock helpers
|
|
3
3
|
* Zones: telegram ownership, filesystem, transport authority
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
chmodSync,
|
|
9
|
+
closeSync,
|
|
9
10
|
existsSync,
|
|
11
|
+
fstatSync,
|
|
10
12
|
lstatSync,
|
|
11
13
|
mkdirSync,
|
|
12
14
|
mkdtempSync,
|
|
15
|
+
openSync,
|
|
13
16
|
readFileSync,
|
|
14
17
|
readdirSync,
|
|
15
18
|
renameSync,
|
|
@@ -18,8 +21,15 @@ import {
|
|
|
18
21
|
writeFileSync,
|
|
19
22
|
} from "node:fs";
|
|
20
23
|
import { randomUUID } from "node:crypto";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
24
|
+
import { isDeepStrictEqual } from "node:util";
|
|
25
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
26
|
+
import { isWireRecord as runtimeStateRecord } from "./wire.ts";
|
|
27
|
+
import {
|
|
28
|
+
isTelegramSessionPollingJournalPath,
|
|
29
|
+
resolveTelegramOwnersPath,
|
|
30
|
+
resolveTelegramSessionPollingJournalPath,
|
|
31
|
+
resolveTelegramUpdateJournalPathForProfile,
|
|
32
|
+
} from "./paths.ts";
|
|
23
33
|
|
|
24
34
|
export const TELEGRAM_LOCK_KEY = "default";
|
|
25
35
|
export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
|
|
@@ -57,7 +67,7 @@ export function resolveTelegramLockKey(activeProfile?: string): string {
|
|
|
57
67
|
return activeProfile || TELEGRAM_LOCK_KEY;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
interface TelegramActiveProfileGetter {
|
|
61
71
|
getActiveProfileName: () => string | undefined;
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -69,6 +79,31 @@ export function createTelegramLockKeyResolver(
|
|
|
69
79
|
};
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
/** Structural session view; Locks never imports the lifecycle owner. */
|
|
83
|
+
interface TelegramOwnedStateSessionPort<TContext extends TelegramLockContext> {
|
|
84
|
+
get: () => TContext | undefined;
|
|
85
|
+
getGeneration: () => number;
|
|
86
|
+
isCurrent: (ctx: TContext, generation?: number) => boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Captures exact context, session generation and owned leader epoch before awaits.
|
|
91
|
+
* Session replacement, release or re-election revokes the grant; a successor never renews it.
|
|
92
|
+
*/
|
|
93
|
+
export function createTelegramOwnedStateAuthorityCapture<TContext extends TelegramLockContext>(
|
|
94
|
+
lock: Pick<TelegramLockRuntime<TContext>, "owns" | "getOwnedLeaderEpoch">,
|
|
95
|
+
session: TelegramOwnedStateSessionPort<TContext>,
|
|
96
|
+
): () => (() => boolean) | undefined {
|
|
97
|
+
return () => {
|
|
98
|
+
const ctx = session.get(), generation = session.getGeneration();
|
|
99
|
+
if (ctx === undefined || !session.isCurrent(ctx, generation)) return undefined;
|
|
100
|
+
const epoch = lock.getOwnedLeaderEpoch();
|
|
101
|
+
if (epoch === undefined) return undefined;
|
|
102
|
+
const current = (): boolean => session.isCurrent(ctx, generation) && lock.owns(ctx) && lock.getOwnedLeaderEpoch() === epoch;
|
|
103
|
+
return current() ? current : undefined;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
export interface TelegramLockEntry {
|
|
73
108
|
pid: number;
|
|
74
109
|
cwd?: string;
|
|
@@ -78,6 +113,8 @@ export interface TelegramLockEntry {
|
|
|
78
113
|
runtimeGeneration?: number;
|
|
79
114
|
busSocketPath?: string;
|
|
80
115
|
busSecret?: string;
|
|
116
|
+
/** Polling journal custody; inherited by every successor and kept after release. */
|
|
117
|
+
journalPath?: string;
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
export interface TelegramLockContext {
|
|
@@ -90,16 +127,18 @@ export type TelegramLockState =
|
|
|
90
127
|
| { kind: "active-elsewhere"; lock: TelegramLockEntry }
|
|
91
128
|
| { kind: "stale"; lock: TelegramLockEntry };
|
|
92
129
|
|
|
93
|
-
|
|
130
|
+
interface TelegramLockAcquireOptions {
|
|
94
131
|
force?: boolean;
|
|
95
132
|
expectedOwner?: TelegramLockEntry;
|
|
96
133
|
election?: boolean;
|
|
97
134
|
}
|
|
98
135
|
|
|
99
|
-
|
|
136
|
+
type TelegramLockAcquireResult =
|
|
100
137
|
| { ok: true; lock: TelegramLockEntry; replacedStale: boolean }
|
|
101
138
|
| { ok: false; lock: TelegramLockEntry };
|
|
102
139
|
|
|
140
|
+
export type TelegramOwnedStatePublicationResult<T> = { committed: false } | { committed: true; result: T };
|
|
141
|
+
|
|
103
142
|
export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
|
|
104
143
|
acquire: (
|
|
105
144
|
ctx: TContext,
|
|
@@ -111,24 +150,37 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
|
|
|
111
150
|
getOwnedLeaderEpoch: () => number | string | undefined;
|
|
112
151
|
owns: (ctx?: TelegramLockContext) => boolean;
|
|
113
152
|
commitIfOwned: (commit: () => void) => boolean;
|
|
153
|
+
/** Consolidated-store publication; domain reducers retain their own exact payload/CAS rules. */
|
|
154
|
+
publishStateSectionIfOwned?: <T>(
|
|
155
|
+
section: "workspace" | "runtime",
|
|
156
|
+
mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
|
|
157
|
+
options: TelegramOwnedStatePublicationOptions,
|
|
158
|
+
) => TelegramOwnedStatePublicationResult<T>;
|
|
114
159
|
refresh: (ctx?: TelegramLockContext) => boolean;
|
|
160
|
+
/** The polling journal named by owners.json for this key, owned or not. */
|
|
161
|
+
getJournalPath: () => string | undefined;
|
|
115
162
|
}
|
|
116
163
|
|
|
117
|
-
|
|
164
|
+
interface TelegramLockOwnershipGuard<
|
|
118
165
|
TContext extends TelegramLockContext,
|
|
119
166
|
> {
|
|
120
167
|
ownsContext: (ctx: TContext) => boolean;
|
|
121
168
|
}
|
|
122
169
|
|
|
123
|
-
|
|
170
|
+
interface TelegramLockContextStore<
|
|
124
171
|
TContext extends TelegramLockContext,
|
|
125
172
|
> {
|
|
126
173
|
get: () => TContext | undefined;
|
|
127
174
|
}
|
|
128
175
|
|
|
129
|
-
|
|
176
|
+
interface TelegramLockRuntimeOptions {
|
|
130
177
|
key?: string | (() => string | undefined);
|
|
131
178
|
locksPath?: string;
|
|
179
|
+
/** Consolidated version-2 state envelope; exclusive with `locksPath`. */
|
|
180
|
+
statePath?: string;
|
|
181
|
+
statePublication?: Pick<TelegramRuntimeStatePublicationOptions, "onPublicationBoundary" | "publishRename">;
|
|
182
|
+
/** Read-only ownership file of releases that used another directory; a live fresh owner there blocks acquisition. */
|
|
183
|
+
legacyLocksPath?: string;
|
|
132
184
|
pid?: number;
|
|
133
185
|
isProcessAlive?: (pid: number) => boolean;
|
|
134
186
|
instanceId?: string;
|
|
@@ -138,6 +190,42 @@ export interface TelegramLockRuntimeOptions {
|
|
|
138
190
|
mintLeaderEpoch?: () => number | string;
|
|
139
191
|
runtimeGeneration?: number;
|
|
140
192
|
staleHeartbeatMs?: number;
|
|
193
|
+
/** First leader without an inherited pointer names the polling journal it creates. */
|
|
194
|
+
createJournalPath?: (ctx: TelegramLockContext) => string | undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Leader polling journal: the path owners.json names, else the session that would host it on acquisition.
|
|
199
|
+
* Without a session identity, the flat root `inbox` remains the compatibility fallback.
|
|
200
|
+
*/
|
|
201
|
+
export function createTelegramLeaderJournalPathResolver(deps: {
|
|
202
|
+
getNamedJournalPath: () => string | undefined;
|
|
203
|
+
getSessionId: () => string | undefined;
|
|
204
|
+
getProfileName: () => string | undefined;
|
|
205
|
+
}) {
|
|
206
|
+
// An existing flat root journal keeps its custody; new installations never create one.
|
|
207
|
+
const createOwn = (profileName = deps.getProfileName()): string | undefined => {
|
|
208
|
+
const root = resolveTelegramUpdateJournalPathForProfile(profileName);
|
|
209
|
+
if (existsSync(root)) return root;
|
|
210
|
+
const sessionId = deps.getSessionId();
|
|
211
|
+
return sessionId === undefined ? undefined : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
createJournalPath: () => createOwn(),
|
|
215
|
+
resolve(profileName?: string): string {
|
|
216
|
+
const named = deps.getNamedJournalPath();
|
|
217
|
+
if (named && (isTelegramSessionPollingJournalPath(named) ||
|
|
218
|
+
named === resolveTelegramUpdateJournalPathForProfile(profileName))) return named;
|
|
219
|
+
return createOwn(profileName) ?? resolveTelegramUpdateJournalPathForProfile(profileName);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A released key keeps only `{ journalPath }`: no owner, but the successor's polling custody. */
|
|
225
|
+
export function readTelegramLockJournalPath(value: unknown): string | undefined {
|
|
226
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
227
|
+
const path = (value as Record<string, unknown>).journalPath;
|
|
228
|
+
return typeof path === "string" && path ? path : undefined;
|
|
141
229
|
}
|
|
142
230
|
|
|
143
231
|
export function readLocks(path = getOwnersPath()): Record<string, unknown> {
|
|
@@ -177,7 +265,7 @@ function sleepSync(ms: number): void {
|
|
|
177
265
|
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
178
266
|
}
|
|
179
267
|
|
|
180
|
-
|
|
268
|
+
interface TelegramRenameRetryOptions {
|
|
181
269
|
rename?: typeof renameSync;
|
|
182
270
|
attempts?: number;
|
|
183
271
|
retryDelayMs?: number;
|
|
@@ -650,6 +738,188 @@ export function withTelegramFileTransaction<T>(
|
|
|
650
738
|
}
|
|
651
739
|
}
|
|
652
740
|
|
|
741
|
+
export type TelegramRuntimeStateSection = "transport" | "workspace" | "admission" | "runtime";
|
|
742
|
+
type TelegramRuntimeStateProfile = Partial<Record<TelegramRuntimeStateSection, unknown>>;
|
|
743
|
+
interface TelegramRuntimeStateFile {
|
|
744
|
+
version: 2;
|
|
745
|
+
profiles: Record<string, TelegramRuntimeStateProfile>;
|
|
746
|
+
}
|
|
747
|
+
export class TelegramRuntimeStateError extends Error {
|
|
748
|
+
readonly code: "invalid" | "authority-changed" | "publication-unknown";
|
|
749
|
+
constructor(code: TelegramRuntimeStateError["code"], message: string, options?: ErrorOptions) {
|
|
750
|
+
super(message, options);
|
|
751
|
+
this.name = "TelegramRuntimeStateError";
|
|
752
|
+
this.code = code;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const TELEGRAM_RUNTIME_STATE_SECTIONS: readonly TelegramRuntimeStateSection[] = ["transport", "workspace", "admission", "runtime"];
|
|
756
|
+
const TELEGRAM_ACTIVE_STATE_TRANSACTIONS = Symbol.for("@llblab/pi-telegram/active-state-transactions");
|
|
757
|
+
type TelegramStateGlobal = typeof globalThis & { [TELEGRAM_ACTIVE_STATE_TRANSACTIONS]?: Set<string> };
|
|
758
|
+
function requireRuntimeStatePath(path: string): void {
|
|
759
|
+
if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path)
|
|
760
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
|
|
761
|
+
}
|
|
762
|
+
function requireRuntimeStateProfile(profile: string): void {
|
|
763
|
+
if (typeof profile !== "string" || !profile.length || profile.length > 512 || /[\x00-\x1f]/u.test(profile))
|
|
764
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state profile is invalid.");
|
|
765
|
+
}
|
|
766
|
+
/** Private single-link regular file read with identity continuity; initial absence only is positive. */
|
|
767
|
+
function readTelegramRuntimeSource(path: string): string | undefined {
|
|
768
|
+
requireRuntimeStatePath(path);
|
|
769
|
+
let source: string;
|
|
770
|
+
let observed = false;
|
|
771
|
+
try {
|
|
772
|
+
const stat = lstatSync(path);
|
|
773
|
+
observed = true;
|
|
774
|
+
if (!stat.isFile() || stat.nlink !== 1 ||
|
|
775
|
+
(process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
776
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state must be a private regular file.");
|
|
777
|
+
const fd = openSync(path, "r");
|
|
778
|
+
try {
|
|
779
|
+
const opened = fstatSync(fd);
|
|
780
|
+
if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.nlink !== 1 || !opened.isFile() ||
|
|
781
|
+
(process.platform !== "win32" && (opened.mode & 0o077) !== 0))
|
|
782
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state identity changed before reading.");
|
|
783
|
+
source = readFileSync(fd).toString("utf8");
|
|
784
|
+
} finally { closeSync(fd); }
|
|
785
|
+
const after = lstatSync(path);
|
|
786
|
+
if (after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs)
|
|
787
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state changed during observation.");
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
if (!observed && (error as { code?: unknown }).code === "ENOENT") return undefined;
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
return source;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** Strict, observational envelope read; section owners validate their own payloads. Legacy state is never adopted here. */
|
|
797
|
+
export function readTelegramRuntimeState(path: string): TelegramRuntimeStateFile {
|
|
798
|
+
const observed = readTelegramRuntimeSource(path);
|
|
799
|
+
if (observed === undefined) return { version: 2, profiles: {} };
|
|
800
|
+
let value: unknown;
|
|
801
|
+
try { value = JSON.parse(observed); }
|
|
802
|
+
catch (error) { throw new TelegramRuntimeStateError("invalid", "Telegram runtime state is unreadable.", { cause: error }); }
|
|
803
|
+
if (!runtimeStateRecord(value) || value.version !== 2 || !runtimeStateRecord(value.profiles) ||
|
|
804
|
+
Object.keys(value).some(key => key !== "version" && key !== "profiles"))
|
|
805
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state envelope is unsupported or malformed.");
|
|
806
|
+
for (const [profile, sections] of Object.entries(value.profiles)) {
|
|
807
|
+
requireRuntimeStateProfile(profile);
|
|
808
|
+
if (!runtimeStateRecord(sections) || Object.keys(sections).some(key => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key as TelegramRuntimeStateSection)))
|
|
809
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state sections are malformed.");
|
|
810
|
+
}
|
|
811
|
+
return value as unknown as TelegramRuntimeStateFile;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Operator-approved optimistic recovery before leader acquisition: when the shared envelope, any transport section or a
|
|
815
|
+
* caller-validated section is damaged, publish a fresh empty envelope instead of refusing. All profiles lose runtime
|
|
816
|
+
* continuity. Filesystem access errors are not damage and still throw. Returns whether a reset was published.
|
|
817
|
+
*/
|
|
818
|
+
export function resetDamagedTelegramRuntimeState(path: string,
|
|
819
|
+
validateProfile?: (profile: string, sections: Readonly<TelegramRuntimeStateProfile>) => void): boolean {
|
|
820
|
+
requireRuntimeStatePath(path);
|
|
821
|
+
const runtimeDir = join(dirname(path), "runtime");
|
|
822
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
823
|
+
return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
|
|
824
|
+
let damaged = false;
|
|
825
|
+
try {
|
|
826
|
+
for (const [profile, sections] of Object.entries(readTelegramRuntimeState(path).profiles)) {
|
|
827
|
+
assertTelegramStateTransport(sections.transport);
|
|
828
|
+
try { validateProfile?.(profile, sections); } catch { damaged = true; }
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (!(error instanceof TelegramRuntimeStateError)) throw error;
|
|
832
|
+
damaged = true;
|
|
833
|
+
}
|
|
834
|
+
if (!damaged) return false;
|
|
835
|
+
const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
836
|
+
try {
|
|
837
|
+
writeFileSync(tempPath, `${JSON.stringify({ version: 2, profiles: {} }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
838
|
+
if (!renameTelegramPathWithRetry(tempPath, path)) throw new Error("Telegram runtime state reset publication disappeared.");
|
|
839
|
+
} finally {
|
|
840
|
+
try { unlinkSync(tempPath); }
|
|
841
|
+
catch (error) { if ((error as { code?: unknown }).code !== "ENOENT") throw error; }
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
export interface TelegramRuntimeStateMutation<T> {
|
|
847
|
+
value: unknown;
|
|
848
|
+
result: T;
|
|
849
|
+
}
|
|
850
|
+
interface TelegramRuntimeStatePublicationOptions {
|
|
851
|
+
/** Exact domain authority, recaptured by the caller before invoking this synchronous transaction. */
|
|
852
|
+
isCurrent: () => boolean;
|
|
853
|
+
onPublicationBoundary?: (boundary: "before-write" | "after-write-before-rename" | "after-rename") => void;
|
|
854
|
+
publishRename?: typeof renameSync;
|
|
855
|
+
}
|
|
856
|
+
export interface TelegramOwnedStatePublicationOptions extends TelegramRuntimeStatePublicationOptions {
|
|
857
|
+
/** Optional caller-bound physical/logical identity; a mismatch cannot redirect an owner grant. */
|
|
858
|
+
expectedScope?: { path: string; profile: string };
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* One physical read/check/write transaction for one named section. The caller supplies domain policy; copies expose
|
|
862
|
+
* current sibling facts without granting writes to them. No await, nested transaction, repair or legacy import.
|
|
863
|
+
*/
|
|
864
|
+
export function mutateTelegramRuntimeStateSection<T>(
|
|
865
|
+
path: string, profile: string, section: TelegramRuntimeStateSection,
|
|
866
|
+
mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
|
|
867
|
+
options: TelegramRuntimeStatePublicationOptions,
|
|
868
|
+
): T {
|
|
869
|
+
requireRuntimeStatePath(path);
|
|
870
|
+
requireRuntimeStateProfile(profile);
|
|
871
|
+
if (!TELEGRAM_RUNTIME_STATE_SECTIONS.includes(section))
|
|
872
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state section is invalid.");
|
|
873
|
+
const active = ((globalThis as TelegramStateGlobal)[TELEGRAM_ACTIVE_STATE_TRANSACTIONS] ??= new Set<string>());
|
|
874
|
+
if (active.has(path)) throw new TelegramRuntimeStateError("invalid", "Nested Telegram runtime state transaction is not allowed.");
|
|
875
|
+
const assertCurrent = (): void => {
|
|
876
|
+
if (!options.isCurrent()) throw new TelegramRuntimeStateError("authority-changed", "Telegram runtime state publication authority changed.");
|
|
877
|
+
};
|
|
878
|
+
assertCurrent();
|
|
879
|
+
active.add(path);
|
|
880
|
+
try {
|
|
881
|
+
const runtimeDir = join(dirname(path), "runtime");
|
|
882
|
+
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
883
|
+
return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
|
|
884
|
+
assertCurrent();
|
|
885
|
+
const file = readTelegramRuntimeState(path);
|
|
886
|
+
const previous = Object.hasOwn(file.profiles, profile) ? file.profiles[profile]! : {};
|
|
887
|
+
const outcome = mutate(structuredClone(previous[section]), structuredClone(previous));
|
|
888
|
+
if (!runtimeStateRecord(outcome) || !Object.hasOwn(outcome, "value") || !Object.hasOwn(outcome, "result") || "then" in outcome)
|
|
889
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram runtime state mutations must return a synchronous value and result.");
|
|
890
|
+
// Normalize intentional optional properties to their wire representation before comparison/publication.
|
|
891
|
+
const next = outcome.value === undefined ? undefined : JSON.parse(JSON.stringify(outcome.value)) as unknown;
|
|
892
|
+
assertCurrent();
|
|
893
|
+
if (isDeepStrictEqual(previous[section], next)) return outcome.result;
|
|
894
|
+
const updated = { ...previous };
|
|
895
|
+
if (next === undefined) delete updated[section];
|
|
896
|
+
else updated[section] = next;
|
|
897
|
+
if (Object.keys(updated).length) Object.defineProperty(file.profiles, profile, { value: updated, enumerable: true, configurable: true, writable: true });
|
|
898
|
+
else delete file.profiles[profile];
|
|
899
|
+
const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
900
|
+
try {
|
|
901
|
+
options.onPublicationBoundary?.("before-write");
|
|
902
|
+
assertCurrent();
|
|
903
|
+
writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
904
|
+
options.onPublicationBoundary?.("after-write-before-rename");
|
|
905
|
+
assertCurrent();
|
|
906
|
+
try {
|
|
907
|
+
if (!renameTelegramPathWithRetry(tempPath, path, { rename: options.publishRename }))
|
|
908
|
+
throw new Error("Telegram runtime state temporary publication disappeared.");
|
|
909
|
+
options.onPublicationBoundary?.("after-rename");
|
|
910
|
+
assertCurrent();
|
|
911
|
+
} catch (error) {
|
|
912
|
+
throw new TelegramRuntimeStateError("publication-unknown", "Telegram runtime state publication outcome is unknown.", { cause: error });
|
|
913
|
+
}
|
|
914
|
+
return outcome.result;
|
|
915
|
+
} finally {
|
|
916
|
+
try { unlinkSync(tempPath); }
|
|
917
|
+
catch (error) { if ((error as { code?: unknown }).code !== "ENOENT") throw error; }
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
} finally { active.delete(path); }
|
|
921
|
+
}
|
|
922
|
+
|
|
653
923
|
function withLockTransaction<T>(
|
|
654
924
|
locksPath: string,
|
|
655
925
|
mutate: (locks: Record<string, unknown>) => {
|
|
@@ -730,6 +1000,7 @@ export function parseTelegramLockEntry(
|
|
|
730
1000
|
: undefined,
|
|
731
1001
|
busSecret:
|
|
732
1002
|
typeof record.busSecret === "string" ? record.busSecret : undefined,
|
|
1003
|
+
...(readTelegramLockJournalPath(record) ? { journalPath: readTelegramLockJournalPath(record) } : {}),
|
|
733
1004
|
};
|
|
734
1005
|
}
|
|
735
1006
|
|
|
@@ -744,7 +1015,7 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
744
1015
|
}
|
|
745
1016
|
}
|
|
746
1017
|
|
|
747
|
-
|
|
1018
|
+
function formatTelegramLockEntry(lock: TelegramLockEntry): string {
|
|
748
1019
|
return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
|
|
749
1020
|
}
|
|
750
1021
|
|
|
@@ -832,9 +1103,11 @@ function createLockEntry(
|
|
|
832
1103
|
getNowMs?: () => number;
|
|
833
1104
|
mintLeaderEpoch?: () => number | string;
|
|
834
1105
|
runtimeGeneration?: number;
|
|
1106
|
+
journalPath?: string;
|
|
835
1107
|
},
|
|
836
1108
|
): TelegramLockEntry {
|
|
837
1109
|
const lock: TelegramLockEntry = { pid, cwd: ctx.cwd };
|
|
1110
|
+
if (options.journalPath) lock.journalPath = options.journalPath;
|
|
838
1111
|
if (options.instanceId) {
|
|
839
1112
|
const nowMs = options.getNowMs?.();
|
|
840
1113
|
lock.instanceId = options.instanceId;
|
|
@@ -860,10 +1133,27 @@ function formatLockState(state: TelegramLockState): string {
|
|
|
860
1133
|
}
|
|
861
1134
|
}
|
|
862
1135
|
|
|
1136
|
+
function assertTelegramStateTransport(value: unknown): void {
|
|
1137
|
+
if (value === undefined) return;
|
|
1138
|
+
if (!runtimeStateRecord(value)) throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
1139
|
+
const keys = ["pid", "cwd", "instanceId", "heartbeatMs", "leaderEpoch", "runtimeGeneration", "busSocketPath", "busSecret", "journalPath"];
|
|
1140
|
+
if (Object.keys(value).some(key => !keys.includes(key)) ||
|
|
1141
|
+
(value.pid === undefined ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value) :
|
|
1142
|
+
!Number.isSafeInteger(value.pid) || (value.pid as number) <= 0) ||
|
|
1143
|
+
["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some(key => value[key] !== undefined && typeof value[key] !== "string") ||
|
|
1144
|
+
["heartbeatMs", "runtimeGeneration"].some(key => value[key] !== undefined && (!Number.isSafeInteger(value[key]) || (value[key] as number) < 0)) ||
|
|
1145
|
+
(value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
|
|
1146
|
+
(value.leaderEpoch !== undefined && (typeof value.leaderEpoch === "string" ? value.leaderEpoch.length === 0 : !Number.isSafeInteger(value.leaderEpoch))))
|
|
1147
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
|
|
1148
|
+
}
|
|
1149
|
+
|
|
863
1150
|
export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
864
1151
|
options: TelegramLockRuntimeOptions = {},
|
|
865
1152
|
): TelegramLockRuntime<TContext> {
|
|
866
1153
|
const key = options.key ?? TELEGRAM_LOCK_KEY;
|
|
1154
|
+
if (options.statePath && options.locksPath)
|
|
1155
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram transport must select one storage identity.");
|
|
1156
|
+
const statePath = options.statePath;
|
|
867
1157
|
const locksPath = options.locksPath ?? getOwnersPath();
|
|
868
1158
|
const pid = options.pid ?? process.pid;
|
|
869
1159
|
const isAlive = options.isProcessAlive ?? isProcessAlive;
|
|
@@ -881,9 +1171,48 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
881
1171
|
if (typeof key === "function") return key() || TELEGRAM_LOCK_KEY;
|
|
882
1172
|
return key;
|
|
883
1173
|
};
|
|
1174
|
+
const readOwners = (): Record<string, unknown> => {
|
|
1175
|
+
if (!statePath) return readLocks(locksPath);
|
|
1176
|
+
// Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
|
|
1177
|
+
try {
|
|
1178
|
+
const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
|
|
1179
|
+
.filter(([, value]) => Object.hasOwn(value, "transport")).map(([profile, value]) => [profile, value.transport]));
|
|
1180
|
+
assertTelegramStateTransport(transports[resolveEffectiveKey()]);
|
|
1181
|
+
return transports;
|
|
1182
|
+
} catch { return {}; }
|
|
1183
|
+
};
|
|
1184
|
+
const transactOwners = <T>(mutate: (locks: Record<string, unknown>) => { result: T; changed: boolean }): T => {
|
|
1185
|
+
if (!statePath) return withLockTransaction(locksPath, mutate);
|
|
1186
|
+
const profile = resolveEffectiveKey();
|
|
1187
|
+
return mutateTelegramRuntimeStateSection(statePath, profile, "transport", current => {
|
|
1188
|
+
assertTelegramStateTransport(current);
|
|
1189
|
+
const locks = { [profile]: current };
|
|
1190
|
+
const outcome = mutate(locks);
|
|
1191
|
+
const value = outcome.changed ? locks[profile] : current;
|
|
1192
|
+
assertTelegramStateTransport(value);
|
|
1193
|
+
return { value, result: outcome.result };
|
|
1194
|
+
}, { ...options.statePublication, isCurrent: () => resolveEffectiveKey() === profile });
|
|
1195
|
+
};
|
|
884
1196
|
const readLock = () => {
|
|
885
1197
|
const effectiveKey = resolveEffectiveKey();
|
|
886
|
-
return parseTelegramLockEntry(
|
|
1198
|
+
return parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
1199
|
+
};
|
|
1200
|
+
// A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
|
|
1201
|
+
// endpoint, secret and epoch belong to another protocol and must never become a follower target.
|
|
1202
|
+
const readLegacyOwner = (effectiveKey: string): TelegramLockEntry | undefined => {
|
|
1203
|
+
if (!options.legacyLocksPath || options.legacyLocksPath === (statePath ?? locksPath)) return undefined;
|
|
1204
|
+
try {
|
|
1205
|
+
const records = statePath ? readLocksForTransaction(options.legacyLocksPath) : readLocks(options.legacyLocksPath);
|
|
1206
|
+
if (statePath) for (const value of Object.values(records)) assertTelegramStateTransport(value);
|
|
1207
|
+
const raw = records[effectiveKey];
|
|
1208
|
+
const legacy = parseTelegramLockEntry(raw);
|
|
1209
|
+
if (!legacy || getLockState(legacy, -1, isAlive, stateOptions()).kind !== "active-elsewhere") return undefined;
|
|
1210
|
+
return { pid: legacy.pid, ...(legacy.cwd ? { cwd: legacy.cwd } : {}), ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
|
|
1211
|
+
...(legacy.heartbeatMs !== undefined ? { heartbeatMs: legacy.heartbeatMs } : {}) };
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (statePath) throw error;
|
|
1214
|
+
return undefined;
|
|
1215
|
+
}
|
|
887
1216
|
};
|
|
888
1217
|
const adoptCompatibleOwnedLock = (
|
|
889
1218
|
effectiveKey: string,
|
|
@@ -908,8 +1237,10 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
908
1237
|
};
|
|
909
1238
|
return {
|
|
910
1239
|
acquire: (ctx, acquireOptions = {}) =>
|
|
911
|
-
|
|
1240
|
+
transactOwners<TelegramLockAcquireResult>((locks) => {
|
|
912
1241
|
const effectiveKey = resolveEffectiveKey();
|
|
1242
|
+
const legacy = readLegacyOwner(effectiveKey);
|
|
1243
|
+
if (legacy) return { result: { ok: false, lock: legacy } as const, changed: false };
|
|
913
1244
|
const current = parseTelegramLockEntry(locks[effectiveKey]);
|
|
914
1245
|
const state = getLockState(current, pid, isAlive, stateOptions());
|
|
915
1246
|
const expectedOwned = adoptCompatibleOwnedLock(
|
|
@@ -971,7 +1302,9 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
971
1302
|
changed: false,
|
|
972
1303
|
};
|
|
973
1304
|
}
|
|
1305
|
+
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ?? options.createJournalPath?.(ctx);
|
|
974
1306
|
const lock = createLockEntry(pid, ctx, {
|
|
1307
|
+
journalPath,
|
|
975
1308
|
instanceId: options.instanceId,
|
|
976
1309
|
busSocketPath: options.busSocketPath,
|
|
977
1310
|
busSecret: options.busSecret,
|
|
@@ -995,7 +1328,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
995
1328
|
release: () => {
|
|
996
1329
|
// Withdraw local send authority even if the durable release fails.
|
|
997
1330
|
deliveryRevoked = true;
|
|
998
|
-
return
|
|
1331
|
+
return transactOwners((locks) => {
|
|
999
1332
|
const effectiveKey = resolveEffectiveKey();
|
|
1000
1333
|
const state = getLockState(
|
|
1001
1334
|
parseTelegramLockEntry(locks[effectiveKey]),
|
|
@@ -1010,7 +1343,9 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1010
1343
|
ownedLock,
|
|
1011
1344
|
);
|
|
1012
1345
|
if (changed) {
|
|
1013
|
-
|
|
1346
|
+
const journalPath = readTelegramLockJournalPath(locks[effectiveKey]);
|
|
1347
|
+
if (journalPath) locks[effectiveKey] = { journalPath };
|
|
1348
|
+
else delete locks[effectiveKey];
|
|
1014
1349
|
ownedLockKey = undefined;
|
|
1015
1350
|
ownedLock = undefined;
|
|
1016
1351
|
}
|
|
@@ -1018,26 +1353,27 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1018
1353
|
});
|
|
1019
1354
|
},
|
|
1020
1355
|
getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
|
|
1356
|
+
getJournalPath: () => readTelegramLockJournalPath(readOwners()[resolveEffectiveKey()]),
|
|
1021
1357
|
getStatusLabel: () =>
|
|
1022
1358
|
formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
|
|
1023
1359
|
getOwnedLeaderEpoch: () => {
|
|
1024
1360
|
if (deliveryRevoked) return undefined;
|
|
1025
1361
|
const effectiveKey = resolveEffectiveKey();
|
|
1026
|
-
const lock = parseTelegramLockEntry(
|
|
1362
|
+
const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
1027
1363
|
const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
|
|
1028
1364
|
return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
|
|
1029
1365
|
},
|
|
1030
1366
|
owns: (ctx) => {
|
|
1031
1367
|
if (deliveryRevoked) return false;
|
|
1032
1368
|
const effectiveKey = resolveEffectiveKey();
|
|
1033
|
-
const lock = parseTelegramLockEntry(
|
|
1369
|
+
const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
|
|
1034
1370
|
return hasSameLockOwner(
|
|
1035
1371
|
lock,
|
|
1036
1372
|
adoptCompatibleOwnedLock(effectiveKey, lock, ctx),
|
|
1037
1373
|
);
|
|
1038
1374
|
},
|
|
1039
1375
|
commitIfOwned: (commit) =>
|
|
1040
|
-
!deliveryRevoked &&
|
|
1376
|
+
!deliveryRevoked && transactOwners((locks) => {
|
|
1041
1377
|
const effectiveKey = resolveEffectiveKey();
|
|
1042
1378
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1043
1379
|
const exactOwner =
|
|
@@ -1050,10 +1386,36 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1050
1386
|
return { result: false, changed: false };
|
|
1051
1387
|
}
|
|
1052
1388
|
commit();
|
|
1053
|
-
|
|
1389
|
+
// External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
|
|
1390
|
+
const current = !statePath || (!deliveryRevoked && hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
|
|
1391
|
+
return { result: current, changed: false };
|
|
1054
1392
|
}),
|
|
1393
|
+
publishStateSectionIfOwned<T>(
|
|
1394
|
+
section: "workspace" | "runtime",
|
|
1395
|
+
mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
|
|
1396
|
+
publication: TelegramOwnedStatePublicationOptions,
|
|
1397
|
+
): TelegramOwnedStatePublicationResult<T> {
|
|
1398
|
+
if (section !== "workspace" && section !== "runtime")
|
|
1399
|
+
throw new TelegramRuntimeStateError("invalid", "Telegram owner section publication is restricted to Workspace and runtime observations.");
|
|
1400
|
+
if (!statePath) throw new TelegramRuntimeStateError("invalid", "Consolidated transport storage is not selected.");
|
|
1401
|
+
const profile = resolveEffectiveKey(), expected = ownedLock ? { ...ownedLock } : undefined;
|
|
1402
|
+
if (publication.expectedScope && (publication.expectedScope.path !== statePath || publication.expectedScope.profile !== profile))
|
|
1403
|
+
return { committed: false };
|
|
1404
|
+
if (deliveryRevoked || ownedLockKey !== profile || !expected || !publication.isCurrent()) return { committed: false };
|
|
1405
|
+
const isCurrent = (): boolean => !deliveryRevoked && resolveEffectiveKey() === profile && ownedLockKey === profile &&
|
|
1406
|
+
hasSameLockOwner(ownedLock, expected) && publication.isCurrent() &&
|
|
1407
|
+
hasSameLockOwner(parseTelegramLockEntry(readOwners()[profile]), expected);
|
|
1408
|
+
if (!isCurrent()) return { committed: false };
|
|
1409
|
+
return mutateTelegramRuntimeStateSection<TelegramOwnedStatePublicationResult<T>>(statePath, profile, section, (current, observed) => {
|
|
1410
|
+
assertTelegramStateTransport(observed.transport);
|
|
1411
|
+
if (!hasSameLockOwner(parseTelegramLockEntry(observed.transport), expected))
|
|
1412
|
+
return { value: current, result: { committed: false } as const };
|
|
1413
|
+
const outcome = mutate(current, observed);
|
|
1414
|
+
return { value: outcome.value, result: { committed: true, result: outcome.result } as const };
|
|
1415
|
+
}, { ...publication, isCurrent });
|
|
1416
|
+
},
|
|
1055
1417
|
refresh: (ctx) =>
|
|
1056
|
-
!deliveryRevoked &&
|
|
1418
|
+
!deliveryRevoked && transactOwners((locks) => {
|
|
1057
1419
|
const effectiveKey = resolveEffectiveKey();
|
|
1058
1420
|
const lock = parseTelegramLockEntry(locks[effectiveKey]);
|
|
1059
1421
|
const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
|
|
@@ -1077,6 +1439,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
1077
1439
|
? { busSocketPath: options.busSocketPath }
|
|
1078
1440
|
: {}),
|
|
1079
1441
|
busSecret: options.busSecret ?? lock.busSecret,
|
|
1442
|
+
...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
|
|
1080
1443
|
};
|
|
1081
1444
|
locks[effectiveKey] = refreshedLock;
|
|
1082
1445
|
ownedLockKey = effectiveKey;
|
|
@@ -1106,7 +1469,7 @@ export function createTelegramDirectDeliveryOwnershipChecker<
|
|
|
1106
1469
|
};
|
|
1107
1470
|
}
|
|
1108
1471
|
|
|
1109
|
-
|
|
1472
|
+
interface TelegramLockedPollingStartOptions {
|
|
1110
1473
|
force?: boolean;
|
|
1111
1474
|
forceFreshLeaderThread?: boolean;
|
|
1112
1475
|
requestedThreadName?: string;
|
|
@@ -1114,7 +1477,7 @@ export interface TelegramLockedPollingStartOptions {
|
|
|
1114
1477
|
onAcquired?: () => Promise<void> | void;
|
|
1115
1478
|
}
|
|
1116
1479
|
|
|
1117
|
-
|
|
1480
|
+
type TelegramLockedPollingStartResult =
|
|
1118
1481
|
| { ok: true; message?: string; canTakeover?: false }
|
|
1119
1482
|
| { ok: false; message: string; canTakeover?: boolean; owner?: string };
|
|
1120
1483
|
|
|
@@ -1128,6 +1491,8 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1128
1491
|
stop: () => Promise<string>;
|
|
1129
1492
|
suspend: () => Promise<void>;
|
|
1130
1493
|
isSuspended: () => boolean;
|
|
1494
|
+
/** Fence one owned polling generation; suspension, restart, conflict or lock loss revokes it. */
|
|
1495
|
+
captureTransportAuthority: (ctx: TContext) => (() => boolean) | undefined;
|
|
1131
1496
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
1132
1497
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
1133
1498
|
registerFollowerWithOwner?: (
|
|
@@ -1141,10 +1506,12 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1141
1506
|
stopFollowerRegistration?: () => void;
|
|
1142
1507
|
}
|
|
1143
1508
|
|
|
1144
|
-
|
|
1509
|
+
interface TelegramLockedPollingRuntimeDeps<
|
|
1145
1510
|
TContext extends TelegramLockContext,
|
|
1146
1511
|
> {
|
|
1147
1512
|
lock: TelegramLockRuntime<TContext>;
|
|
1513
|
+
/** Optimistically replaces a damaged shared runtime state before a non-election acquisition. */
|
|
1514
|
+
resetDamagedState?: () => boolean;
|
|
1148
1515
|
hasBotToken: () => boolean;
|
|
1149
1516
|
getBotTokenDiagnostic?: () => string | undefined;
|
|
1150
1517
|
canStartPolling?: (ctx: TContext) => boolean;
|
|
@@ -1343,6 +1710,12 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1343
1710
|
(deps.isContextCurrent?.(ctx) ?? true);
|
|
1344
1711
|
if (ownershipStop) await ownershipStop;
|
|
1345
1712
|
if (!isCurrent()) return cancelled;
|
|
1713
|
+
if (!options.election && deps.resetDamagedState) {
|
|
1714
|
+
try {
|
|
1715
|
+
if (deps.resetDamagedState()) deps.recordRuntimeEvent?.("lock",
|
|
1716
|
+
new Error("Damaged Telegram runtime state was reset; previous runtime continuity was discarded."), { phase: "state-reset" });
|
|
1717
|
+
} catch (error) { deps.recordRuntimeEvent?.("lock", error, { phase: "state-reset" }); }
|
|
1718
|
+
}
|
|
1346
1719
|
let acquired = deps.lock.acquire(ctx, {
|
|
1347
1720
|
force: options.force,
|
|
1348
1721
|
expectedOwner:
|
|
@@ -1438,6 +1811,12 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1438
1811
|
return "Telegram bridge disconnected.";
|
|
1439
1812
|
},
|
|
1440
1813
|
suspend: suspendPolling,
|
|
1814
|
+
captureTransportAuthority(ctx) {
|
|
1815
|
+
const generation = pollingGeneration;
|
|
1816
|
+
const current = () => generation === pollingGeneration && activeContext === ctx && !ownershipStop &&
|
|
1817
|
+
(deps.isContextCurrent?.(ctx) ?? true) && deps.lock.owns(snapshotLockContext(ctx));
|
|
1818
|
+
return current() ? current : undefined;
|
|
1819
|
+
},
|
|
1441
1820
|
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1442
1821
|
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1443
1822
|
onPersistentConflict: async (ctx, count) => {
|