@llblab/pi-kit 0.6.0 → 0.7.1
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/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/README.md +13 -9
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as Sync from "./sync.ts";
|
|
9
|
+
import {
|
|
10
|
+
createTelegramThreadDisplayReconciler,
|
|
11
|
+
resolveTelegramInitialWorkspaceDisplayName,
|
|
12
|
+
} from "./thread-display.ts";
|
|
13
|
+
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
9
14
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
15
|
import {
|
|
11
16
|
getTelegramApiErrorRequestTarget,
|
|
@@ -32,11 +37,37 @@ import {
|
|
|
32
37
|
type TelegramBusSocketPathSource,
|
|
33
38
|
TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
34
39
|
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
40
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
41
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
42
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
35
43
|
} from "./bus.ts";
|
|
36
44
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
37
45
|
import type { TelegramQueueHandoffPayload } from "./queue.ts";
|
|
46
|
+
import {
|
|
47
|
+
createTelegramWorkspaceOperationRuntime,
|
|
48
|
+
type TelegramWorkspaceOperationRunner,
|
|
49
|
+
} from "./workspace-retirement.ts";
|
|
50
|
+
import {
|
|
51
|
+
createTelegramWorkspaceAdmissionOperationId,
|
|
52
|
+
runWithTelegramWorkspaceAdmissionsAsync,
|
|
53
|
+
type TelegramWorkspaceAdmissionLedger,
|
|
54
|
+
} from "./workspace-admission.ts";
|
|
55
|
+
|
|
56
|
+
export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
|
|
57
|
+
|
|
58
|
+
export type TelegramBusWorkspaceAdmissionRunner =
|
|
59
|
+
TelegramWorkspaceOperationRunner;
|
|
38
60
|
|
|
39
61
|
export interface TelegramBusLeaderRuntime<TContext> {
|
|
62
|
+
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
63
|
+
captureWorkspaceExternalProtection?: (
|
|
64
|
+
binding: Threads.TelegramWorkspaceThreadBinding,
|
|
65
|
+
) => Threads.TelegramWorkspaceExternalProtectionEvidence;
|
|
66
|
+
reconcileThreadDisplay?: () => Promise<{ changed: number }>;
|
|
67
|
+
setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
|
|
68
|
+
renameLeaderThread?: (
|
|
69
|
+
threadName: string,
|
|
70
|
+
) => Promise<Threads.TelegramTopicTargetRecord>;
|
|
40
71
|
startPolling: (ctx: TContext) => Promise<void>;
|
|
41
72
|
stopPolling: () => Promise<void>;
|
|
42
73
|
routeQueueHandoff: (input: {
|
|
@@ -84,10 +115,15 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
|
|
|
84
115
|
getCwd?: (ctx: TContext) => string | undefined;
|
|
85
116
|
getTelegramProfile?: () => string | undefined;
|
|
86
117
|
shouldForceFreshUnnamed?: () => boolean;
|
|
118
|
+
getRequestedThreadName?: () => string | undefined;
|
|
119
|
+
resolveInitialWorkspaceDisplayTitle?: (
|
|
120
|
+
binding: Threads.TelegramWorkspaceDisplayBinding,
|
|
121
|
+
) => string | undefined;
|
|
87
122
|
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
88
123
|
callApi: <TResponse>(
|
|
89
124
|
method: string,
|
|
90
125
|
body: Record<string, unknown>,
|
|
126
|
+
options?: TelegramApiCallOptions,
|
|
91
127
|
) => Promise<TResponse>;
|
|
92
128
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
93
129
|
getThreadReconciliationMachineState?: () =>
|
|
@@ -124,6 +160,10 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
124
160
|
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
125
161
|
onProvisioningStart?: () => void;
|
|
126
162
|
onProvisioningEnd?: () => void;
|
|
163
|
+
resolveInitialWorkspaceDisplayTitle?: (
|
|
164
|
+
binding: Threads.TelegramWorkspaceDisplayBinding,
|
|
165
|
+
) => string | undefined;
|
|
166
|
+
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
127
167
|
getNowMs?: () => number;
|
|
128
168
|
recordRuntimeEvent: (
|
|
129
169
|
category: string,
|
|
@@ -137,6 +177,7 @@ export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
|
137
177
|
Threads.TelegramTopicTargetStore,
|
|
138
178
|
| "list"
|
|
139
179
|
| "markStaleByTarget"
|
|
180
|
+
| "markWorkspaceBindingInactiveByTarget"
|
|
140
181
|
| "persist"
|
|
141
182
|
| "upsertPendingCleanup"
|
|
142
183
|
| "removePendingCleanup"
|
|
@@ -178,6 +219,9 @@ export interface TelegramBusLeaderApiProxyDeps {
|
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
222
|
+
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
223
|
+
persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
224
|
+
onThreadDisplayChanged?: () => void;
|
|
181
225
|
runtime: Omit<
|
|
182
226
|
TelegramBusLeaderRuntimeDeps<TContext>,
|
|
183
227
|
| "callApi"
|
|
@@ -192,6 +236,7 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
192
236
|
getCwd?: (ctx: TContext) => string | undefined;
|
|
193
237
|
getTelegramProfile?: () => string | undefined;
|
|
194
238
|
shouldForceFreshUnnamed?: () => boolean;
|
|
239
|
+
getRequestedThreadName?: () => string | undefined;
|
|
195
240
|
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
196
241
|
callApi: TelegramBusLeaderTargetProvisionerDeps<TContext>["callApi"];
|
|
197
242
|
callMultipart: TelegramBusLeaderApiProxyDeps["callMultipart"];
|
|
@@ -208,11 +253,60 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
208
253
|
recordRuntimeEvent: NonNullable<
|
|
209
254
|
TelegramBusLeaderRuntimeDeps<TContext>["recordRuntimeEvent"]
|
|
210
255
|
>;
|
|
256
|
+
captureWorkspaceExternalProtection?: (
|
|
257
|
+
binding: Threads.TelegramWorkspaceThreadBinding,
|
|
258
|
+
) => Threads.TelegramWorkspaceExternalProtectionEvidence;
|
|
259
|
+
getWorkspaceAdmission?: () => Pick<
|
|
260
|
+
TelegramWorkspaceAdmissionLedger,
|
|
261
|
+
"acquireAdmission" | "releaseAdmission"
|
|
262
|
+
> | undefined;
|
|
263
|
+
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
211
264
|
}
|
|
212
265
|
|
|
213
266
|
export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
214
267
|
deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>,
|
|
215
|
-
): TelegramBusLeaderRuntime<TContext> {
|
|
268
|
+
): TelegramBusLeaderRuntime<TContext> & {
|
|
269
|
+
renameLeaderThreadAdmitted: (
|
|
270
|
+
threadName: string,
|
|
271
|
+
expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
|
|
272
|
+
) => Promise<Threads.TelegramTopicTargetRecord>;
|
|
273
|
+
resetLeaderThreadName: (
|
|
274
|
+
expectedTarget: Threads.TelegramTopicTargetRecord["target"],
|
|
275
|
+
) => Promise<{ threadName: string }>;
|
|
276
|
+
resetThreadNameAdmitted: (
|
|
277
|
+
target: Threads.TelegramTopicTargetRecord["target"],
|
|
278
|
+
) => Promise<{ threadName: string }>;
|
|
279
|
+
} {
|
|
280
|
+
const runWorkspaceAdmission: TelegramBusWorkspaceAdmissionRunner | undefined =
|
|
281
|
+
deps.getWorkspaceAdmission
|
|
282
|
+
? (input, operation) => {
|
|
283
|
+
const admission = deps.getWorkspaceAdmission?.();
|
|
284
|
+
if (!admission) {
|
|
285
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
286
|
+
}
|
|
287
|
+
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
288
|
+
ledger: admission,
|
|
289
|
+
...input,
|
|
290
|
+
operation,
|
|
291
|
+
onReleaseError(error) {
|
|
292
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
293
|
+
phase: "workspace-admission-release",
|
|
294
|
+
operationKind: input.operationKind,
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
: undefined;
|
|
300
|
+
const runWorkspaceOperation = deps.runWorkspaceOperation ??
|
|
301
|
+
createTelegramWorkspaceOperationRuntime({
|
|
302
|
+
getWorkspaceAdmission: deps.getWorkspaceAdmission,
|
|
303
|
+
onReleaseError(error, operationKind) {
|
|
304
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
305
|
+
phase: "workspace-admission-release",
|
|
306
|
+
operationKind,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
}).run;
|
|
216
310
|
const provisionerPorts = {
|
|
217
311
|
getAllowedUserId: deps.getAllowedUserId,
|
|
218
312
|
topicTargetStore: deps.topicTargetStore,
|
|
@@ -222,31 +316,397 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
222
316
|
setSyncState: deps.setSyncState,
|
|
223
317
|
onProvisioningStart: deps.onProvisioningStart,
|
|
224
318
|
onProvisioningEnd: deps.onProvisioningEnd,
|
|
319
|
+
...(deps.getThreadDisplayMode
|
|
320
|
+
? {
|
|
321
|
+
resolveInitialWorkspaceDisplayTitle(
|
|
322
|
+
binding: Threads.TelegramWorkspaceDisplayBinding,
|
|
323
|
+
) {
|
|
324
|
+
return resolveTelegramInitialWorkspaceDisplayName({
|
|
325
|
+
bindings: deps.topicTargetStore.listWorkspaceBindings(),
|
|
326
|
+
binding,
|
|
327
|
+
mode: deps.getThreadDisplayMode!(),
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
runWorkspaceOperation,
|
|
225
333
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
226
334
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
335
|
+
const display = deps.getThreadDisplayMode ? createTelegramThreadDisplayReconciler({
|
|
336
|
+
store: deps.topicTargetStore,
|
|
337
|
+
getMode: deps.getThreadDisplayMode,
|
|
338
|
+
getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
|
|
339
|
+
getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
|
|
340
|
+
captureBindingAuthority(binding) {
|
|
341
|
+
const matches = (target: TelegramTarget | undefined) =>
|
|
342
|
+
target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
|
|
343
|
+
const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
|
|
344
|
+
if (leader && matches(leader.target)) {
|
|
345
|
+
return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target);
|
|
346
|
+
}
|
|
347
|
+
const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
|
|
348
|
+
if (!follower?.registrationGeneration) return undefined;
|
|
349
|
+
const generation = follower.registrationGeneration;
|
|
350
|
+
return () => {
|
|
351
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
352
|
+
return current?.registrationGeneration === generation && matches(current.target);
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
callApi: deps.callApi,
|
|
356
|
+
}) : undefined;
|
|
357
|
+
const reconcileThreadDisplayOperation = async () => {
|
|
358
|
+
const result = await display!.reconcile();
|
|
359
|
+
deps.onThreadDisplayChanged?.();
|
|
360
|
+
return result;
|
|
361
|
+
};
|
|
362
|
+
const reconcileThreadDisplay = () => runWorkspaceOperation(
|
|
363
|
+
{
|
|
364
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
365
|
+
operationKind: "workspace.reconcile-display",
|
|
366
|
+
scopes: [{ kind: "profile" }],
|
|
367
|
+
},
|
|
368
|
+
reconcileThreadDisplayOperation,
|
|
369
|
+
);
|
|
370
|
+
const scheduleDisplay = () => {
|
|
371
|
+
if (!display) return;
|
|
372
|
+
void reconcileThreadDisplay().catch((error) => {
|
|
373
|
+
deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
let modeTail: Promise<void> = Promise.resolve();
|
|
377
|
+
const applyThreadDisplayMode = (mode: TelegramThreadDisplayMode, isCurrent: () => boolean): Promise<void> => {
|
|
378
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
379
|
+
const profile = deps.getTelegramProfile?.();
|
|
380
|
+
const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
381
|
+
deps.getTelegramProfile?.() === profile && isCurrent();
|
|
382
|
+
const run = modeTail.then(() => runWorkspaceOperation(
|
|
383
|
+
{
|
|
384
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
385
|
+
operationKind: "workspace.set-display-mode",
|
|
386
|
+
scopes: [{ kind: "profile" }],
|
|
387
|
+
},
|
|
388
|
+
async () => {
|
|
389
|
+
if (!display || !deps.persistThreadDisplayMode || !current()) {
|
|
390
|
+
throw new Error("Telegram Thread display setting requires current leader authority.");
|
|
391
|
+
}
|
|
392
|
+
const assertDisplayPeers = () => {
|
|
393
|
+
if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) =>
|
|
394
|
+
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE),
|
|
395
|
+
)) throw new Error("Update or restart all connected followers before changing Thread display mode.");
|
|
396
|
+
};
|
|
397
|
+
assertDisplayPeers();
|
|
398
|
+
await deps.persistThreadDisplayMode(mode, current);
|
|
399
|
+
assertDisplayPeers();
|
|
400
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
401
|
+
throw new Error("Telegram Thread display preference changed before application.");
|
|
402
|
+
}
|
|
403
|
+
await reconcileThreadDisplayOperation();
|
|
404
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
405
|
+
throw new Error("Telegram Thread display application lost its originating authority.");
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
));
|
|
409
|
+
modeTail = run.catch(() => undefined);
|
|
410
|
+
return run;
|
|
411
|
+
};
|
|
412
|
+
const provisionLeaderTarget = createTelegramBusLeaderTargetProvisioner({
|
|
413
|
+
...provisionerPorts,
|
|
414
|
+
instanceId: deps.instanceId,
|
|
415
|
+
getCwd: deps.getCwd,
|
|
416
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
417
|
+
shouldForceFreshUnnamed: deps.shouldForceFreshUnnamed,
|
|
418
|
+
getRequestedThreadName: deps.getRequestedThreadName,
|
|
419
|
+
getThreadReconciliationMachineState:
|
|
420
|
+
deps.getThreadReconciliationMachineState,
|
|
421
|
+
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
422
|
+
setLeaderTarget: deps.setLeaderTarget,
|
|
423
|
+
});
|
|
424
|
+
const disconnectFollower = createTelegramBusFollowerDisconnectHandler({
|
|
425
|
+
...provisionerPorts,
|
|
426
|
+
});
|
|
427
|
+
const cleanupConfirmedDeadFollower = createTelegramBusFollowerConfirmedDeadHandler({
|
|
428
|
+
...provisionerPorts,
|
|
429
|
+
});
|
|
430
|
+
const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
|
|
431
|
+
...provisionerPorts,
|
|
432
|
+
});
|
|
433
|
+
const renameLeaderThreadAdmitted = async (
|
|
434
|
+
threadName: string,
|
|
435
|
+
expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
|
|
436
|
+
) => {
|
|
437
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
438
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
"Telegram Workspace Thread rename requires leader ownership.",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
444
|
+
records: deps.topicTargetStore.list(),
|
|
231
445
|
instanceId: deps.instanceId,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
446
|
+
});
|
|
447
|
+
if (typeof record?.target.threadId !== "number") {
|
|
448
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
449
|
+
}
|
|
450
|
+
if (expectedTarget && (record.target.chatId !== expectedTarget.chatId ||
|
|
451
|
+
record.target.threadId !== expectedTarget.threadId)) {
|
|
452
|
+
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
453
|
+
}
|
|
454
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
455
|
+
store: deps.topicTargetStore,
|
|
456
|
+
callApi: deps.callApi,
|
|
457
|
+
assertAuthority() {
|
|
458
|
+
if (
|
|
459
|
+
deps.getCurrentLeaderEpoch &&
|
|
460
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
461
|
+
) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
"Telegram Workspace Thread rename lost leader ownership.",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
467
|
+
records: deps.topicTargetStore.list(),
|
|
468
|
+
instanceId: deps.instanceId,
|
|
469
|
+
});
|
|
470
|
+
if (current?.target.chatId !== record.target.chatId ||
|
|
471
|
+
current.target.threadId !== record.target.threadId) {
|
|
472
|
+
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
const renamed = await rename({
|
|
477
|
+
target: {
|
|
478
|
+
chatId: record.target.chatId,
|
|
479
|
+
threadId: record.target.threadId,
|
|
480
|
+
},
|
|
481
|
+
threadName,
|
|
482
|
+
slot: record.slot,
|
|
483
|
+
});
|
|
484
|
+
if (!renamed) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
Threads.getTelegramTopicThreadNameValidationError(
|
|
487
|
+
threadName,
|
|
488
|
+
record.slot,
|
|
489
|
+
) ?? "Telegram Workspace Thread name is already reserved.",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
await deps.topicTargetStore.persist();
|
|
493
|
+
return renamed;
|
|
494
|
+
};
|
|
495
|
+
const resetThreadNameAdmitted = async (
|
|
496
|
+
target: Threads.TelegramTopicTargetRecord["target"],
|
|
497
|
+
assertTargetCurrent: () => void = () => undefined,
|
|
498
|
+
): Promise<{ threadName: string }> => {
|
|
499
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
500
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
501
|
+
throw new Error("Telegram Workspace Thread reset requires leader ownership.");
|
|
502
|
+
}
|
|
503
|
+
const binding = deps.topicTargetStore.listWorkspaceBindings().find(
|
|
504
|
+
(candidate) => candidate.target.chatId === target.chatId &&
|
|
505
|
+
candidate.target.threadId === target.threadId,
|
|
506
|
+
);
|
|
507
|
+
if (!binding) throw new Error("No Workspace Thread binding is available to reset.");
|
|
508
|
+
const automaticTitle = resolveTelegramInitialWorkspaceDisplayName({
|
|
509
|
+
bindings: deps.topicTargetStore.listWorkspaceBindings(),
|
|
510
|
+
binding: { ...binding, manualThreadName: undefined },
|
|
511
|
+
mode: deps.getThreadDisplayMode?.() ?? "letters",
|
|
512
|
+
preserveRetainedManualName: false,
|
|
513
|
+
});
|
|
514
|
+
if (!automaticTitle) {
|
|
515
|
+
throw new Error("Telegram Workspace automatic title is unavailable.");
|
|
516
|
+
}
|
|
517
|
+
assertTargetCurrent();
|
|
518
|
+
await deps.callApi("editForumTopic", {
|
|
519
|
+
chat_id: target.chatId,
|
|
520
|
+
message_thread_id: target.threadId,
|
|
521
|
+
name: automaticTitle,
|
|
522
|
+
});
|
|
523
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
524
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
525
|
+
throw new Error("Telegram Workspace Thread reset lost leader ownership.");
|
|
526
|
+
}
|
|
527
|
+
assertTargetCurrent();
|
|
528
|
+
const reset = deps.topicTargetStore.clearManualNameByTarget(
|
|
529
|
+
target,
|
|
530
|
+
automaticTitle,
|
|
531
|
+
);
|
|
532
|
+
if (!reset) throw new Error("Telegram Workspace Thread reset changed binding.");
|
|
533
|
+
await deps.topicTargetStore.persist();
|
|
534
|
+
return { threadName: automaticTitle };
|
|
535
|
+
};
|
|
536
|
+
const renameLeaderThread = (threadName: string) => runWorkspaceOperation({
|
|
537
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
538
|
+
operationKind: "workspace.rename-leader",
|
|
539
|
+
scopes: [{ kind: "profile" }],
|
|
540
|
+
}, () => renameLeaderThreadAdmitted(threadName));
|
|
541
|
+
const resetLeaderThreadName = (
|
|
542
|
+
expectedTarget: Threads.TelegramTopicTargetRecord["target"],
|
|
543
|
+
) => runWorkspaceOperation({
|
|
544
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
545
|
+
operationKind: "workspace.reset-leader-name",
|
|
546
|
+
scopes: [{ kind: "profile" }],
|
|
547
|
+
}, () => {
|
|
548
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
549
|
+
records: deps.topicTargetStore.list(),
|
|
550
|
+
instanceId: deps.instanceId,
|
|
551
|
+
});
|
|
552
|
+
if (!record || typeof record.target.threadId !== "number") {
|
|
553
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
554
|
+
}
|
|
555
|
+
if (record.target.chatId !== expectedTarget.chatId ||
|
|
556
|
+
record.target.threadId !== expectedTarget.threadId) {
|
|
557
|
+
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
558
|
+
}
|
|
559
|
+
return resetThreadNameAdmitted(record.target, () => {
|
|
560
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
561
|
+
records: deps.topicTargetStore.list(),
|
|
562
|
+
instanceId: deps.instanceId,
|
|
563
|
+
});
|
|
564
|
+
if (current?.target.chatId !== expectedTarget.chatId ||
|
|
565
|
+
current.target.threadId !== expectedTarget.threadId) {
|
|
566
|
+
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
const runtime = createTelegramBusLeaderRuntime({
|
|
571
|
+
...deps.runtime,
|
|
572
|
+
applyThreadDisplayMode,
|
|
573
|
+
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
574
|
+
onFollowerRegistered: scheduleDisplay,
|
|
575
|
+
provisionLeaderTarget: (ctx) => {
|
|
576
|
+
const chatId = deps.getAllowedUserId();
|
|
577
|
+
return runWorkspaceOperation(
|
|
578
|
+
{
|
|
579
|
+
operationId: `leader-provision:${deps.instanceId}`,
|
|
580
|
+
operationKind: "workspace.provision-leader",
|
|
581
|
+
scopes: [
|
|
582
|
+
typeof chatId === "number"
|
|
583
|
+
? { kind: "chat", chatId }
|
|
584
|
+
: { kind: "profile" },
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
() => provisionLeaderTarget(ctx),
|
|
588
|
+
);
|
|
589
|
+
},
|
|
590
|
+
getFollowerDisplayTitle(follower) {
|
|
591
|
+
const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) =>
|
|
592
|
+
binding.target.chatId === follower.target?.chatId &&
|
|
593
|
+
binding.target.threadId === follower.target?.threadId,
|
|
594
|
+
);
|
|
595
|
+
return binding?.displayTitle ?? binding?.threadName;
|
|
596
|
+
},
|
|
597
|
+
onFollowerDisconnected: (follower) => runWorkspaceOperation(
|
|
598
|
+
{
|
|
599
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
600
|
+
operationKind: "workspace.disconnect-follower",
|
|
601
|
+
scopes: [{ kind: "profile" }],
|
|
602
|
+
},
|
|
603
|
+
() => disconnectFollower(follower),
|
|
604
|
+
),
|
|
605
|
+
async renameFollowerThread(follower, threadName) {
|
|
606
|
+
return runWorkspaceOperation({
|
|
607
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
608
|
+
operationKind: "workspace.rename-follower",
|
|
609
|
+
scopes: [{ kind: "profile" }],
|
|
610
|
+
}, async () => {
|
|
611
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
612
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
"Telegram Workspace Thread rename requires leader ownership.",
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (typeof follower.target?.threadId !== "number") {
|
|
618
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
619
|
+
}
|
|
620
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
621
|
+
store: deps.topicTargetStore,
|
|
622
|
+
callApi: deps.callApi,
|
|
623
|
+
assertAuthority() {
|
|
624
|
+
if (
|
|
625
|
+
deps.getCurrentLeaderEpoch &&
|
|
626
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
627
|
+
) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"Telegram Workspace Thread rename lost leader ownership.",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
633
|
+
if (current?.registrationGeneration !== follower.registrationGeneration ||
|
|
634
|
+
current?.target?.chatId !== follower.target?.chatId ||
|
|
635
|
+
current?.target?.threadId !== follower.target?.threadId) {
|
|
636
|
+
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
const renamed = await rename({
|
|
641
|
+
target: {
|
|
642
|
+
chatId: follower.target.chatId,
|
|
643
|
+
threadId: follower.target.threadId,
|
|
644
|
+
},
|
|
645
|
+
threadName,
|
|
646
|
+
slot: follower.slot,
|
|
647
|
+
});
|
|
648
|
+
if (
|
|
649
|
+
deps.getCurrentLeaderEpoch &&
|
|
650
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
651
|
+
) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
"Telegram Workspace Thread rename lost leader ownership.",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (!renamed) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
Threads.getTelegramTopicThreadNameValidationError(
|
|
659
|
+
threadName,
|
|
660
|
+
follower.slot,
|
|
661
|
+
) ?? "Telegram Workspace Thread name is already reserved.",
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
await deps.topicTargetStore.persist();
|
|
665
|
+
return { threadName: renamed.manualThreadName ?? threadName };
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
resetFollowerThreadName: (follower) => runWorkspaceOperation(
|
|
669
|
+
{
|
|
670
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
671
|
+
operationKind: "workspace.reset-follower-name",
|
|
672
|
+
scopes: [{ kind: "profile" }],
|
|
673
|
+
},
|
|
674
|
+
() => {
|
|
675
|
+
if (!follower.target || typeof follower.target.threadId !== "number") {
|
|
676
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
677
|
+
}
|
|
678
|
+
const target = {
|
|
679
|
+
chatId: follower.target.chatId,
|
|
680
|
+
threadId: follower.target.threadId,
|
|
681
|
+
};
|
|
682
|
+
return resetThreadNameAdmitted(target, () => {
|
|
683
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
684
|
+
if (current?.registrationGeneration !== follower.registrationGeneration ||
|
|
685
|
+
current?.target?.chatId !== target.chatId ||
|
|
686
|
+
current?.target?.threadId !== target.threadId) {
|
|
687
|
+
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
},
|
|
691
|
+
),
|
|
692
|
+
onFollowerConfirmedDead: (follower) => runWorkspaceOperation(
|
|
693
|
+
{
|
|
694
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
695
|
+
operationKind: "workspace.cleanup-dead-follower",
|
|
696
|
+
scopes: [{ kind: "profile" }],
|
|
697
|
+
},
|
|
698
|
+
() => cleanupConfirmedDeadFollower(follower),
|
|
699
|
+
),
|
|
700
|
+
provisionFollowerTarget: (registration, options) => runWorkspaceOperation(
|
|
701
|
+
{
|
|
702
|
+
operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
|
|
703
|
+
operationKind: "workspace.provision-follower",
|
|
704
|
+
scopes: [{ kind: "profile" }],
|
|
705
|
+
},
|
|
706
|
+
() => provisionFollowerTarget(registration, options),
|
|
707
|
+
),
|
|
249
708
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
709
|
+
runWorkspaceAdmission,
|
|
250
710
|
callApi: createTelegramBusLeaderApiProxy({
|
|
251
711
|
call: deps.callApi,
|
|
252
712
|
callMultipart: deps.callMultipart,
|
|
@@ -255,6 +715,25 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
255
715
|
}),
|
|
256
716
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
257
717
|
});
|
|
718
|
+
const assembled = {
|
|
719
|
+
...runtime,
|
|
720
|
+
runWorkspaceOperation,
|
|
721
|
+
renameLeaderThread,
|
|
722
|
+
renameLeaderThreadAdmitted,
|
|
723
|
+
resetLeaderThreadName,
|
|
724
|
+
resetThreadNameAdmitted,
|
|
725
|
+
captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
726
|
+
async startPolling(ctx: TContext) {
|
|
727
|
+
await runtime.startPolling(ctx);
|
|
728
|
+
if (display) scheduleDisplay();
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
if (!display) return assembled;
|
|
732
|
+
return {
|
|
733
|
+
...assembled,
|
|
734
|
+
reconcileThreadDisplay,
|
|
735
|
+
setThreadDisplayMode: (mode) => applyThreadDisplayMode(mode, () => true),
|
|
736
|
+
};
|
|
258
737
|
}
|
|
259
738
|
|
|
260
739
|
export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
@@ -306,9 +785,22 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
306
785
|
) => Promise<unknown> | unknown;
|
|
307
786
|
provisionFollowerTarget?: (
|
|
308
787
|
registration: TelegramBusInstanceRegistration,
|
|
788
|
+
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
309
789
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
790
|
+
renameFollowerThread?: (
|
|
791
|
+
follower: TelegramBusFollowerView,
|
|
792
|
+
threadName: string,
|
|
793
|
+
) => Promise<{ threadName: string }> | { threadName: string };
|
|
794
|
+
resetFollowerThreadName?: (
|
|
795
|
+
follower: TelegramBusFollowerView,
|
|
796
|
+
) => Promise<{ threadName: string }> | { threadName: string };
|
|
797
|
+
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
798
|
+
onFollowerRegistered?: () => void;
|
|
799
|
+
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
800
|
+
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
310
801
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
311
802
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
803
|
+
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
312
804
|
getNowMs?: () => number;
|
|
313
805
|
timeoutMs?: number;
|
|
314
806
|
followerPruneIntervalMs?: number;
|
|
@@ -388,6 +880,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
388
880
|
deps: TelegramBusFollowerTargetProvisionerDeps,
|
|
389
881
|
): (
|
|
390
882
|
registration: TelegramBusInstanceRegistration,
|
|
883
|
+
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
391
884
|
) => Promise<
|
|
392
885
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
393
886
|
> {
|
|
@@ -398,11 +891,42 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
398
891
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
399
892
|
>
|
|
400
893
|
>();
|
|
401
|
-
return async (registration) => {
|
|
894
|
+
return async (registration, options) => {
|
|
895
|
+
if (options?.existingWorkspaceBindingOnly && !registration.cwd) {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
402
898
|
const registrationStartedAtMs = Date.now();
|
|
403
899
|
const chatId = deps.getAllowedUserId();
|
|
404
900
|
if (typeof chatId !== "number") return registration.target;
|
|
405
901
|
await deps.topicTargetStore.load();
|
|
902
|
+
let capacityUnavailable = false;
|
|
903
|
+
const workspaceIdentity = registration.cwd
|
|
904
|
+
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
905
|
+
registration.cwd,
|
|
906
|
+
registration.instanceId,
|
|
907
|
+
registration.previousInstanceId,
|
|
908
|
+
{
|
|
909
|
+
existingBindingOnly:
|
|
910
|
+
options?.existingWorkspaceBindingOnly === true,
|
|
911
|
+
onCapacityUnavailable() {
|
|
912
|
+
capacityUnavailable = true;
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
)
|
|
916
|
+
: undefined;
|
|
917
|
+
if (registration.cwd && !workspaceIdentity) {
|
|
918
|
+
if (options?.existingWorkspaceBindingOnly) return undefined;
|
|
919
|
+
if (capacityUnavailable) {
|
|
920
|
+
throw new Error("Telegram Workspace slot reservation is unavailable.");
|
|
921
|
+
}
|
|
922
|
+
throw new Error("Telegram Workspace identity is already claimed.");
|
|
923
|
+
}
|
|
924
|
+
const workspaceBinding = workspaceIdentity
|
|
925
|
+
? deps.topicTargetStore.getWorkspaceBinding(
|
|
926
|
+
workspaceIdentity.cwd,
|
|
927
|
+
workspaceIdentity.instanceSlot,
|
|
928
|
+
)
|
|
929
|
+
: undefined;
|
|
406
930
|
const provision = Threads.createTelegramTopicTargetProvisioner({
|
|
407
931
|
topicChatId: chatId,
|
|
408
932
|
store: deps.topicTargetStore,
|
|
@@ -414,11 +938,13 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
414
938
|
// silently claim old offline/failed tabs: they may be closed/deleted in
|
|
415
939
|
// Telegram and therefore invisible to the operator.
|
|
416
940
|
claimPendingTargets: false,
|
|
941
|
+
resolveInitialWorkspaceDisplayTitle:
|
|
942
|
+
deps.resolveInitialWorkspaceDisplayTitle,
|
|
417
943
|
});
|
|
418
944
|
const recordsBeforeProvision = deps.topicTargetStore.list();
|
|
419
945
|
const followerProfileKey =
|
|
420
946
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
421
|
-
const requestedTarget = registration.target;
|
|
947
|
+
const requestedTarget = registration.target ?? workspaceBinding?.target;
|
|
422
948
|
const reconnectRecord = recordsBeforeProvision.find((record) => {
|
|
423
949
|
const matchesRequestedTarget =
|
|
424
950
|
!requestedTarget ||
|
|
@@ -432,10 +958,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
432
958
|
!!registration.previousInstanceId &&
|
|
433
959
|
record.instanceId === registration.previousInstanceId &&
|
|
434
960
|
matchesRequestedTarget;
|
|
961
|
+
const matchesWorkspaceRecovery =
|
|
962
|
+
!!workspaceBinding &&
|
|
963
|
+
record.status === "probe-required" &&
|
|
964
|
+
matchesRequestedTarget;
|
|
435
965
|
return (
|
|
436
966
|
record.owner?.kind === "manual-follower" &&
|
|
437
967
|
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
438
|
-
matchesSessionHandoff)
|
|
968
|
+
matchesSessionHandoff || matchesWorkspaceRecovery)
|
|
439
969
|
);
|
|
440
970
|
});
|
|
441
971
|
const followerOwner =
|
|
@@ -456,7 +986,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
456
986
|
recoverableTarget,
|
|
457
987
|
)
|
|
458
988
|
: undefined;
|
|
459
|
-
const registrationKey =
|
|
989
|
+
const registrationKey =
|
|
990
|
+
workspaceIdentity?.bindingKey ||
|
|
991
|
+
followerProfileKey ||
|
|
992
|
+
registration.instanceId;
|
|
460
993
|
const pendingRegistration = pendingRegistrations.get(registrationKey);
|
|
461
994
|
if (pendingRegistration) return pendingRegistration;
|
|
462
995
|
const provisionTarget = async () => {
|
|
@@ -472,7 +1005,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
472
1005
|
instanceId: registration.instanceId,
|
|
473
1006
|
},
|
|
474
1007
|
profileKey: followerProfileKey,
|
|
475
|
-
threadName: registration.threadName,
|
|
1008
|
+
threadName: workspaceBinding?.threadName ?? registration.threadName,
|
|
1009
|
+
preferredSlot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
|
|
1010
|
+
...(workspaceIdentity
|
|
1011
|
+
? {
|
|
1012
|
+
workspaceBindingKey: workspaceIdentity.bindingKey,
|
|
1013
|
+
workspaceCwd: workspaceIdentity.cwd,
|
|
1014
|
+
}
|
|
1015
|
+
: {}),
|
|
476
1016
|
});
|
|
477
1017
|
} finally {
|
|
478
1018
|
deps.onProvisioningEnd?.();
|
|
@@ -480,7 +1020,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
480
1020
|
};
|
|
481
1021
|
const recoverRequestedTarget = async () => {
|
|
482
1022
|
const nowMs = getNowMs();
|
|
483
|
-
const carriedThreadName =
|
|
1023
|
+
const carriedThreadName =
|
|
1024
|
+
workspaceBinding?.threadName ?? registration.threadName;
|
|
484
1025
|
const requestedThreadName =
|
|
485
1026
|
carriedThreadName &&
|
|
486
1027
|
Threads.isTelegramTopicThreadNameValidForSlot(
|
|
@@ -495,7 +1036,17 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
495
1036
|
)
|
|
496
1037
|
? recoveryHint.threadName
|
|
497
1038
|
: undefined;
|
|
498
|
-
|
|
1039
|
+
const requestedSlot =
|
|
1040
|
+
workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot ?? recoveryHint?.slot;
|
|
1041
|
+
const recoveredSlot = deps.topicTargetStore.allocateSlot(
|
|
1042
|
+
followerProfileKey,
|
|
1043
|
+
requestedSlot,
|
|
1044
|
+
workspaceIdentity?.bindingKey,
|
|
1045
|
+
);
|
|
1046
|
+
if (!recoveredSlot) {
|
|
1047
|
+
throw new Error("Telegram Workspace slot reservation is unavailable.");
|
|
1048
|
+
}
|
|
1049
|
+
const recoveredRecord: Threads.TelegramTopicTargetRecord = {
|
|
499
1050
|
profileKey: followerProfileKey,
|
|
500
1051
|
owner:
|
|
501
1052
|
followerOwner.kind === "manual-follower"
|
|
@@ -513,17 +1064,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
513
1064
|
updatedAtMs: nowMs,
|
|
514
1065
|
instanceId: registration.instanceId,
|
|
515
1066
|
...(requestedThreadName ? { threadName: requestedThreadName } : {}),
|
|
1067
|
+
slot: recoveredSlot,
|
|
516
1068
|
lastSyncObservedAtMs: nowMs,
|
|
517
1069
|
lastReconcileAction: "follower-live-target-recovery",
|
|
518
1070
|
};
|
|
519
|
-
const requestedSlot = registration.slot ?? recoveryHint?.slot;
|
|
520
|
-
const requestedSlotAvailable =
|
|
521
|
-
!!requestedSlot &&
|
|
522
|
-
/^[A-Z]$/.test(requestedSlot) &&
|
|
523
|
-
!recordsBeforeProvision.some((record) => record.slot === requestedSlot);
|
|
524
|
-
if (requestedSlotAvailable) {
|
|
525
|
-
recoveredRecord = { ...recoveredRecord, slot: requestedSlot };
|
|
526
|
-
}
|
|
527
1071
|
return {
|
|
528
1072
|
target: recoveredRecord.target,
|
|
529
1073
|
reused: true,
|
|
@@ -533,13 +1077,22 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
533
1077
|
const runRegistration = async (): Promise<
|
|
534
1078
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
535
1079
|
> => {
|
|
536
|
-
|
|
1080
|
+
const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
|
|
1081
|
+
if (recoveryTarget) {
|
|
1082
|
+
Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
|
|
1083
|
+
}
|
|
1084
|
+
const pendingTargetRecovery = recoverableTarget && deps.topicTargetStore.listPendingProvisions().some(
|
|
1085
|
+
(entry) => entry.target?.chatId === recoverableTarget.chatId &&
|
|
1086
|
+
entry.target?.threadId === recoverableTarget.threadId &&
|
|
1087
|
+
(entry.instanceId === registration.instanceId || entry.profileKey === followerProfileKey),
|
|
1088
|
+
);
|
|
1089
|
+
let result: Threads.TelegramTopicTargetProvisionResult = reconnectRecord
|
|
537
1090
|
? {
|
|
538
1091
|
target: reconnectRecord.target,
|
|
539
1092
|
reused: true,
|
|
540
1093
|
record: reconnectRecord,
|
|
541
1094
|
}
|
|
542
|
-
: recoverableTarget
|
|
1095
|
+
: recoverableTarget && !pendingTargetRecovery
|
|
543
1096
|
? await recoverRequestedTarget()
|
|
544
1097
|
: await provisionTarget();
|
|
545
1098
|
const crossSessionReuse =
|
|
@@ -577,7 +1130,10 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
577
1130
|
!result.reused || requiresVisibilityProbe
|
|
578
1131
|
? createTelegramBusInstanceLifecycleAnnouncement({
|
|
579
1132
|
target: result.target,
|
|
580
|
-
threadName: result.
|
|
1133
|
+
threadName: result.displayTitle ??
|
|
1134
|
+
(workspaceBinding?.target.chatId === result.target.chatId &&
|
|
1135
|
+
workspaceBinding.target.threadId === result.target.threadId
|
|
1136
|
+
? workspaceBinding.displayTitle : undefined) ?? result.record.threadName,
|
|
581
1137
|
slot: result.record.slot,
|
|
582
1138
|
state: "connected",
|
|
583
1139
|
})
|
|
@@ -602,6 +1158,19 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
602
1158
|
if (recoverableTarget || probeRequiredRecord) {
|
|
603
1159
|
const activatedRecord = deps.topicTargetStore.upsert({
|
|
604
1160
|
...result.record,
|
|
1161
|
+
...(probeRequiredRecord && crossSessionReuse
|
|
1162
|
+
? {
|
|
1163
|
+
profileKey: followerProfileKey,
|
|
1164
|
+
owner:
|
|
1165
|
+
followerOwner.kind === "manual-follower"
|
|
1166
|
+
? followerOwner
|
|
1167
|
+
: {
|
|
1168
|
+
kind: "manual-follower" as const,
|
|
1169
|
+
instanceId: registration.instanceId,
|
|
1170
|
+
},
|
|
1171
|
+
instanceId: registration.instanceId,
|
|
1172
|
+
}
|
|
1173
|
+
: {}),
|
|
605
1174
|
status: "active",
|
|
606
1175
|
updatedAtMs: getNowMs(),
|
|
607
1176
|
lastSyncObservedAtMs: getNowMs(),
|
|
@@ -650,7 +1219,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
650
1219
|
connectedAnnouncement =
|
|
651
1220
|
createTelegramBusInstanceLifecycleAnnouncement({
|
|
652
1221
|
target: result.target,
|
|
653
|
-
threadName: result.record.threadName,
|
|
1222
|
+
threadName: result.displayTitle ?? result.record.threadName,
|
|
654
1223
|
slot: result.record.slot,
|
|
655
1224
|
state: "connected",
|
|
656
1225
|
});
|
|
@@ -676,6 +1245,34 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
676
1245
|
}
|
|
677
1246
|
}
|
|
678
1247
|
}
|
|
1248
|
+
if (workspaceIdentity) {
|
|
1249
|
+
const workspaceCommit =
|
|
1250
|
+
Threads.commitTelegramWorkspaceProvisionBinding({
|
|
1251
|
+
store: deps.topicTargetStore,
|
|
1252
|
+
instanceId: registration.instanceId,
|
|
1253
|
+
profileKey: followerProfileKey,
|
|
1254
|
+
displayTitle: result.displayTitle,
|
|
1255
|
+
binding: {
|
|
1256
|
+
...workspaceIdentity,
|
|
1257
|
+
target: { ...result.target },
|
|
1258
|
+
...(result.record.threadName
|
|
1259
|
+
? { threadName: result.record.threadName }
|
|
1260
|
+
: {}),
|
|
1261
|
+
...(result.record.slot ? { slot: result.record.slot } : {}),
|
|
1262
|
+
journalBindingKeys: [followerProfileKey],
|
|
1263
|
+
journalBindingsComplete: true,
|
|
1264
|
+
updatedAtMs: getNowMs(),
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
if (connectedAnnouncement && workspaceCommit.displayTitle) {
|
|
1268
|
+
connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
1269
|
+
target: result.target, threadName: workspaceCommit.displayTitle,
|
|
1270
|
+
slot: result.record.slot, state: "connected",
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
|
|
1274
|
+
await deps.topicTargetStore.persist();
|
|
1275
|
+
}
|
|
679
1276
|
deps.setSyncState(
|
|
680
1277
|
Sync.markTelegramSyncSliceFresh(
|
|
681
1278
|
deps.getSyncState(),
|
|
@@ -712,7 +1309,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
712
1309
|
});
|
|
713
1310
|
}
|
|
714
1311
|
}
|
|
715
|
-
|
|
1312
|
+
const reconcile = async (): Promise<void> => {
|
|
716
1313
|
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
717
1314
|
ThreadReconciler.planThreadReconciliation({
|
|
718
1315
|
nowMs: getNowMs(),
|
|
@@ -747,6 +1344,20 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
747
1344
|
},
|
|
748
1345
|
);
|
|
749
1346
|
await deps.topicTargetStore.persist();
|
|
1347
|
+
};
|
|
1348
|
+
try {
|
|
1349
|
+
if (deps.runWorkspaceOperation) {
|
|
1350
|
+
await deps.runWorkspaceOperation(
|
|
1351
|
+
{
|
|
1352
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1353
|
+
operationKind: "workspace.reconcile-follower-provision",
|
|
1354
|
+
scopes: [{ kind: "profile" }],
|
|
1355
|
+
},
|
|
1356
|
+
reconcile,
|
|
1357
|
+
);
|
|
1358
|
+
} else {
|
|
1359
|
+
await reconcile();
|
|
1360
|
+
}
|
|
750
1361
|
} catch (error) {
|
|
751
1362
|
deps.recordRuntimeEvent("telegram", error, {
|
|
752
1363
|
phase: "follower-register-background-reconcile",
|
|
@@ -776,6 +1387,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
776
1387
|
};
|
|
777
1388
|
const registrationPromise = runRegistration().finally(() => {
|
|
778
1389
|
pendingRegistrations.delete(registrationKey);
|
|
1390
|
+
deps.topicTargetStore.releaseWorkspaceClaim(registration.instanceId);
|
|
779
1391
|
});
|
|
780
1392
|
pendingRegistrations.set(registrationKey, registrationPromise);
|
|
781
1393
|
return registrationPromise;
|
|
@@ -823,7 +1435,15 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
823
1435
|
{
|
|
824
1436
|
isCleanupTargetProtected,
|
|
825
1437
|
callApi: deps.callApi,
|
|
826
|
-
markStaleByTarget
|
|
1438
|
+
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
1439
|
+
const stale = deps.topicTargetStore.markStaleByTarget(
|
|
1440
|
+
target, syncStatus, lastSyncError,
|
|
1441
|
+
);
|
|
1442
|
+
const inactive = deps.topicTargetStore.markWorkspaceBindingInactiveByTarget(
|
|
1443
|
+
target, (deps.getNowMs ?? Date.now)(),
|
|
1444
|
+
);
|
|
1445
|
+
return stale || inactive;
|
|
1446
|
+
},
|
|
827
1447
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
828
1448
|
persist: deps.topicTargetStore.persist,
|
|
829
1449
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
@@ -893,9 +1513,13 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
893
1513
|
);
|
|
894
1514
|
}
|
|
895
1515
|
await deps.topicTargetStore.load();
|
|
1516
|
+
const cwd = deps.getCwd?.(ctx);
|
|
1517
|
+
const normalizedCwd = cwd
|
|
1518
|
+
? Threads.normalizeTelegramWorkspacePath(cwd)
|
|
1519
|
+
: undefined;
|
|
896
1520
|
const profileKey = Threads.getTelegramThreadOwnerKey({
|
|
897
1521
|
kind: "leader",
|
|
898
|
-
cwd:
|
|
1522
|
+
cwd: normalizedCwd,
|
|
899
1523
|
instanceId: deps.instanceId,
|
|
900
1524
|
telegramProfile: deps.getTelegramProfile?.(),
|
|
901
1525
|
});
|
|
@@ -942,9 +1566,12 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
942
1566
|
ownTarget = await Sync.ensureTelegramLeaderThreadBinding({
|
|
943
1567
|
getAllowedUserId: deps.getAllowedUserId,
|
|
944
1568
|
instanceId: deps.instanceId,
|
|
945
|
-
cwd:
|
|
1569
|
+
cwd: normalizedCwd,
|
|
946
1570
|
telegramProfile: deps.getTelegramProfile?.(),
|
|
947
1571
|
forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
|
|
1572
|
+
requestedThreadName: deps.getRequestedThreadName?.(),
|
|
1573
|
+
resolveInitialWorkspaceDisplayTitle:
|
|
1574
|
+
deps.resolveInitialWorkspaceDisplayTitle,
|
|
948
1575
|
getNowMs,
|
|
949
1576
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
950
1577
|
getThreadReconciliationMachineState:
|
|
@@ -952,6 +1579,20 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
952
1579
|
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
953
1580
|
topicTargetStore: deps.topicTargetStore,
|
|
954
1581
|
callApi: deps.callApi,
|
|
1582
|
+
probeWorkspaceBinding: async (binding) => {
|
|
1583
|
+
const announcement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
1584
|
+
target: binding.target,
|
|
1585
|
+
threadName: binding.displayTitle ?? binding.threadName,
|
|
1586
|
+
slot: binding.slot,
|
|
1587
|
+
state: "connected",
|
|
1588
|
+
});
|
|
1589
|
+
await deps.callApi("sendMessage", {
|
|
1590
|
+
chat_id: announcement.target.chatId,
|
|
1591
|
+
message_thread_id: announcement.target.threadId,
|
|
1592
|
+
text: announcement.text,
|
|
1593
|
+
parse_mode: announcement.parseMode,
|
|
1594
|
+
});
|
|
1595
|
+
},
|
|
955
1596
|
recordEvent: deps.recordRuntimeEvent,
|
|
956
1597
|
});
|
|
957
1598
|
} finally {
|
|
@@ -1002,7 +1643,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
1002
1643
|
const connectedAnnouncement =
|
|
1003
1644
|
createTelegramBusInstanceLifecycleAnnouncement({
|
|
1004
1645
|
target: ownTarget.target,
|
|
1005
|
-
threadName: ownTarget.threadName,
|
|
1646
|
+
threadName: ownTarget.displayTitle ?? ownTarget.threadName,
|
|
1006
1647
|
slot: ownTarget.slot,
|
|
1007
1648
|
state: "connected",
|
|
1008
1649
|
});
|
|
@@ -1130,6 +1771,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1130
1771
|
) => Promise<unknown> | unknown;
|
|
1131
1772
|
provisionFollowerTarget?: (
|
|
1132
1773
|
registration: TelegramBusInstanceRegistration,
|
|
1774
|
+
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
1133
1775
|
) =>
|
|
1134
1776
|
| Promise<
|
|
1135
1777
|
| (TelegramTarget & { slot?: string; threadName?: string })
|
|
@@ -1140,8 +1782,20 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1140
1782
|
onFollowerDisconnected?: (
|
|
1141
1783
|
follower: TelegramBusFollowerView,
|
|
1142
1784
|
) => Promise<void> | void;
|
|
1785
|
+
renameFollowerThread?: (
|
|
1786
|
+
follower: TelegramBusFollowerView,
|
|
1787
|
+
threadName: string,
|
|
1788
|
+
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1789
|
+
resetFollowerThreadName?: (
|
|
1790
|
+
follower: TelegramBusFollowerView,
|
|
1791
|
+
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1792
|
+
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
1793
|
+
onFollowerRegistered?: () => void;
|
|
1794
|
+
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
1795
|
+
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
1143
1796
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
1144
1797
|
runFollowerMutation?: TelegramBusFollowerMutationRunner;
|
|
1798
|
+
runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
|
|
1145
1799
|
}): (
|
|
1146
1800
|
envelope: TelegramBusEnvelope,
|
|
1147
1801
|
) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
|
|
@@ -1423,22 +2077,45 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1423
2077
|
return createUnauthorizedBusAck(envelope.requestId);
|
|
1424
2078
|
}
|
|
1425
2079
|
switch (envelope.kind) {
|
|
1426
|
-
case "follower.register":
|
|
2080
|
+
case "follower.register":
|
|
2081
|
+
case "follower.restoreWorkspace": {
|
|
1427
2082
|
const compatibility = getTelegramBusProtocolCompatibility({
|
|
1428
2083
|
local: deps.protocolIdentity,
|
|
1429
2084
|
remote: envelope.registration.protocol,
|
|
1430
2085
|
});
|
|
1431
|
-
|
|
2086
|
+
const displayCompatible = () =>
|
|
2087
|
+
(deps.getThreadDisplayMode?.() ?? "names") === "names" || (
|
|
2088
|
+
hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
|
|
2089
|
+
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)
|
|
2090
|
+
);
|
|
2091
|
+
const restoringWorkspace =
|
|
2092
|
+
envelope.kind === "follower.restoreWorkspace";
|
|
2093
|
+
const supportsWorkspaceAutoConnect =
|
|
2094
|
+
hasTelegramBusCapability(
|
|
2095
|
+
deps.protocolIdentity,
|
|
2096
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
2097
|
+
) &&
|
|
2098
|
+
hasTelegramBusCapability(
|
|
2099
|
+
envelope.registration.protocol,
|
|
2100
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
2101
|
+
);
|
|
2102
|
+
if (
|
|
2103
|
+
!compatibility.compatible || !displayCompatible() ||
|
|
2104
|
+
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
2105
|
+
) {
|
|
1432
2106
|
return {
|
|
1433
2107
|
kind: "bus.ack" as const,
|
|
1434
2108
|
requestId: envelope.requestId,
|
|
1435
2109
|
ok: false,
|
|
1436
2110
|
protocol: deps.protocolIdentity,
|
|
1437
2111
|
error: { code: "incompatible-protocol" as const },
|
|
1438
|
-
message: `Incompatible Telegram bus protocol: ${
|
|
2112
|
+
message: `Incompatible Telegram bus protocol: ${
|
|
2113
|
+
compatibility.reason ?? "missing-capability"
|
|
2114
|
+
}.`,
|
|
1439
2115
|
};
|
|
1440
2116
|
}
|
|
1441
|
-
|
|
2117
|
+
const registrationOperation = () =>
|
|
2118
|
+
runFollowerMutation(envelope.registration, async () => {
|
|
1442
2119
|
try {
|
|
1443
2120
|
if (!envelope.registration.registrationGeneration) {
|
|
1444
2121
|
throw new Error(
|
|
@@ -1453,7 +2130,18 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1453
2130
|
}
|
|
1454
2131
|
const target = await deps.provisionFollowerTarget?.(
|
|
1455
2132
|
envelope.registration,
|
|
2133
|
+
{ existingWorkspaceBindingOnly: restoringWorkspace },
|
|
1456
2134
|
);
|
|
2135
|
+
if (restoringWorkspace && !target) {
|
|
2136
|
+
return {
|
|
2137
|
+
kind: "bus.ack" as const,
|
|
2138
|
+
requestId: envelope.requestId,
|
|
2139
|
+
ok: false,
|
|
2140
|
+
protocol: deps.protocolIdentity,
|
|
2141
|
+
error: { code: "workspace-binding-unavailable" as const },
|
|
2142
|
+
message: "No remembered Telegram Workspace Thread is available.",
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
1457
2145
|
if (
|
|
1458
2146
|
deps.getCurrentLeaderEpoch &&
|
|
1459
2147
|
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
@@ -1462,14 +2150,21 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1462
2150
|
"Telegram follower registration lost leader ownership.",
|
|
1463
2151
|
);
|
|
1464
2152
|
}
|
|
2153
|
+
if (!displayCompatible()) {
|
|
2154
|
+
throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2155
|
+
}
|
|
1465
2156
|
const registeredTarget = target ?? envelope.registration.target;
|
|
2157
|
+
const registeredSlot = target?.slot ?? envelope.registration.slot;
|
|
2158
|
+
if (deps.provisionFollowerTarget &&
|
|
2159
|
+
registeredTarget?.threadId !== undefined &&
|
|
2160
|
+
(!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
|
|
2161
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
2162
|
+
}
|
|
1466
2163
|
deps.followerRegistry.register({
|
|
1467
2164
|
...envelope.registration,
|
|
1468
2165
|
connectedAtMs: getNowMs(),
|
|
1469
2166
|
target: registeredTarget,
|
|
1470
|
-
...(
|
|
1471
|
-
? { slot: target?.slot ?? envelope.registration.slot }
|
|
1472
|
-
: {}),
|
|
2167
|
+
...(registeredSlot ? { slot: registeredSlot } : {}),
|
|
1473
2168
|
...((target?.threadName ?? envelope.registration.threadName)
|
|
1474
2169
|
? {
|
|
1475
2170
|
threadName:
|
|
@@ -1477,12 +2172,20 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1477
2172
|
}
|
|
1478
2173
|
: {}),
|
|
1479
2174
|
});
|
|
2175
|
+
const follower = deps.followerRegistry.get(envelope.registration.instanceId);
|
|
2176
|
+
const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
|
|
2177
|
+
deps.onFollowerRegistered?.();
|
|
1480
2178
|
return {
|
|
1481
2179
|
kind: "bus.ack" as const,
|
|
1482
2180
|
requestId: envelope.requestId,
|
|
1483
2181
|
ok: true,
|
|
1484
2182
|
protocol: deps.protocolIdentity,
|
|
1485
|
-
...(registeredTarget ? {
|
|
2183
|
+
...(registeredTarget ? {
|
|
2184
|
+
result: {
|
|
2185
|
+
...registeredTarget,
|
|
2186
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
2187
|
+
},
|
|
2188
|
+
} : {}),
|
|
1486
2189
|
};
|
|
1487
2190
|
} catch (error) {
|
|
1488
2191
|
return {
|
|
@@ -1498,9 +2201,203 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1498
2201
|
}
|
|
1499
2202
|
},
|
|
1500
2203
|
);
|
|
2204
|
+
if (!deps.runWorkspaceAdmission) return registrationOperation();
|
|
2205
|
+
try {
|
|
2206
|
+
return await deps.runWorkspaceAdmission(
|
|
2207
|
+
{
|
|
2208
|
+
operationId: `follower-registration:${envelope.requestId}`,
|
|
2209
|
+
operationKind: "workspace.register-follower",
|
|
2210
|
+
scopes: [{ kind: "profile" }],
|
|
2211
|
+
},
|
|
2212
|
+
registrationOperation,
|
|
2213
|
+
);
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
return {
|
|
2216
|
+
kind: "bus.ack" as const,
|
|
2217
|
+
requestId: envelope.requestId,
|
|
2218
|
+
ok: false,
|
|
2219
|
+
protocol: deps.protocolIdentity,
|
|
2220
|
+
message:
|
|
2221
|
+
error instanceof Error
|
|
2222
|
+
? error.message
|
|
2223
|
+
: "Telegram bus follower registration admission failed.",
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
1501
2226
|
}
|
|
1502
2227
|
case "follower.offerQueueHandoff":
|
|
1503
2228
|
return routeQueueHandoff(envelope);
|
|
2229
|
+
case "follower.setThreadDisplayMode": {
|
|
2230
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2231
|
+
const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2232
|
+
deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
|
|
2233
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2234
|
+
if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
|
|
2235
|
+
!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) ||
|
|
2236
|
+
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)) {
|
|
2237
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2238
|
+
message: "Thread display settings require current registration and compatible leader authority." };
|
|
2239
|
+
}
|
|
2240
|
+
try {
|
|
2241
|
+
await deps.applyThreadDisplayMode(envelope.mode, current);
|
|
2242
|
+
if (!current()) throw new Error("Thread display setting completed for a stale registration.");
|
|
2243
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result: { mode: envelope.mode } };
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2246
|
+
message: error instanceof Error ? error.message : "Thread display setting was not fully applied." };
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
case "follower.renameThread": {
|
|
2250
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2251
|
+
return runFollowerMutation(
|
|
2252
|
+
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2253
|
+
async () => {
|
|
2254
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2255
|
+
if (!follower) {
|
|
2256
|
+
return {
|
|
2257
|
+
kind: "bus.ack" as const,
|
|
2258
|
+
requestId: envelope.requestId,
|
|
2259
|
+
ok: false,
|
|
2260
|
+
message: "Unknown Telegram bus follower instance.",
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
if (
|
|
2264
|
+
!follower.registrationGeneration ||
|
|
2265
|
+
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
2266
|
+
follower.target?.chatId !== envelope.target.chatId ||
|
|
2267
|
+
follower.target?.threadId !== envelope.target.threadId
|
|
2268
|
+
) {
|
|
2269
|
+
return {
|
|
2270
|
+
kind: "bus.ack" as const,
|
|
2271
|
+
requestId: envelope.requestId,
|
|
2272
|
+
ok: false,
|
|
2273
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
if (
|
|
2277
|
+
!hasTelegramBusCapability(
|
|
2278
|
+
deps.protocolIdentity,
|
|
2279
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2280
|
+
) ||
|
|
2281
|
+
!hasTelegramBusCapability(
|
|
2282
|
+
follower.protocol,
|
|
2283
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2284
|
+
) ||
|
|
2285
|
+
!deps.renameFollowerThread
|
|
2286
|
+
) {
|
|
2287
|
+
return {
|
|
2288
|
+
kind: "bus.ack" as const,
|
|
2289
|
+
requestId: envelope.requestId,
|
|
2290
|
+
ok: false,
|
|
2291
|
+
message: "Telegram Workspace Thread rename is unavailable.",
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
try {
|
|
2295
|
+
const result = await deps.renameFollowerThread(
|
|
2296
|
+
follower,
|
|
2297
|
+
envelope.threadName,
|
|
2298
|
+
);
|
|
2299
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
2300
|
+
if (
|
|
2301
|
+
current?.registrationGeneration !== follower.registrationGeneration
|
|
2302
|
+
) {
|
|
2303
|
+
return {
|
|
2304
|
+
kind: "bus.ack" as const,
|
|
2305
|
+
requestId: envelope.requestId,
|
|
2306
|
+
ok: false,
|
|
2307
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
deps.followerRegistry.register({
|
|
2311
|
+
...current,
|
|
2312
|
+
threadName: result.threadName,
|
|
2313
|
+
connectedAtMs: current.connectedAtMs,
|
|
2314
|
+
});
|
|
2315
|
+
return {
|
|
2316
|
+
kind: "bus.ack" as const,
|
|
2317
|
+
requestId: envelope.requestId,
|
|
2318
|
+
ok: true,
|
|
2319
|
+
result,
|
|
2320
|
+
};
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
return {
|
|
2323
|
+
kind: "bus.ack" as const,
|
|
2324
|
+
requestId: envelope.requestId,
|
|
2325
|
+
ok: false,
|
|
2326
|
+
message:
|
|
2327
|
+
error instanceof Error
|
|
2328
|
+
? error.message
|
|
2329
|
+
: "Telegram Workspace Thread rename failed.",
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
},
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
case "follower.resetThreadName": {
|
|
2336
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2337
|
+
return runFollowerMutation(
|
|
2338
|
+
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2339
|
+
async () => {
|
|
2340
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2341
|
+
if (!follower || !follower.registrationGeneration ||
|
|
2342
|
+
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
2343
|
+
follower.target?.chatId !== envelope.target.chatId ||
|
|
2344
|
+
follower.target?.threadId !== envelope.target.threadId) {
|
|
2345
|
+
return {
|
|
2346
|
+
kind: "bus.ack" as const,
|
|
2347
|
+
requestId: envelope.requestId,
|
|
2348
|
+
ok: false,
|
|
2349
|
+
message: "Stale or unknown Telegram bus follower registration.",
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
if (!hasTelegramBusCapability(
|
|
2353
|
+
deps.protocolIdentity,
|
|
2354
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2355
|
+
) || !hasTelegramBusCapability(
|
|
2356
|
+
follower.protocol,
|
|
2357
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2358
|
+
) || !deps.resetFollowerThreadName) {
|
|
2359
|
+
return {
|
|
2360
|
+
kind: "bus.ack" as const,
|
|
2361
|
+
requestId: envelope.requestId,
|
|
2362
|
+
ok: false,
|
|
2363
|
+
message: "Telegram Workspace Thread reset is unavailable.",
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
const result = await deps.resetFollowerThreadName(follower);
|
|
2368
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
2369
|
+
if (current?.registrationGeneration !==
|
|
2370
|
+
follower.registrationGeneration) {
|
|
2371
|
+
return {
|
|
2372
|
+
kind: "bus.ack" as const,
|
|
2373
|
+
requestId: envelope.requestId,
|
|
2374
|
+
ok: false,
|
|
2375
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
deps.followerRegistry.register({
|
|
2379
|
+
...current,
|
|
2380
|
+
threadName: result.threadName,
|
|
2381
|
+
connectedAtMs: current.connectedAtMs,
|
|
2382
|
+
});
|
|
2383
|
+
return {
|
|
2384
|
+
kind: "bus.ack" as const,
|
|
2385
|
+
requestId: envelope.requestId,
|
|
2386
|
+
ok: true,
|
|
2387
|
+
result,
|
|
2388
|
+
};
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
return {
|
|
2391
|
+
kind: "bus.ack" as const,
|
|
2392
|
+
requestId: envelope.requestId,
|
|
2393
|
+
ok: false,
|
|
2394
|
+
message: error instanceof Error
|
|
2395
|
+
? error.message : "Telegram Workspace Thread reset failed.",
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
},
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
1504
2401
|
case "follower.disconnect": {
|
|
1505
2402
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1506
2403
|
return runFollowerMutation(
|
|
@@ -1573,12 +2470,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1573
2470
|
envelope.instanceId,
|
|
1574
2471
|
getNowMs(),
|
|
1575
2472
|
);
|
|
2473
|
+
const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
|
|
1576
2474
|
return follower
|
|
1577
2475
|
? {
|
|
1578
2476
|
kind: "bus.ack",
|
|
1579
2477
|
requestId: envelope.requestId,
|
|
1580
2478
|
ok: true,
|
|
1581
2479
|
result: {
|
|
2480
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
1582
2481
|
eligibleElectionSlots: deps.followerRegistry
|
|
1583
2482
|
.list()
|
|
1584
2483
|
.filter(
|
|
@@ -1860,7 +2759,8 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1860
2759
|
): TelegramBusLeaderRuntime<TContext> {
|
|
1861
2760
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1862
2761
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
1863
|
-
const followerStaleAfterMs =
|
|
2762
|
+
const followerStaleAfterMs =
|
|
2763
|
+
deps.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
1864
2764
|
const runFollowerMutation = createTelegramBusFollowerMutationRunner();
|
|
1865
2765
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
1866
2766
|
let pruneGeneration = 0;
|
|
@@ -2014,8 +2914,15 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
2014
2914
|
routeQueueHandoff: deps.routeQueueHandoff,
|
|
2015
2915
|
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
2016
2916
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
2917
|
+
renameFollowerThread: deps.renameFollowerThread,
|
|
2918
|
+
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
2919
|
+
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
2920
|
+
onFollowerRegistered: deps.onFollowerRegistered,
|
|
2921
|
+
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
|
2922
|
+
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
2017
2923
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
2018
2924
|
runFollowerMutation,
|
|
2925
|
+
runWorkspaceAdmission: deps.runWorkspaceAdmission,
|
|
2019
2926
|
});
|
|
2020
2927
|
const routeQueueHandoffEnvelope = async (
|
|
2021
2928
|
input: Parameters<TelegramBusLeaderRuntime<TContext>["routeQueueHandoff"]>[0],
|