@llblab/pi-telegram 0.20.0 → 0.20.2
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 +4 -1
- package/BACKLOG.md +5 -17
- package/CHANGELOG.md +22 -0
- package/README.md +61 -58
- package/docs/README.md +2 -2
- package/docs/architecture.md +5 -3
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +7 -4
- package/docs/outbound.md +5 -5
- package/index.ts +138 -208
- package/lib/bindings.ts +39 -13
- package/lib/bus-follower.ts +138 -7
- package/lib/bus-leader.ts +185 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +83 -23
- package/lib/paths.ts +11 -0
- package/lib/prompts.ts +19 -7
- package/lib/setup.ts +27 -10
- package/lib/status.ts +9 -2
- package/lib/threads.ts +64 -26
- package/lib/turns.ts +0 -2
- package/package.json +1 -1
package/lib/bus-follower.ts
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
createTelegramBusLocalServer,
|
|
17
17
|
createUnauthorizedBusAck,
|
|
18
18
|
getTelegramBusSocketPath,
|
|
19
|
+
resolveTelegramBusSocketPath,
|
|
19
20
|
sendTelegramBusLocalEnvelope,
|
|
20
21
|
type TelegramBusEnvelope,
|
|
22
|
+
type TelegramBusSocketPathSource,
|
|
21
23
|
} from "./bus.ts";
|
|
22
24
|
import {
|
|
23
25
|
getTelegramBusTransportRetryPolicy,
|
|
@@ -133,7 +135,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntime {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
export interface TelegramBusFollowerApiCallerDeps {
|
|
136
|
-
socketPath:
|
|
138
|
+
socketPath: TelegramBusSocketPathSource;
|
|
137
139
|
instanceId: string;
|
|
138
140
|
createRequestId: () => string;
|
|
139
141
|
getAuthSecret?: () => string | undefined;
|
|
@@ -149,6 +151,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
149
151
|
getLeaderAuthSecret?: (leader: { busSecret?: string }) => string | undefined;
|
|
150
152
|
setActiveAuthSecret?: (secret: string | undefined) => void;
|
|
151
153
|
followerBusSocketPath?: string;
|
|
154
|
+
getFollowerBusSocketPath?: () => string;
|
|
152
155
|
getLeaderSocketPath?: () => string;
|
|
153
156
|
startReceiving?: () => Promise<void>;
|
|
154
157
|
stopReceiving?: () => Promise<void> | void;
|
|
@@ -170,6 +173,63 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
170
173
|
onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
export function createTelegramManualFollowerProfileKeyResolver(input: {
|
|
177
|
+
getActiveProfileName: () => string | undefined;
|
|
178
|
+
manualFollowerOwnerId: string;
|
|
179
|
+
}): () => string {
|
|
180
|
+
return () =>
|
|
181
|
+
Threads.getTelegramThreadOwnerKey({
|
|
182
|
+
kind: "manual-follower",
|
|
183
|
+
instanceId: input.manualFollowerOwnerId,
|
|
184
|
+
telegramProfile: input.getActiveProfileName(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type TelegramBusFollowerPromotionHandler<TContext> = (
|
|
189
|
+
ctx: TContext,
|
|
190
|
+
binding: TelegramBusFollowerPromotedBinding,
|
|
191
|
+
) => Promise<void>;
|
|
192
|
+
|
|
193
|
+
export function createTelegramBusFollowerPromotionHandler<
|
|
194
|
+
TContext extends { cwd: string },
|
|
195
|
+
>(input: {
|
|
196
|
+
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
197
|
+
instanceId: string;
|
|
198
|
+
getActiveProfileName: () => string | undefined;
|
|
199
|
+
startLeader: (ctx: TContext) => Promise<unknown> | unknown;
|
|
200
|
+
recordRuntimeEvent: (
|
|
201
|
+
category: string,
|
|
202
|
+
error: unknown,
|
|
203
|
+
details?: Record<string, unknown>,
|
|
204
|
+
) => void;
|
|
205
|
+
}): TelegramBusFollowerPromotionHandler<TContext> {
|
|
206
|
+
return async (ctx, binding) => {
|
|
207
|
+
const promotedRecord = await Threads.promoteTelegramFollowerBindingToLeader({
|
|
208
|
+
store: input.topicTargetStore,
|
|
209
|
+
instanceId: input.instanceId,
|
|
210
|
+
cwd: ctx.cwd,
|
|
211
|
+
telegramProfile: input.getActiveProfileName(),
|
|
212
|
+
target: binding.target,
|
|
213
|
+
slot: binding.slot,
|
|
214
|
+
threadName: binding.threadName,
|
|
215
|
+
});
|
|
216
|
+
if (promotedRecord) {
|
|
217
|
+
input.recordRuntimeEvent(
|
|
218
|
+
"bus",
|
|
219
|
+
"Follower thread binding promoted to leader",
|
|
220
|
+
{
|
|
221
|
+
phase: "follower-promoted-binding",
|
|
222
|
+
chatId: promotedRecord.target.chatId,
|
|
223
|
+
threadId: promotedRecord.target.threadId,
|
|
224
|
+
slot: promotedRecord.slot,
|
|
225
|
+
threadName: promotedRecord.threadName,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
await input.startLeader(ctx);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
173
233
|
export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
174
234
|
topicTargetStore: Pick<
|
|
175
235
|
Threads.TelegramTopicTargetStore,
|
|
@@ -180,7 +240,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
|
180
240
|
"getTarget" | "setRegistered"
|
|
181
241
|
>;
|
|
182
242
|
instanceId: string;
|
|
183
|
-
|
|
243
|
+
getManualFollowerProfileKey: () => string;
|
|
184
244
|
manualFollowerOwnerId: string;
|
|
185
245
|
getSyncState: () => Sync.TelegramSyncState;
|
|
186
246
|
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
@@ -238,7 +298,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
|
|
|
238
298
|
TCallbackQuery,
|
|
239
299
|
TMessage = unknown,
|
|
240
300
|
> {
|
|
241
|
-
socketPath:
|
|
301
|
+
socketPath: TelegramBusSocketPathSource;
|
|
242
302
|
instanceId: string;
|
|
243
303
|
getAuthSecret?: () => string | undefined;
|
|
244
304
|
getContext: () => TContext | undefined;
|
|
@@ -273,6 +333,71 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
|
|
|
273
333
|
) => void;
|
|
274
334
|
}
|
|
275
335
|
|
|
336
|
+
export interface TelegramBusFollowerRuntimeAssemblyDeps<
|
|
337
|
+
TContext extends { cwd?: string },
|
|
338
|
+
TReactionUpdate,
|
|
339
|
+
TCallbackQuery,
|
|
340
|
+
TMessage = unknown,
|
|
341
|
+
> {
|
|
342
|
+
receiver: Omit<
|
|
343
|
+
TelegramBusForwardedUpdateReceiverRuntimeDeps<
|
|
344
|
+
TContext,
|
|
345
|
+
TReactionUpdate,
|
|
346
|
+
TCallbackQuery,
|
|
347
|
+
TMessage
|
|
348
|
+
>,
|
|
349
|
+
"handleReplaceTarget"
|
|
350
|
+
>;
|
|
351
|
+
targetReplacement: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>;
|
|
352
|
+
recovery: Omit<
|
|
353
|
+
TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
|
|
354
|
+
"getRegistrationRuntime"
|
|
355
|
+
>;
|
|
356
|
+
registration: Omit<
|
|
357
|
+
TelegramBusFollowerRegistrationRuntimeDeps<TContext>,
|
|
358
|
+
"startReceiving" | "stopReceiving" | "onHeartbeatFailure"
|
|
359
|
+
>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface TelegramBusFollowerRuntimeAssembly<TContext> {
|
|
363
|
+
receiver: TelegramBusForwardedUpdateReceiverRuntime;
|
|
364
|
+
registration: TelegramBusFollowerRegistrationRuntime<TContext>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function createTelegramBusFollowerRuntimeAssembly<
|
|
368
|
+
TContext extends { cwd?: string },
|
|
369
|
+
TReactionUpdate,
|
|
370
|
+
TCallbackQuery,
|
|
371
|
+
TMessage = unknown,
|
|
372
|
+
>(
|
|
373
|
+
deps: TelegramBusFollowerRuntimeAssemblyDeps<
|
|
374
|
+
TContext,
|
|
375
|
+
TReactionUpdate,
|
|
376
|
+
TCallbackQuery,
|
|
377
|
+
TMessage
|
|
378
|
+
>,
|
|
379
|
+
): TelegramBusFollowerRuntimeAssembly<TContext> {
|
|
380
|
+
const receiver = createTelegramBusForwardedUpdateReceiverRuntime({
|
|
381
|
+
...deps.receiver,
|
|
382
|
+
handleReplaceTarget:
|
|
383
|
+
createTelegramBusFollowerTargetReplacementHandler(
|
|
384
|
+
deps.targetReplacement,
|
|
385
|
+
),
|
|
386
|
+
});
|
|
387
|
+
let registration: TelegramBusFollowerRegistrationRuntime<TContext>;
|
|
388
|
+
const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
|
|
389
|
+
...deps.recovery,
|
|
390
|
+
getRegistrationRuntime: () => registration,
|
|
391
|
+
});
|
|
392
|
+
registration = createTelegramBusFollowerRegistrationRuntime({
|
|
393
|
+
...deps.registration,
|
|
394
|
+
startReceiving: receiver.start,
|
|
395
|
+
stopReceiving: receiver.stop,
|
|
396
|
+
onHeartbeatFailure: recovery,
|
|
397
|
+
});
|
|
398
|
+
return { receiver, registration };
|
|
399
|
+
}
|
|
400
|
+
|
|
276
401
|
export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
277
402
|
deps: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
|
|
278
403
|
): NonNullable<
|
|
@@ -305,7 +430,7 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
|
305
430
|
);
|
|
306
431
|
}
|
|
307
432
|
const profileKey =
|
|
308
|
-
currentRecord?.profileKey ?? deps.
|
|
433
|
+
currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
|
|
309
434
|
deps.topicTargetStore.upsert({
|
|
310
435
|
profileKey,
|
|
311
436
|
owner: {
|
|
@@ -354,11 +479,12 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
354
479
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
355
480
|
const timeoutMs = deps.timeoutMs ?? 30000;
|
|
356
481
|
return async (method, args) => {
|
|
482
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
357
483
|
const response = await sendTelegramBusLocalEnvelope({
|
|
358
|
-
socketPath
|
|
484
|
+
socketPath,
|
|
359
485
|
timeoutMs,
|
|
360
486
|
retry: getTelegramBusTransportRetryPolicy({
|
|
361
|
-
endpoint:
|
|
487
|
+
endpoint: socketPath,
|
|
362
488
|
operation: "operation",
|
|
363
489
|
}),
|
|
364
490
|
envelope: {
|
|
@@ -650,6 +776,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
650
776
|
let activeLeaderSocketPath: string | undefined;
|
|
651
777
|
let activeAuthSecret: string | undefined;
|
|
652
778
|
let activeContext: TContext | undefined;
|
|
779
|
+
let lastKnownTarget: TelegramTarget | undefined;
|
|
653
780
|
const stopHeartbeat = () => {
|
|
654
781
|
if (!heartbeatInterval) return;
|
|
655
782
|
clearInterval(heartbeatInterval);
|
|
@@ -660,6 +787,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
660
787
|
activeAuthSecret = undefined;
|
|
661
788
|
deps.setActiveAuthSecret?.(undefined);
|
|
662
789
|
deps.registrationState?.setRegistered(false);
|
|
790
|
+
lastKnownTarget = undefined;
|
|
663
791
|
activeContext = undefined;
|
|
664
792
|
void deps.stopReceiving?.();
|
|
665
793
|
};
|
|
@@ -725,7 +853,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
725
853
|
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
726
854
|
cwd: ctx.cwd,
|
|
727
855
|
pid: getPid(),
|
|
728
|
-
|
|
856
|
+
target: deps.registrationState?.getTarget() ?? lastKnownTarget,
|
|
857
|
+
busSocketPath:
|
|
858
|
+
deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
|
|
729
859
|
connectedAtMs: getNowMs(),
|
|
730
860
|
},
|
|
731
861
|
});
|
|
@@ -778,6 +908,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
778
908
|
registrationResult.target,
|
|
779
909
|
registrationResult,
|
|
780
910
|
);
|
|
911
|
+
lastKnownTarget = registrationResult.target;
|
|
781
912
|
activeLeaderSocketPath = leaderSocketPath;
|
|
782
913
|
activeContext = ctx;
|
|
783
914
|
await sendHeartbeat();
|
package/lib/bus-leader.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TelegramBusFollowerRegistry,
|
|
21
21
|
type TelegramBusFollowerView,
|
|
22
22
|
type TelegramBusInstanceRegistration,
|
|
23
|
+
type TelegramBusSocketPathSource,
|
|
23
24
|
} from "./bus.ts";
|
|
24
25
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
25
26
|
|
|
@@ -153,15 +154,105 @@ export interface TelegramBusLeaderApiProxyDeps {
|
|
|
153
154
|
) => Promise<unknown> | unknown;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
export interface
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
158
|
+
runtime: Omit<
|
|
159
|
+
TelegramBusLeaderRuntimeDeps<TContext>,
|
|
160
|
+
| "callApi"
|
|
161
|
+
| "onFollowerPruned"
|
|
162
|
+
| "provisionFollowerTarget"
|
|
163
|
+
| "provisionLeaderTarget"
|
|
164
|
+
| "reconcileFollowerBindings"
|
|
165
|
+
| "recordRuntimeEvent"
|
|
166
|
+
>;
|
|
167
|
+
getAllowedUserId: () => number | undefined;
|
|
168
|
+
instanceId: string;
|
|
169
|
+
getCwd?: (ctx: TContext) => string | undefined;
|
|
170
|
+
getTelegramProfile?: () => string | undefined;
|
|
171
|
+
shouldForceFreshUnnamed?: () => boolean;
|
|
172
|
+
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
173
|
+
callApi: TelegramBusLeaderTargetProvisionerDeps<TContext>["callApi"];
|
|
174
|
+
callMultipart: TelegramBusLeaderApiProxyDeps["callMultipart"];
|
|
175
|
+
downloadFile: TelegramBusLeaderApiProxyDeps["downloadFile"];
|
|
176
|
+
recoverStaleTargetError?: TelegramBusLeaderApiProxyDeps["recoverStaleTargetError"];
|
|
177
|
+
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
178
|
+
getThreadReconciliationMachineState?: TelegramBusLeaderTargetProvisionerDeps<TContext>["getThreadReconciliationMachineState"];
|
|
179
|
+
recordThreadReconciliationPlan?: TelegramBusLeaderTargetProvisionerDeps<TContext>["recordThreadReconciliationPlan"];
|
|
180
|
+
getSyncState: () => Sync.TelegramSyncState;
|
|
181
|
+
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
182
|
+
setLeaderTarget: TelegramBusLeaderTargetProvisionerDeps<TContext>["setLeaderTarget"];
|
|
183
|
+
onProvisioningStart?: () => void;
|
|
184
|
+
onProvisioningEnd?: () => void;
|
|
185
|
+
recordRuntimeEvent: NonNullable<
|
|
186
|
+
TelegramBusLeaderRuntimeDeps<TContext>["recordRuntimeEvent"]
|
|
187
|
+
>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
191
|
+
deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>,
|
|
192
|
+
): TelegramBusLeaderRuntime<TContext> {
|
|
193
|
+
const provisionerPorts = {
|
|
194
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
195
|
+
topicTargetStore: deps.topicTargetStore,
|
|
196
|
+
callApi: deps.callApi,
|
|
197
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
198
|
+
getSyncState: deps.getSyncState,
|
|
199
|
+
setSyncState: deps.setSyncState,
|
|
200
|
+
onProvisioningStart: deps.onProvisioningStart,
|
|
201
|
+
onProvisioningEnd: deps.onProvisioningEnd,
|
|
202
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
203
|
+
};
|
|
204
|
+
return createTelegramBusLeaderRuntime({
|
|
205
|
+
...deps.runtime,
|
|
206
|
+
provisionLeaderTarget: createTelegramBusLeaderTargetProvisioner({
|
|
207
|
+
...provisionerPorts,
|
|
208
|
+
instanceId: deps.instanceId,
|
|
209
|
+
getCwd: deps.getCwd,
|
|
210
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
211
|
+
shouldForceFreshUnnamed: deps.shouldForceFreshUnnamed,
|
|
212
|
+
getThreadReconciliationMachineState:
|
|
213
|
+
deps.getThreadReconciliationMachineState,
|
|
214
|
+
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
215
|
+
setLeaderTarget: deps.setLeaderTarget,
|
|
216
|
+
}),
|
|
217
|
+
onFollowerPruned: createTelegramBusFollowerPruneHandler({
|
|
218
|
+
...provisionerPorts,
|
|
219
|
+
}),
|
|
220
|
+
provisionFollowerTarget: createTelegramBusFollowerTargetProvisioner({
|
|
221
|
+
...provisionerPorts,
|
|
222
|
+
}),
|
|
223
|
+
reconcileFollowerBindings:
|
|
224
|
+
createTelegramBusFollowerBindingRealityReconciler({
|
|
225
|
+
topicTargetStore: deps.topicTargetStore,
|
|
226
|
+
followerRegistry: deps.runtime.followerRegistry,
|
|
227
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
228
|
+
}),
|
|
229
|
+
callApi: createTelegramBusLeaderApiProxy({
|
|
230
|
+
call: deps.callApi,
|
|
231
|
+
callMultipart: deps.callMultipart,
|
|
232
|
+
downloadFile: deps.downloadFile,
|
|
233
|
+
recoverStaleTargetError: deps.recoverStaleTargetError,
|
|
234
|
+
}),
|
|
235
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface TelegramBusFollowerBindingRealityDeps {
|
|
240
|
+
topicTargetStore: Pick<
|
|
241
|
+
Threads.TelegramTopicTargetStore,
|
|
242
|
+
| "load"
|
|
243
|
+
| "list"
|
|
244
|
+
| "markOfflineByInstanceId"
|
|
245
|
+
| "forgetIdentityByProfileKey"
|
|
246
|
+
| "getBotState"
|
|
247
|
+
| "persist"
|
|
248
|
+
| "setBotState"
|
|
249
|
+
>;
|
|
250
|
+
followerRegistry: Pick<TelegramBusFollowerRegistry, "list">;
|
|
159
251
|
recordRuntimeEvent: (
|
|
160
252
|
category: string,
|
|
161
253
|
error: unknown,
|
|
162
254
|
details?: Record<string, unknown>,
|
|
163
255
|
) => void;
|
|
164
|
-
getNowMs?: () => number;
|
|
165
256
|
}
|
|
166
257
|
|
|
167
258
|
export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
@@ -176,7 +267,7 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
|
|
|
176
267
|
) => void;
|
|
177
268
|
|
|
178
269
|
export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
179
|
-
socketPath:
|
|
270
|
+
socketPath: TelegramBusSocketPathSource;
|
|
180
271
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
181
272
|
authSecret?: string;
|
|
182
273
|
startPolling: (ctx: TContext) => void | Promise<void>;
|
|
@@ -191,11 +282,12 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
191
282
|
provisionFollowerTarget?: (
|
|
192
283
|
registration: TelegramBusInstanceRegistration,
|
|
193
284
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
194
|
-
|
|
285
|
+
reconcileFollowerBindings?: () => Promise<unknown> | unknown;
|
|
195
286
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
196
287
|
getNowMs?: () => number;
|
|
197
288
|
followerPruneIntervalMs?: number;
|
|
198
289
|
followerStaleAfterMs?: number;
|
|
290
|
+
followerRecoveryGraceMs?: number;
|
|
199
291
|
onFollowerPruned?: (
|
|
200
292
|
follower: TelegramBusFollowerView,
|
|
201
293
|
) => Promise<void> | void;
|
|
@@ -206,39 +298,61 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
206
298
|
) => void;
|
|
207
299
|
}
|
|
208
300
|
|
|
209
|
-
export function
|
|
210
|
-
deps:
|
|
211
|
-
): () => Promise<
|
|
301
|
+
export function createTelegramBusFollowerBindingRealityReconciler(
|
|
302
|
+
deps: TelegramBusFollowerBindingRealityDeps,
|
|
303
|
+
): () => Promise<number> {
|
|
212
304
|
return async () => {
|
|
213
305
|
await deps.topicTargetStore.load();
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
record.
|
|
221
|
-
record.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
connectedAtMs: nowMs,
|
|
233
|
-
});
|
|
234
|
-
restored += 1;
|
|
306
|
+
const liveInstanceIds = new Set(
|
|
307
|
+
deps.followerRegistry.list().map((follower) => follower.instanceId),
|
|
308
|
+
);
|
|
309
|
+
const staleRecords = deps.topicTargetStore.list().filter((record) => {
|
|
310
|
+
return (
|
|
311
|
+
record.owner?.kind === "manual-follower" &&
|
|
312
|
+
!!record.instanceId &&
|
|
313
|
+
!liveInstanceIds.has(record.instanceId)
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
const staleInstanceIds = new Set(
|
|
317
|
+
staleRecords.flatMap((record) =>
|
|
318
|
+
record.instanceId ? [record.instanceId] : [],
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
let removed = 0;
|
|
322
|
+
for (const instanceId of staleInstanceIds) {
|
|
323
|
+
removed += deps.topicTargetStore.markOfflineByInstanceId(instanceId);
|
|
235
324
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
for (const record of staleRecords) {
|
|
326
|
+
const profileKey =
|
|
327
|
+
record.profileKey ??
|
|
328
|
+
(record.owner
|
|
329
|
+
? Threads.getTelegramThreadOwnerKey(record.owner)
|
|
330
|
+
: undefined);
|
|
331
|
+
if (profileKey) deps.topicTargetStore.forgetIdentityByProfileKey(profileKey);
|
|
332
|
+
}
|
|
333
|
+
if (removed === 0) return 0;
|
|
334
|
+
const cursorRealigned =
|
|
335
|
+
Threads.reconcileTelegramFreshAllocationCursor(deps.topicTargetStore);
|
|
336
|
+
await deps.topicTargetStore.persist();
|
|
337
|
+
deps.recordRuntimeEvent(
|
|
338
|
+
"bus",
|
|
339
|
+
"Historical follower bindings removed from live state",
|
|
340
|
+
{
|
|
341
|
+
phase: "follower-binding-reality",
|
|
342
|
+
removed,
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
if (cursorRealigned) {
|
|
346
|
+
deps.recordRuntimeEvent(
|
|
347
|
+
"bus",
|
|
348
|
+
"Fresh allocation cursor realigned after live-state compaction",
|
|
349
|
+
{
|
|
350
|
+
phase: "follower-binding-reality-cursor",
|
|
351
|
+
lastSlot: deps.topicTargetStore.getBotState().lastSlot,
|
|
352
|
+
},
|
|
353
|
+
);
|
|
241
354
|
}
|
|
355
|
+
return removed;
|
|
242
356
|
};
|
|
243
357
|
}
|
|
244
358
|
|
|
@@ -338,6 +452,16 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
338
452
|
claimPendingTargets: false,
|
|
339
453
|
});
|
|
340
454
|
const recordsBeforeProvision = deps.topicTargetStore.list();
|
|
455
|
+
const reconnectRecord = registration.target
|
|
456
|
+
? recordsBeforeProvision.find((record) => {
|
|
457
|
+
return (
|
|
458
|
+
record.owner?.kind === "manual-follower" &&
|
|
459
|
+
record.instanceId === registration.instanceId &&
|
|
460
|
+
record.target.chatId === registration.target?.chatId &&
|
|
461
|
+
record.target.threadId === registration.target.threadId
|
|
462
|
+
);
|
|
463
|
+
})
|
|
464
|
+
: undefined;
|
|
341
465
|
const followerProfileKey =
|
|
342
466
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
343
467
|
const followerOwner =
|
|
@@ -367,7 +491,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
367
491
|
const runRegistration = async (): Promise<
|
|
368
492
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
369
493
|
> => {
|
|
370
|
-
let result =
|
|
494
|
+
let result = reconnectRecord
|
|
495
|
+
? { target: reconnectRecord.target, reused: true, record: reconnectRecord }
|
|
496
|
+
: await provisionTarget();
|
|
371
497
|
deps.setSyncState(
|
|
372
498
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
373
499
|
nowMs: getNowMs(),
|
|
@@ -943,12 +1069,32 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
943
1069
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
944
1070
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
945
1071
|
const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
|
|
1072
|
+
const followerRecoveryGraceMs = deps.followerRecoveryGraceMs ?? 5000;
|
|
946
1073
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
1074
|
+
let followerRealityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
947
1075
|
const stopPruning = () => {
|
|
948
1076
|
if (!pruneInterval) return;
|
|
949
1077
|
clearInterval(pruneInterval);
|
|
950
1078
|
pruneInterval = undefined;
|
|
951
1079
|
};
|
|
1080
|
+
const stopFollowerRealityTimer = () => {
|
|
1081
|
+
if (!followerRealityTimer) return;
|
|
1082
|
+
clearTimeout(followerRealityTimer);
|
|
1083
|
+
followerRealityTimer = undefined;
|
|
1084
|
+
};
|
|
1085
|
+
const scheduleFollowerBindingReality = () => {
|
|
1086
|
+
stopFollowerRealityTimer();
|
|
1087
|
+
if (!deps.reconcileFollowerBindings) return;
|
|
1088
|
+
followerRealityTimer = setTimeout(() => {
|
|
1089
|
+
followerRealityTimer = undefined;
|
|
1090
|
+
void Promise.resolve(deps.reconcileFollowerBindings?.()).catch((error) =>
|
|
1091
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1092
|
+
phase: "follower-binding-reality",
|
|
1093
|
+
}),
|
|
1094
|
+
);
|
|
1095
|
+
}, followerRecoveryGraceMs);
|
|
1096
|
+
followerRealityTimer.unref?.();
|
|
1097
|
+
};
|
|
952
1098
|
const pruneFollowers = async () => {
|
|
953
1099
|
const removed = deps.followerRegistry.pruneStale(
|
|
954
1100
|
getNowMs(),
|
|
@@ -997,25 +1143,21 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
997
1143
|
return {
|
|
998
1144
|
startPolling: async (ctx) => {
|
|
999
1145
|
await localServer.start();
|
|
1000
|
-
|
|
1001
|
-
await deps.restoreFollowerRegistry?.();
|
|
1002
|
-
} catch (error) {
|
|
1003
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
1004
|
-
phase: "follower-registry-restore",
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1146
|
+
scheduleFollowerBindingReality();
|
|
1007
1147
|
startPruning();
|
|
1008
1148
|
try {
|
|
1009
1149
|
await deps.provisionLeaderTarget?.(ctx);
|
|
1010
1150
|
await deps.startPolling(ctx);
|
|
1011
1151
|
} catch (error) {
|
|
1012
1152
|
stopPruning();
|
|
1153
|
+
stopFollowerRealityTimer();
|
|
1013
1154
|
await localServer.stop();
|
|
1014
1155
|
throw error;
|
|
1015
1156
|
}
|
|
1016
1157
|
},
|
|
1017
1158
|
stopPolling: async () => {
|
|
1018
1159
|
stopPruning();
|
|
1160
|
+
stopFollowerRealityTimer();
|
|
1019
1161
|
try {
|
|
1020
1162
|
await deps.stopPolling();
|
|
1021
1163
|
} finally {
|
package/lib/bus-transport.ts
CHANGED
|
@@ -99,31 +99,55 @@ export function getTelegramBusTransportRetryPolicy(input: {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function normalizeTelegramBusEndpointScope(value: string): string {
|
|
103
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
|
|
104
|
+
}
|
|
105
|
+
|
|
102
106
|
export function getTelegramBusLeaderEndpoint(input: {
|
|
103
107
|
agentDir: string;
|
|
104
108
|
platform: NodeJS.Platform | string;
|
|
109
|
+
profileName?: string;
|
|
105
110
|
}): string {
|
|
111
|
+
const profileScope = input.profileName
|
|
112
|
+
? normalizeTelegramBusEndpointScope(input.profileName)
|
|
113
|
+
: undefined;
|
|
106
114
|
return input.platform === "win32"
|
|
107
|
-
? getTelegramBusPipePath({
|
|
108
|
-
|
|
115
|
+
? getTelegramBusPipePath({
|
|
116
|
+
agentDir: input.agentDir,
|
|
117
|
+
scope: profileScope ? `bus-${profileScope}` : "bus",
|
|
118
|
+
})
|
|
119
|
+
: join(
|
|
120
|
+
input.agentDir,
|
|
121
|
+
"tmp",
|
|
122
|
+
"telegram",
|
|
123
|
+
profileScope ? `bus.${profileScope}.sock` : "bus.sock",
|
|
124
|
+
);
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
export function getTelegramBusFollowerEndpoint(input: {
|
|
112
128
|
agentDir: string;
|
|
113
129
|
platform: NodeJS.Platform | string;
|
|
114
130
|
instanceId: string;
|
|
131
|
+
profileName?: string;
|
|
115
132
|
}): string {
|
|
133
|
+
const instanceScope = normalizeTelegramBusEndpointScope(input.instanceId);
|
|
134
|
+
const profileScope = input.profileName
|
|
135
|
+
? normalizeTelegramBusEndpointScope(input.profileName)
|
|
136
|
+
: undefined;
|
|
116
137
|
return input.platform === "win32"
|
|
117
138
|
? getTelegramBusPipePath({
|
|
118
139
|
agentDir: input.agentDir,
|
|
119
|
-
scope:
|
|
140
|
+
scope: profileScope
|
|
141
|
+
? `follower-${profileScope}-${instanceScope}`
|
|
142
|
+
: `follower-${instanceScope}`,
|
|
120
143
|
})
|
|
121
144
|
: join(
|
|
122
145
|
input.agentDir,
|
|
123
146
|
"tmp",
|
|
124
147
|
"telegram",
|
|
125
148
|
"followers",
|
|
126
|
-
|
|
149
|
+
...(profileScope ? [profileScope] : []),
|
|
150
|
+
`${instanceScope}.sock`,
|
|
127
151
|
);
|
|
128
152
|
}
|
|
129
153
|
|