@llblab/pi-telegram 0.51.5 → 0.52.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 +10 -8
- package/BACKLOG.md +1 -8
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +16 -6
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +5 -7
- package/dist/lib/bindings.d.ts +9 -2
- package/dist/lib/bindings.js +22 -13
- package/dist/lib/bus-api.js +4 -2
- package/dist/lib/bus-follower.d.ts +55 -57
- package/dist/lib/bus-follower.js +213 -140
- package/dist/lib/bus-leader.d.ts +21 -2
- package/dist/lib/bus-leader.js +181 -25
- package/dist/lib/bus-transport.d.ts +4 -1
- package/dist/lib/bus-transport.js +28 -4
- package/dist/lib/bus.d.ts +52 -56
- package/dist/lib/bus.js +78 -246
- package/dist/lib/channel-posts.d.ts +1 -3
- package/dist/lib/channel-posts.js +23 -26
- package/dist/lib/command-templates.d.ts +0 -5
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/commands.d.ts +12 -26
- package/dist/lib/commands.js +152 -98
- package/dist/lib/config.d.ts +0 -17
- package/dist/lib/config.js +12 -14
- package/dist/lib/delivery.d.ts +0 -4
- package/dist/lib/delivery.js +2 -5
- package/dist/lib/extension.js +281 -61
- package/dist/lib/generative-apps.d.ts +0 -19
- package/dist/lib/inbound.d.ts +0 -2
- package/dist/lib/inbound.js +2 -2
- package/dist/lib/journal.d.ts +177 -16
- package/dist/lib/journal.js +1115 -249
- package/dist/lib/keyboard.d.ts +0 -2
- package/dist/lib/keyboard.js +2 -2
- package/dist/lib/lifecycle.d.ts +57 -2
- package/dist/lib/lifecycle.js +110 -5
- package/dist/lib/locks.d.ts +100 -12
- package/dist/lib/locks.js +380 -14
- package/dist/lib/logging.d.ts +30 -12
- package/dist/lib/logging.js +129 -72
- package/dist/lib/media.d.ts +28 -5
- package/dist/lib/media.js +26 -8
- package/dist/lib/menu-model.d.ts +0 -11
- package/dist/lib/menu-model.js +8 -8
- package/dist/lib/menu-settings.d.ts +0 -18
- package/dist/lib/menu-settings.js +18 -18
- package/dist/lib/menu-status.d.ts +4 -4
- package/dist/lib/menu-status.js +12 -7
- package/dist/lib/menu-thinking.d.ts +1 -1
- package/dist/lib/menu-thinking.js +1 -1
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +4 -3
- package/dist/lib/model.js +2 -4
- package/dist/lib/outbound-attachments.d.ts +0 -1
- package/dist/lib/outbound-attachments.js +1 -1
- package/dist/lib/outbound-buttons.js +1 -8
- package/dist/lib/outbound-markup.d.ts +1 -6
- package/dist/lib/outbound-markup.js +4 -21
- package/dist/lib/outbound.d.ts +0 -8
- package/dist/lib/outbound.js +5 -5
- package/dist/lib/paths.d.ts +50 -7
- package/dist/lib/paths.js +155 -17
- package/dist/lib/pi.d.ts +1 -0
- package/dist/lib/pi.js +2 -1
- package/dist/lib/polling.d.ts +0 -5
- package/dist/lib/polling.js +5 -5
- package/dist/lib/preview.d.ts +0 -3
- package/dist/lib/preview.js +3 -3
- package/dist/lib/prompt-templates.d.ts +0 -1
- package/dist/lib/prompt-templates.js +1 -1
- package/dist/lib/queue.d.ts +3 -15
- package/dist/lib/queue.js +69 -8
- package/dist/lib/recovery.d.ts +29 -9
- package/dist/lib/recovery.js +104 -33
- package/dist/lib/replies.d.ts +0 -4
- package/dist/lib/replies.js +1 -1
- package/dist/lib/routing.d.ts +73 -8
- package/dist/lib/routing.js +2206 -307
- package/dist/lib/sections.d.ts +0 -2
- package/dist/lib/sections.js +1 -1
- package/dist/lib/status.d.ts +46 -9
- package/dist/lib/status.js +179 -27
- package/dist/lib/sync.d.ts +0 -5
- package/dist/lib/sync.js +9 -8
- package/dist/lib/telegram-api.d.ts +11 -3
- package/dist/lib/telegram-api.js +34 -23
- package/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/dist/lib/thread-cleanup-manager.js +18 -21
- package/dist/lib/thread-naming.d.ts +32 -5
- package/dist/lib/thread-naming.js +191 -7
- package/dist/lib/thread-reconciler.d.ts +2 -0
- package/dist/lib/thread-reconciler.js +1 -1
- package/dist/lib/threads.d.ts +292 -59
- package/dist/lib/threads.js +1664 -373
- package/dist/lib/turns.d.ts +0 -1
- package/dist/lib/turns.js +1 -1
- package/dist/lib/updates.d.ts +184 -28
- package/dist/lib/updates.js +1032 -114
- package/dist/lib/wire.d.ts +12 -0
- package/dist/lib/wire.js +21 -0
- package/dist/lib/workspace-admission.d.ts +25 -17
- package/dist/lib/workspace-admission.js +95 -20
- package/dist/lib/workspace-identity.d.ts +20 -0
- package/dist/lib/workspace-identity.js +103 -0
- package/dist/lib/workspace-retirement.d.ts +45 -10
- package/dist/lib/workspace-retirement.js +196 -64
- package/dist/package.json +6 -6
- package/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/docs/README.md +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +158 -39
- package/docs/callback-namespaces.md +1 -1
- package/docs/delivery.md +1 -1
- package/docs/generative-apps.md +2 -2
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +127 -19
- package/docs/public-api.md +5 -3
- package/docs/ui-style.md +26 -8
- package/docs/updates.md +1 -1
- package/lib/activity-verbosity.ts +5 -9
- package/lib/bindings.ts +23 -10
- package/lib/bus-api.ts +5 -2
- package/lib/bus-follower.ts +251 -226
- package/lib/bus-leader.ts +178 -31
- package/lib/bus-transport.ts +30 -8
- package/lib/bus.ts +110 -334
- package/lib/channel-posts.ts +29 -29
- package/lib/command-templates.ts +5 -5
- package/lib/commands.ts +150 -112
- package/lib/config.ts +12 -17
- package/lib/delivery.ts +2 -8
- package/lib/extension.ts +282 -72
- package/lib/generative-apps.ts +0 -22
- package/lib/inbound.ts +2 -2
- package/lib/journal.ts +1182 -326
- package/lib/keyboard.ts +2 -2
- package/lib/lifecycle.ts +141 -5
- package/lib/locks.ts +402 -23
- package/lib/logging.ts +151 -101
- package/lib/media.ts +51 -9
- package/lib/menu-model.ts +8 -8
- package/lib/menu-settings.ts +18 -18
- package/lib/menu-status.ts +11 -0
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +5 -1
- package/lib/model.ts +2 -4
- package/lib/outbound-attachments.ts +1 -1
- package/lib/outbound-buttons.ts +1 -10
- package/lib/outbound-markup.ts +5 -24
- package/lib/outbound.ts +5 -5
- package/lib/paths.ts +177 -17
- package/lib/pi.ts +5 -2
- package/lib/polling.ts +5 -5
- package/lib/preview.ts +3 -3
- package/lib/prompt-templates.ts +1 -1
- package/lib/queue.ts +67 -9
- package/lib/recovery.ts +104 -44
- package/lib/replies.ts +1 -1
- package/lib/routing.ts +1955 -375
- package/lib/sections.ts +1 -1
- package/lib/status.ts +185 -36
- package/lib/sync.ts +8 -8
- package/lib/telegram-api.ts +48 -33
- package/lib/thread-cleanup-manager.ts +24 -21
- package/lib/thread-naming.ts +267 -9
- package/lib/thread-reconciler.ts +3 -1
- package/lib/threads.ts +1718 -491
- package/lib/turns.ts +1 -1
- package/lib/updates.ts +1072 -151
- package/lib/wire.ts +28 -0
- package/lib/workspace-admission.ts +97 -47
- package/lib/workspace-identity.ts +147 -0
- package/lib/workspace-retirement.ts +234 -102
- package/package.json +6 -6
- package/scripts/audit-exports.mjs +100 -0
- package/scripts/check-downgrade.mjs +80 -43
- package/skills/generated-control-surface/SKILL.md +3 -3
- package/skills/telegram-bridge/references/diagnosis.md +3 -3
package/dist/lib/bus-follower.js
CHANGED
|
@@ -6,21 +6,23 @@
|
|
|
6
6
|
* It must not spawn Pi processes or create hidden Telegram-originated instances.
|
|
7
7
|
*/
|
|
8
8
|
import { basename } from "node:path";
|
|
9
|
-
import
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
10
|
import * as Threads from "./threads.js";
|
|
11
|
+
import * as WorkspaceIdentity from "./workspace-identity.js";
|
|
12
|
+
import { isWireRecord as isRecord } from "./wire.js";
|
|
11
13
|
import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
|
|
12
14
|
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
13
15
|
import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
14
|
-
import { createTelegramBusFollowerDeliveryIdentity,
|
|
16
|
+
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, } from "./bus.js";
|
|
15
17
|
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
16
18
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
20
|
+
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
21
|
+
const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
22
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
|
|
21
23
|
export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
|
|
25
|
+
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
|
|
24
26
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
|
|
25
27
|
export function getTelegramFollowerSessionHandoff() {
|
|
26
28
|
const value = globalThis[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY];
|
|
@@ -44,7 +46,7 @@ export function setTelegramFollowerSessionHandoff(handoff) {
|
|
|
44
46
|
else
|
|
45
47
|
store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
|
|
48
50
|
if (!handoff)
|
|
49
51
|
return false;
|
|
50
52
|
const pid = options.pid ?? process.pid;
|
|
@@ -147,16 +149,21 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
147
149
|
(ctx === undefined || readyContext.ctx === ctx) &&
|
|
148
150
|
ports.registrationState.getGeneration() === readyContext.generation &&
|
|
149
151
|
ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
|
|
152
|
+
ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
|
|
150
153
|
ports.registration.isContextActive?.(readyContext.ctx) !== false);
|
|
151
154
|
const prepareContext = async (ctx) => {
|
|
152
155
|
const generation = ports.registrationState.getGeneration();
|
|
153
156
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
157
|
+
const sessionId = ports.registration.getSessionId?.(ctx);
|
|
154
158
|
readyContext = undefined;
|
|
159
|
+
if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
|
|
160
|
+
throw new Error("Telegram follower session change requires acknowledged leader registration.");
|
|
155
161
|
await ports.registration.onRegistered?.(ctx);
|
|
156
162
|
if (generation && ports.registrationState.getGeneration() === generation &&
|
|
157
163
|
ports.registration.getSessionGeneration?.() === sessionGeneration &&
|
|
164
|
+
ports.registration.getSessionId?.(ctx) === sessionId &&
|
|
158
165
|
ports.registration.isContextActive?.(ctx) !== false)
|
|
159
|
-
readyContext = { generation, ctx, sessionGeneration };
|
|
166
|
+
readyContext = { generation, ctx, sessionGeneration, sessionId };
|
|
160
167
|
};
|
|
161
168
|
const sharedRuntimeDeps = {
|
|
162
169
|
instanceId: ports.instanceId,
|
|
@@ -170,10 +177,6 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
170
177
|
getRegistrationGeneration() {
|
|
171
178
|
return isReadyContext() ? readyContext?.generation : undefined;
|
|
172
179
|
},
|
|
173
|
-
handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
|
|
174
|
-
...ports.targetReplacement,
|
|
175
|
-
...sharedRuntimeDeps,
|
|
176
|
-
}),
|
|
177
180
|
});
|
|
178
181
|
let registration;
|
|
179
182
|
const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
|
|
@@ -205,83 +208,123 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
205
208
|
await prepareContext(ctx);
|
|
206
209
|
},
|
|
207
210
|
};
|
|
208
|
-
return { receiver, registration };
|
|
211
|
+
return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
|
|
209
212
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
213
|
+
/** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
|
|
214
|
+
export function createTelegramBusFollowerRestoreContextGetter(deps) {
|
|
215
|
+
return (ctx) => {
|
|
216
|
+
if (!deps.isContextCurrent(ctx))
|
|
217
|
+
return undefined;
|
|
218
|
+
const leader = deps.getLeaderState();
|
|
219
|
+
const secret = deps.getAuthenticatedSecret();
|
|
220
|
+
const leaderProtocol = deps.getLeaderProtocol();
|
|
221
|
+
if (!leaderProtocol || !hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
|
|
222
|
+
return undefined;
|
|
223
|
+
if (leader.kind !== "active-elsewhere" || !secret || leader.lock.busSecret !== secret ||
|
|
224
|
+
!leader.lock.instanceId || leader.lock.leaderEpoch === undefined)
|
|
225
|
+
return undefined;
|
|
226
|
+
const profileBindingKey = deps.getProfileBindingKey();
|
|
227
|
+
const operatorUserId = deps.getOperatorUserId();
|
|
228
|
+
const sessionId = deps.getSessionId(ctx);
|
|
229
|
+
const cwd = deps.getCwd(ctx);
|
|
230
|
+
const generation = deps.getGeneration();
|
|
231
|
+
if (!profileBindingKey || !sessionId || !cwd || typeof operatorUserId !== "number" ||
|
|
232
|
+
!Number.isSafeInteger(operatorUserId) || operatorUserId <= 0 ||
|
|
233
|
+
!Number.isSafeInteger(generation) || generation < 0 || !deps.isContextCurrent(ctx))
|
|
234
|
+
return undefined;
|
|
235
|
+
return { executor: { instanceId: leader.lock.instanceId, leaderEpoch: String(leader.lock.leaderEpoch) },
|
|
236
|
+
profileBindingKey, operatorUserId, sessionId, cwd, generation, leaderProtocol: structuredClone(leaderProtocol) };
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
|
|
240
|
+
export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
241
|
+
return async (input, ctx) => {
|
|
242
|
+
const captured = deps.getContextAuthority(ctx);
|
|
243
|
+
if (!captured)
|
|
244
|
+
throw new Error("Stale Telegram follower Restore context.");
|
|
245
|
+
const authority = structuredClone(captured);
|
|
246
|
+
const request = structuredClone(input);
|
|
247
|
+
if (!authority.profileBindingKey || !authority.sessionId || !authority.cwd ||
|
|
248
|
+
!authority.executor?.instanceId || !authority.executor.leaderEpoch ||
|
|
249
|
+
!(typeof authority.generation === "string" ? authority.generation.length > 0 :
|
|
250
|
+
Number.isSafeInteger(authority.generation) && authority.generation >= 0) ||
|
|
251
|
+
!Number.isSafeInteger(authority.operatorUserId) || authority.operatorUserId <= 0 ||
|
|
252
|
+
!request.operationId || !request.registrationGeneration || !["apply", "inspect"].includes(request.mode))
|
|
253
|
+
throw new Error("Invalid Telegram follower Restore authority.");
|
|
254
|
+
const assertCurrent = () => {
|
|
255
|
+
if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
|
|
256
|
+
deps.registrationState.getGeneration() !== request.registrationGeneration)
|
|
257
|
+
throw new Error("Stale Telegram follower Restore authority.");
|
|
226
258
|
};
|
|
227
259
|
assertCurrent();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
260
|
+
if (!deps.getWorkspaceAdmission)
|
|
261
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
262
|
+
const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
|
|
263
|
+
assertCurrent();
|
|
264
|
+
const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
|
|
265
|
+
assertCurrent();
|
|
266
|
+
if (!intent || intent.request.operationId !== request.operationId ||
|
|
267
|
+
(intent.phase !== "recipient-issued" && intent.phase !== "ready") ||
|
|
268
|
+
!isDeepStrictEqual(intent.executor, authority.executor) ||
|
|
269
|
+
intent.operatorUserId !== authority.operatorUserId || !intent.recipient ||
|
|
270
|
+
(request.mode === "apply" && (intent.recipient.kind !== "follower" || intent.recipient.instanceId !== deps.instanceId ||
|
|
271
|
+
intent.recipient.generation !== request.registrationGeneration)) ||
|
|
272
|
+
intent.recipient.sessionId !== authority.sessionId || intent.request.binding.sessionId !== authority.sessionId ||
|
|
273
|
+
intent.request.binding.cwd !== WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
|
|
274
|
+
throw new Error("Telegram follower Restore intent does not match this recipient.");
|
|
275
|
+
const expected = structuredClone(intent);
|
|
276
|
+
await deps.topicTargetStore.load();
|
|
277
|
+
assertCurrent();
|
|
278
|
+
const observe = (canonical) => {
|
|
279
|
+
if (!isDeepStrictEqual(deps.readRestoreIntent(request.operationId, authority.profileBindingKey), expected))
|
|
280
|
+
throw new Error("Telegram follower Restore intent changed.");
|
|
281
|
+
assertCurrent();
|
|
282
|
+
const { binding, target } = expected.request;
|
|
283
|
+
const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
|
|
284
|
+
const snapshot = canonical.threads;
|
|
285
|
+
const records = snapshot.filter(value => value.instanceId === deps.instanceId);
|
|
286
|
+
const owners = snapshot.filter(value => value.status === "active" &&
|
|
287
|
+
(value.slot === binding.slot || isDeepStrictEqual(value.target, target)));
|
|
288
|
+
if (bindings.length !== 1 || records.length !== 1 || owners.length !== 1 || bindings[0]?.cwd !== binding.cwd ||
|
|
289
|
+
bindings[0]?.sessionId !== binding.sessionId || bindings[0]?.slot !== binding.slot || bindings[0]?.inactiveSinceMs !== undefined ||
|
|
290
|
+
!isDeepStrictEqual(bindings[0]?.target, target) || records[0]?.status !== "active" ||
|
|
291
|
+
records[0]?.owner?.kind !== "manual-follower" ||
|
|
292
|
+
records[0]?.slot !== binding.slot || !isDeepStrictEqual(records[0]?.target, target) ||
|
|
293
|
+
deps.registrationState.getSlot() !== binding.slot)
|
|
294
|
+
throw new Error("Telegram follower Restore canonical binding is not committed.");
|
|
295
|
+
const localTarget = deps.registrationState.getTarget();
|
|
296
|
+
const alreadyReady = isDeepStrictEqual(localTarget, target);
|
|
297
|
+
if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
|
|
298
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
299
|
+
assertCurrent();
|
|
300
|
+
if (!alreadyReady && request.mode === "apply") {
|
|
301
|
+
if (expected.phase !== "recipient-issued")
|
|
302
|
+
throw new Error("Telegram follower Restore readiness regressed.");
|
|
303
|
+
deps.registrationState.setRegistered(true, target, { slot: binding.slot,
|
|
304
|
+
threadName: records[0]?.threadName, generation: request.registrationGeneration,
|
|
305
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol() });
|
|
306
|
+
}
|
|
307
|
+
assertCurrent();
|
|
308
|
+
const observedTarget = deps.registrationState.getTarget();
|
|
309
|
+
if (!observedTarget || (!isDeepStrictEqual(observedTarget, target) && !isDeepStrictEqual(observedTarget, binding.target)))
|
|
310
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
311
|
+
const ready = isDeepStrictEqual(observedTarget, target);
|
|
312
|
+
assertCurrent();
|
|
313
|
+
return { operationId: request.operationId, recipient: { kind: "follower", instanceId: deps.instanceId,
|
|
314
|
+
sessionId: authority.sessionId, generation: request.registrationGeneration },
|
|
315
|
+
target: { chatId: observedTarget.chatId, threadId: observedTarget.threadId }, slot: binding.slot, ready };
|
|
316
|
+
};
|
|
317
|
+
let result;
|
|
318
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => { result = observe(snapshot); });
|
|
319
|
+
if (!result)
|
|
320
|
+
throw new Error("Telegram follower Restore observation was not completed.");
|
|
321
|
+
return result;
|
|
283
322
|
});
|
|
284
|
-
|
|
323
|
+
assertCurrent();
|
|
324
|
+
if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
|
|
325
|
+
throw new Error("Telegram follower Restore local registration changed after admission release.");
|
|
326
|
+
return result;
|
|
327
|
+
};
|
|
285
328
|
}
|
|
286
329
|
export function createTelegramBusFollowerClientRuntime(deps) {
|
|
287
330
|
const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
|
|
@@ -319,10 +362,6 @@ export function createTelegramBusFollowerClientRuntime(deps) {
|
|
|
319
362
|
getAuthSecret: deps.getApiAuthSecret,
|
|
320
363
|
getRegistrationGeneration: deps.getRegistrationGeneration,
|
|
321
364
|
}),
|
|
322
|
-
targetController: createTelegramBusFollowerTargetController({
|
|
323
|
-
...sharedClientDeps,
|
|
324
|
-
getAuthSecret: deps.getForwardingAuthSecret,
|
|
325
|
-
}),
|
|
326
365
|
};
|
|
327
366
|
}
|
|
328
367
|
export function createTelegramBusFollowerQueueHandoffClient(deps) {
|
|
@@ -377,7 +416,7 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
|
|
|
377
416
|
: "Telegram queue handoff did not return an acknowledgement.");
|
|
378
417
|
};
|
|
379
418
|
}
|
|
380
|
-
|
|
419
|
+
function createTelegramBusAgentMessageClient(deps) {
|
|
381
420
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
382
421
|
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
383
422
|
const request = async (envelope, requestTimeoutMs = timeoutMs) => {
|
|
@@ -519,7 +558,13 @@ function isTelegramStaleContextError(error) {
|
|
|
519
558
|
export function createTelegramBusFollowerSessionReplacementSuspender(deps) {
|
|
520
559
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
521
560
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
522
|
-
return async () => {
|
|
561
|
+
return async (preserveTarget = true) => {
|
|
562
|
+
if (!preserveTarget) {
|
|
563
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
564
|
+
Threads.setTelegramLeaderSessionHandoff(undefined);
|
|
565
|
+
await deps.suspendPolling();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
523
568
|
const target = deps.registrationState.getTarget();
|
|
524
569
|
if (deps.registrationState.isRegistered() && target) {
|
|
525
570
|
setTelegramFollowerSessionHandoff({
|
|
@@ -646,6 +691,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
646
691
|
let threadName;
|
|
647
692
|
let displayTitle;
|
|
648
693
|
let generation;
|
|
694
|
+
let sessionId;
|
|
649
695
|
let leaderProtocol;
|
|
650
696
|
let eligibleElectionSlots = [];
|
|
651
697
|
let recoveryEpoch = 0;
|
|
@@ -671,6 +717,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
671
717
|
return true;
|
|
672
718
|
},
|
|
673
719
|
getGeneration: () => generation,
|
|
720
|
+
getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
|
|
674
721
|
beginRecovery: () => {
|
|
675
722
|
if (activeRecoveryEpoch !== undefined)
|
|
676
723
|
return activeRecoveryEpoch;
|
|
@@ -714,6 +761,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
714
761
|
generation === metadata?.generation &&
|
|
715
762
|
target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
|
|
716
763
|
? displayTitle : undefined;
|
|
764
|
+
const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
|
|
717
765
|
const availabilityChanged = registered !== next;
|
|
718
766
|
registered = next;
|
|
719
767
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
@@ -723,6 +771,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
723
771
|
metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
|
|
724
772
|
? metadata.displayTitle : retainedDisplayTitle;
|
|
725
773
|
generation = next ? metadata?.generation : undefined;
|
|
774
|
+
sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
|
|
726
775
|
leaderProtocol =
|
|
727
776
|
next && metadata?.leaderProtocol
|
|
728
777
|
? {
|
|
@@ -1080,10 +1129,12 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1080
1129
|
if (deps.isContextActive?.(ctx) === false)
|
|
1081
1130
|
return false;
|
|
1082
1131
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
1083
|
-
const
|
|
1132
|
+
const sessionId = deps.getSessionId?.(ctx);
|
|
1133
|
+
const attempt = { ctx, sessionGeneration, sessionId };
|
|
1084
1134
|
registrationAttempt = attempt;
|
|
1085
1135
|
const isCurrentRequest = () => registrationAttempt === attempt &&
|
|
1086
|
-
deps.getSessionGeneration?.() === sessionGeneration && deps.
|
|
1136
|
+
deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
|
|
1137
|
+
deps.isContextActive?.(ctx) !== false;
|
|
1087
1138
|
const abandonOwnedRequest = async () => {
|
|
1088
1139
|
if (registrationAttempt !== attempt)
|
|
1089
1140
|
return;
|
|
@@ -1141,7 +1192,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1141
1192
|
? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
|
|
1142
1193
|
: {}),
|
|
1143
1194
|
cwd: ctx.cwd,
|
|
1144
|
-
sessionId
|
|
1195
|
+
sessionId,
|
|
1145
1196
|
pid: getPid(),
|
|
1146
1197
|
processBirthId: deps.getProcessBirthId?.(),
|
|
1147
1198
|
sessionGeneration,
|
|
@@ -1197,7 +1248,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1197
1248
|
local: deps.protocolIdentity,
|
|
1198
1249
|
remote: response?.kind === "bus.ack" ? response.protocol : undefined,
|
|
1199
1250
|
});
|
|
1200
|
-
|
|
1251
|
+
// An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
|
|
1252
|
+
const unidentifiedRejection = response?.kind === "bus.ack" && !response.ok && response.protocol === undefined;
|
|
1253
|
+
if (!compatibility.compatible && !unidentifiedRejection) {
|
|
1201
1254
|
stopHeartbeat();
|
|
1202
1255
|
activeLeaderSocketPath = undefined;
|
|
1203
1256
|
activeAuthSecret = undefined;
|
|
@@ -1229,6 +1282,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1229
1282
|
deps.registrationState?.setRegistered(true, registrationResult.target, {
|
|
1230
1283
|
...registrationResult,
|
|
1231
1284
|
generation: registrationGeneration,
|
|
1285
|
+
sessionId,
|
|
1232
1286
|
...(response.protocol
|
|
1233
1287
|
? { leaderProtocol: response.protocol }
|
|
1234
1288
|
: {}),
|
|
@@ -1283,7 +1337,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1283
1337
|
},
|
|
1284
1338
|
setContext(ctx) {
|
|
1285
1339
|
if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
|
|
1286
|
-
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()
|
|
1340
|
+
registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
|
|
1341
|
+
registrationAttempt.sessionId !== deps.getSessionId?.(ctx)))
|
|
1287
1342
|
registrationAttempt = undefined;
|
|
1288
1343
|
activeContext = ctx;
|
|
1289
1344
|
},
|
|
@@ -1551,6 +1606,17 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, pr
|
|
|
1551
1606
|
return prepared;
|
|
1552
1607
|
}
|
|
1553
1608
|
export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
|
|
1609
|
+
// The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
|
|
1610
|
+
const recentDeliveries = new Map();
|
|
1611
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1612
|
+
const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
|
|
1613
|
+
const pruneRecentDeliveries = (nowMs) => {
|
|
1614
|
+
for (const [deliveryId, admittedAtMs] of recentDeliveries) {
|
|
1615
|
+
if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs)
|
|
1616
|
+
break;
|
|
1617
|
+
recentDeliveries.delete(deliveryId);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1554
1620
|
return {
|
|
1555
1621
|
async admit(envelope, ctx) {
|
|
1556
1622
|
const delivery = envelope.delivery;
|
|
@@ -1592,12 +1658,18 @@ export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
|
|
|
1592
1658
|
? { message: carrier }
|
|
1593
1659
|
: { edited_message: carrier }),
|
|
1594
1660
|
};
|
|
1595
|
-
|
|
1596
|
-
deps.signalWorker(ctx);
|
|
1597
|
-
return {
|
|
1661
|
+
const result = {
|
|
1598
1662
|
deliveryId: delivery.deliveryId,
|
|
1599
1663
|
sourceUpdateId: delivery.sourceUpdateId,
|
|
1600
1664
|
};
|
|
1665
|
+
pruneRecentDeliveries(getNowMs());
|
|
1666
|
+
if (recentDeliveries.has(delivery.deliveryId))
|
|
1667
|
+
return result;
|
|
1668
|
+
deps.journal.appendBatch([update]);
|
|
1669
|
+
recentDeliveries.set(delivery.deliveryId, getNowMs());
|
|
1670
|
+
pruneRecentDeliveries(getNowMs());
|
|
1671
|
+
deps.signalWorker(ctx);
|
|
1672
|
+
return result;
|
|
1601
1673
|
},
|
|
1602
1674
|
};
|
|
1603
1675
|
}
|
|
@@ -1674,7 +1746,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1674
1746
|
envelope.kind !== "leader.forwardEditedMessage" &&
|
|
1675
1747
|
envelope.kind !== "leader.wakeInputCustody" &&
|
|
1676
1748
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1677
|
-
envelope.kind !== "leader.
|
|
1749
|
+
envelope.kind !== "leader.workspaceRestore" &&
|
|
1678
1750
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
1679
1751
|
envelope.recipientInstanceId !== deps.instanceId) {
|
|
1680
1752
|
return {
|
|
@@ -1694,14 +1766,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1694
1766
|
message: "Stale Telegram bus follower registration generation.",
|
|
1695
1767
|
};
|
|
1696
1768
|
}
|
|
1769
|
+
if (envelope.kind === "leader.workspaceRestore") {
|
|
1770
|
+
const ctx = deps.getContext();
|
|
1771
|
+
if (!authSecret || !ctx || !deps.isWorkspaceRestoreEnabled?.() || !deps.handleWorkspaceRestore)
|
|
1772
|
+
return {
|
|
1773
|
+
kind: "bus.ack", requestId: envelope.requestId, ok: false, message: "Workspace Restore capability is unavailable.",
|
|
1774
|
+
};
|
|
1775
|
+
try {
|
|
1776
|
+
const result = await deps.handleWorkspaceRestore({ operationId: envelope.operationId,
|
|
1777
|
+
registrationGeneration, mode: envelope.mode }, ctx);
|
|
1778
|
+
if (deps.getAuthSecret?.() !== authSecret || deps.getRegistrationGeneration() !== registrationGeneration ||
|
|
1779
|
+
deps.getContext() !== ctx || !deps.isWorkspaceRestoreEnabled())
|
|
1780
|
+
throw new Error("Workspace Restore receiver authority changed.");
|
|
1781
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
1782
|
+
}
|
|
1783
|
+
catch (error) {
|
|
1784
|
+
deps.recordRuntimeEvent?.("bus", error, { phase: "follower-workspace-restore" });
|
|
1785
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
1786
|
+
message: error instanceof Error ? error.message : "Workspace Restore failed." };
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1697
1789
|
if (envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
1698
1790
|
envelope.recipientBindingKey !== deps.getRecipientBindingKey())
|
|
1699
1791
|
return {
|
|
1700
1792
|
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
1701
1793
|
message: "Mismatched Telegram follower handoff recipient identity.",
|
|
1702
1794
|
};
|
|
1703
|
-
if (envelope.kind !== "leader.
|
|
1704
|
-
envelope.kind !== "leader.offerQueueHandoff" &&
|
|
1795
|
+
if (envelope.kind !== "leader.offerQueueHandoff" &&
|
|
1705
1796
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1706
1797
|
(!envelope.delivery ||
|
|
1707
1798
|
envelope.delivery.recipientBindingKey !==
|
|
@@ -1733,8 +1824,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1733
1824
|
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
1734
1825
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
1735
1826
|
}
|
|
1736
|
-
if (envelope.kind !== "leader.
|
|
1737
|
-
envelope.kind !== "leader.offerQueueHandoff") {
|
|
1827
|
+
if (envelope.kind !== "leader.offerQueueHandoff") {
|
|
1738
1828
|
const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
|
|
1739
1829
|
(deps.isSourceReferenceAdmissionEnabled?.() ?? false);
|
|
1740
1830
|
if (sourceReferenceEnabled &&
|
|
@@ -1752,39 +1842,25 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1752
1842
|
result: receipt,
|
|
1753
1843
|
};
|
|
1754
1844
|
}
|
|
1755
|
-
if (
|
|
1756
|
-
|
|
1757
|
-
throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
|
|
1758
|
-
}
|
|
1759
|
-
const result = await deps.handleQueueHandoff(envelope, ctx);
|
|
1760
|
-
const receipt = envelope.payload.admissionReceipts[0];
|
|
1761
|
-
if (envelope.payload.admissionReceipts.length !== 1 ||
|
|
1762
|
-
!receipt ||
|
|
1763
|
-
result.status !== "staged" ||
|
|
1764
|
-
result.receiptId !== receipt.receiptId ||
|
|
1765
|
-
result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
1766
|
-
result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
|
|
1767
|
-
throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
|
|
1768
|
-
}
|
|
1769
|
-
return {
|
|
1770
|
-
kind: "bus.ack",
|
|
1771
|
-
requestId: envelope.requestId,
|
|
1772
|
-
ok: true,
|
|
1773
|
-
result,
|
|
1774
|
-
};
|
|
1845
|
+
if (!deps.handleQueueHandoff) {
|
|
1846
|
+
throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
|
|
1775
1847
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
}, ctx);
|
|
1848
|
+
const result = await deps.handleQueueHandoff(envelope, ctx);
|
|
1849
|
+
const receipt = envelope.payload.admissionReceipts[0];
|
|
1850
|
+
if (envelope.payload.admissionReceipts.length !== 1 ||
|
|
1851
|
+
!receipt ||
|
|
1852
|
+
result.status !== "staged" ||
|
|
1853
|
+
result.receiptId !== receipt.receiptId ||
|
|
1854
|
+
result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
1855
|
+
result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
|
|
1856
|
+
throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
|
|
1786
1857
|
}
|
|
1787
|
-
return {
|
|
1858
|
+
return {
|
|
1859
|
+
kind: "bus.ack",
|
|
1860
|
+
requestId: envelope.requestId,
|
|
1861
|
+
ok: true,
|
|
1862
|
+
result,
|
|
1863
|
+
};
|
|
1788
1864
|
}
|
|
1789
1865
|
catch (error) {
|
|
1790
1866
|
deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
|
|
@@ -1826,6 +1902,3 @@ function parseTarget(value) {
|
|
|
1826
1902
|
target.threadId = value.threadId;
|
|
1827
1903
|
return target;
|
|
1828
1904
|
}
|
|
1829
|
-
function isRecord(value) {
|
|
1830
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1831
|
-
}
|
package/dist/lib/bus-leader.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
10
|
import { type TelegramApiCallOptions } from "./telegram-api.ts";
|
|
11
11
|
import type { TelegramTarget } from "./target.ts";
|
|
12
|
+
import type { TelegramAttachmentSource } from "./media.ts";
|
|
12
13
|
import * as Threads from "./threads.ts";
|
|
13
14
|
import { type TelegramBusEnvelope, type TelegramBusFollowerRegistry, type TelegramBusFollowerView, type TelegramBusInstanceRegistration, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
|
|
14
15
|
import type { TelegramQueueHandoffPayload } from "./queue.ts";
|
|
@@ -101,14 +102,14 @@ export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
|
101
102
|
export interface TelegramBusLeaderApiProxyDeps {
|
|
102
103
|
call: (method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<unknown>;
|
|
103
104
|
callMultipart: (method: string, fields: Record<string, string>, fieldName: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<unknown>;
|
|
104
|
-
downloadFile: (fileId: string,
|
|
105
|
+
downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<unknown>;
|
|
105
106
|
recoverStaleTargetError?: (apiBody: unknown, error: unknown) => Promise<unknown> | unknown;
|
|
106
107
|
}
|
|
107
108
|
export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
108
109
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
109
110
|
persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
110
111
|
onThreadDisplayChanged?: () => void;
|
|
111
|
-
runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "provisionFollowerTarget" | "provisionLeaderTarget" | "recordRuntimeEvent">;
|
|
112
|
+
runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "getAllowedUserId" | "provisionFollowerTarget" | "commitFollowerRegistration" | "provisionLeaderTarget" | "recordRuntimeEvent">;
|
|
112
113
|
getAllowedUserId: () => number | undefined;
|
|
113
114
|
instanceId: string;
|
|
114
115
|
getCwd?: (ctx: TContext) => string | undefined;
|
|
@@ -168,6 +169,11 @@ export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
|
168
169
|
target?: TelegramTarget;
|
|
169
170
|
}
|
|
170
171
|
export type TelegramBusFollowerMessageOwnershipRecorder = (record: TelegramBusFollowerMessageOwnershipRecord) => void;
|
|
172
|
+
export type TelegramBusFollowerRegistrationCommitter = (input: {
|
|
173
|
+
registration: TelegramBusInstanceRegistration;
|
|
174
|
+
target?: TelegramTarget;
|
|
175
|
+
slot?: string;
|
|
176
|
+
}, publish: () => void) => Promise<void> | void;
|
|
171
177
|
export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
172
178
|
socketPath: TelegramBusSocketPathSource;
|
|
173
179
|
commitEndpointPublication?: (commit: () => void) => boolean;
|
|
@@ -195,6 +201,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
195
201
|
provisionFollowerTarget?: (registration: TelegramBusInstanceRegistration, options?: {
|
|
196
202
|
existingWorkspaceBindingOnly?: boolean;
|
|
197
203
|
}) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
204
|
+
commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
|
|
198
205
|
renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
|
|
199
206
|
threadName: string;
|
|
200
207
|
}> | {
|
|
@@ -209,16 +216,22 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
209
216
|
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
210
217
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
211
218
|
onFollowerRegistered?: () => void;
|
|
219
|
+
/** Observation only: reacquire admission and inspect work, never infer completion.
|
|
220
|
+
* Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
|
|
221
|
+
onWorkspaceRestoreRecipientObserved?: (follower: TelegramBusFollowerView, isCurrent: () => boolean) => Promise<void> | void;
|
|
212
222
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
213
223
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
214
224
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
215
225
|
getTelegramProfile?: () => string | undefined;
|
|
226
|
+
getAllowedUserId?: () => number | undefined;
|
|
216
227
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
217
228
|
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
218
229
|
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|
|
219
230
|
getNowMs?: () => number;
|
|
220
231
|
timeoutMs?: number;
|
|
221
232
|
followerPruneIntervalMs?: number;
|
|
233
|
+
/** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
|
|
234
|
+
afterFollowerPrune?: () => void;
|
|
222
235
|
followerStaleAfterMs?: number;
|
|
223
236
|
isFollowerProcessAlive?: (pid: number) => boolean;
|
|
224
237
|
shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
|
|
@@ -281,6 +294,7 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
281
294
|
slot?: string;
|
|
282
295
|
threadName?: string;
|
|
283
296
|
}) | undefined;
|
|
297
|
+
commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
|
|
284
298
|
onFollowerDisconnected?: (follower: TelegramBusFollowerView) => Promise<void> | void;
|
|
285
299
|
renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
|
|
286
300
|
threadName: string;
|
|
@@ -296,9 +310,14 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
296
310
|
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
297
311
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
298
312
|
onFollowerRegistered?: () => void;
|
|
313
|
+
onWorkspaceRestoreRecipientObserved?: TelegramBusLeaderRuntimeDeps<unknown>["onWorkspaceRestoreRecipientObserved"];
|
|
314
|
+
getWorkspaceRestoreObservationGeneration?: () => number | undefined;
|
|
315
|
+
recordRuntimeEvent?: TelegramBusLeaderRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
299
316
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
300
317
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
301
318
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
319
|
+
getTelegramProfile?: () => string | undefined;
|
|
320
|
+
getAllowedUserId?: () => number | undefined;
|
|
302
321
|
runFollowerMutation?: TelegramBusFollowerMutationRunner;
|
|
303
322
|
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
304
323
|
runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
|