@llblab/pi-telegram 0.20.1 → 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 +3 -1
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/multi-instance-bus.md +5 -4
- package/index.ts +127 -211
- package/lib/bindings.ts +39 -13
- package/lib/bus-follower.ts +128 -2
- package/lib/bus-leader.ts +183 -42
- package/lib/bus.ts +34 -0
- 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/bindings.ts
CHANGED
|
@@ -94,7 +94,9 @@ export function registerTelegramCommandsAndTools({
|
|
|
94
94
|
sendMarkdownReply(chatId, undefined, markdown, options),
|
|
95
95
|
recordRuntimeEvent,
|
|
96
96
|
});
|
|
97
|
-
Prompts.registerTelegramHelpTool(pi
|
|
97
|
+
Prompts.registerTelegramHelpTool(pi, {
|
|
98
|
+
getActiveProfileName: configStore.getActiveProfileName,
|
|
99
|
+
});
|
|
98
100
|
Commands.registerTelegramBridgeCommands(pi, {
|
|
99
101
|
promptForConfig: async (ctx, profileName) => {
|
|
100
102
|
const nextProfileName = profileName ?? undefined;
|
|
@@ -103,36 +105,58 @@ export function registerTelegramCommandsAndTools({
|
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
105
107
|
const previousProfileName = configStore.getActiveProfileName();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
108
|
+
let setupConfigStore = configStore;
|
|
109
|
+
let persistSetupConfig = persistConfig;
|
|
109
110
|
if (!profileName) {
|
|
111
|
+
if (previousProfileName !== nextProfileName) {
|
|
112
|
+
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
113
|
+
}
|
|
110
114
|
configStore.activateProfile(undefined);
|
|
111
115
|
} else {
|
|
112
116
|
const storedConfig = configStore.getStoredConfig();
|
|
113
|
-
|
|
117
|
+
setupConfigStore = Config.createTelegramConfigStore({
|
|
118
|
+
initialConfig: {
|
|
119
|
+
...storedConfig,
|
|
120
|
+
profiles: {
|
|
121
|
+
...(storedConfig.profiles ?? {}),
|
|
122
|
+
[profileName]: storedConfig.profiles?.[profileName] ?? {
|
|
123
|
+
botToken: "",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
setupConfigStore.activateProfile(profileName);
|
|
129
|
+
persistSetupConfig = async () => {
|
|
130
|
+
if (previousProfileName !== profileName) {
|
|
131
|
+
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
132
|
+
}
|
|
133
|
+
configStore.activateProfile(undefined);
|
|
114
134
|
configStore.set({
|
|
115
135
|
...storedConfig,
|
|
116
136
|
profiles: {
|
|
117
137
|
...(storedConfig.profiles ?? {}),
|
|
118
|
-
[profileName]:
|
|
138
|
+
[profileName]: storedConfig.profiles?.[profileName] ?? {
|
|
139
|
+
botToken: "",
|
|
140
|
+
},
|
|
119
141
|
},
|
|
120
142
|
});
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
configStore.activateProfile(profileName);
|
|
144
|
+
configStore.set(setupConfigStore.get());
|
|
145
|
+
await persistConfig();
|
|
146
|
+
};
|
|
123
147
|
}
|
|
124
148
|
const runSetup = Setup.createTelegramSetupPromptRuntime({
|
|
125
|
-
getConfig:
|
|
126
|
-
setConfig:
|
|
149
|
+
getConfig: setupConfigStore.get,
|
|
150
|
+
setConfig: setupConfigStore.set,
|
|
127
151
|
setupGuard: setup,
|
|
128
152
|
getMe: TelegramApi.fetchTelegramBotIdentity,
|
|
129
|
-
persistConfig,
|
|
153
|
+
persistConfig: persistSetupConfig,
|
|
130
154
|
startPolling: lockedPollingRuntime.start,
|
|
131
155
|
updateStatus,
|
|
132
156
|
recordRuntimeEvent,
|
|
133
157
|
});
|
|
134
|
-
await runSetup(ctx);
|
|
135
|
-
if (profileName) {
|
|
158
|
+
const completion = await runSetup(ctx);
|
|
159
|
+
if (profileName && completion.status === "success") {
|
|
136
160
|
ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
|
|
137
161
|
}
|
|
138
162
|
},
|
|
@@ -155,6 +179,8 @@ export function registerTelegramCommandsAndTools({
|
|
|
155
179
|
activateProfileConfig: async (_ctx, profileName) => {
|
|
156
180
|
await configStore.load();
|
|
157
181
|
if (!Config.isValidTelegramProfileName(profileName)) return false;
|
|
182
|
+
const storedConfig = configStore.getStoredConfig();
|
|
183
|
+
if (!storedConfig.profiles?.[profileName]) return false;
|
|
158
184
|
const previousProfileName = configStore.getActiveProfileName();
|
|
159
185
|
if (previousProfileName !== profileName) {
|
|
160
186
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
package/lib/bus-follower.ts
CHANGED
|
@@ -173,6 +173,63 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
173
173
|
onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
|
|
174
174
|
}
|
|
175
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
|
+
|
|
176
233
|
export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
177
234
|
topicTargetStore: Pick<
|
|
178
235
|
Threads.TelegramTopicTargetStore,
|
|
@@ -183,7 +240,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
|
183
240
|
"getTarget" | "setRegistered"
|
|
184
241
|
>;
|
|
185
242
|
instanceId: string;
|
|
186
|
-
|
|
243
|
+
getManualFollowerProfileKey: () => string;
|
|
187
244
|
manualFollowerOwnerId: string;
|
|
188
245
|
getSyncState: () => Sync.TelegramSyncState;
|
|
189
246
|
setSyncState: (state: Sync.TelegramSyncState) => void;
|
|
@@ -276,6 +333,71 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
|
|
|
276
333
|
) => void;
|
|
277
334
|
}
|
|
278
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
|
+
|
|
279
401
|
export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
280
402
|
deps: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
|
|
281
403
|
): NonNullable<
|
|
@@ -308,7 +430,7 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
|
308
430
|
);
|
|
309
431
|
}
|
|
310
432
|
const profileKey =
|
|
311
|
-
currentRecord?.profileKey ?? deps.
|
|
433
|
+
currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
|
|
312
434
|
deps.topicTargetStore.upsert({
|
|
313
435
|
profileKey,
|
|
314
436
|
owner: {
|
|
@@ -654,6 +776,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
654
776
|
let activeLeaderSocketPath: string | undefined;
|
|
655
777
|
let activeAuthSecret: string | undefined;
|
|
656
778
|
let activeContext: TContext | undefined;
|
|
779
|
+
let lastKnownTarget: TelegramTarget | undefined;
|
|
657
780
|
const stopHeartbeat = () => {
|
|
658
781
|
if (!heartbeatInterval) return;
|
|
659
782
|
clearInterval(heartbeatInterval);
|
|
@@ -664,6 +787,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
664
787
|
activeAuthSecret = undefined;
|
|
665
788
|
deps.setActiveAuthSecret?.(undefined);
|
|
666
789
|
deps.registrationState?.setRegistered(false);
|
|
790
|
+
lastKnownTarget = undefined;
|
|
667
791
|
activeContext = undefined;
|
|
668
792
|
void deps.stopReceiving?.();
|
|
669
793
|
};
|
|
@@ -729,6 +853,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
729
853
|
(ctx.cwd ? basename(ctx.cwd) : undefined),
|
|
730
854
|
cwd: ctx.cwd,
|
|
731
855
|
pid: getPid(),
|
|
856
|
+
target: deps.registrationState?.getTarget() ?? lastKnownTarget,
|
|
732
857
|
busSocketPath:
|
|
733
858
|
deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
|
|
734
859
|
connectedAtMs: getNowMs(),
|
|
@@ -783,6 +908,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
783
908
|
registrationResult.target,
|
|
784
909
|
registrationResult,
|
|
785
910
|
);
|
|
911
|
+
lastKnownTarget = registrationResult.target;
|
|
786
912
|
activeLeaderSocketPath = leaderSocketPath;
|
|
787
913
|
activeContext = ctx;
|
|
788
914
|
await sendHeartbeat();
|
package/lib/bus-leader.ts
CHANGED
|
@@ -154,15 +154,105 @@ export interface TelegramBusLeaderApiProxyDeps {
|
|
|
154
154
|
) => Promise<unknown> | unknown;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
export interface
|
|
158
|
-
|
|
159
|
-
|
|
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">;
|
|
160
251
|
recordRuntimeEvent: (
|
|
161
252
|
category: string,
|
|
162
253
|
error: unknown,
|
|
163
254
|
details?: Record<string, unknown>,
|
|
164
255
|
) => void;
|
|
165
|
-
getNowMs?: () => number;
|
|
166
256
|
}
|
|
167
257
|
|
|
168
258
|
export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
@@ -192,11 +282,12 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
192
282
|
provisionFollowerTarget?: (
|
|
193
283
|
registration: TelegramBusInstanceRegistration,
|
|
194
284
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
195
|
-
|
|
285
|
+
reconcileFollowerBindings?: () => Promise<unknown> | unknown;
|
|
196
286
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
197
287
|
getNowMs?: () => number;
|
|
198
288
|
followerPruneIntervalMs?: number;
|
|
199
289
|
followerStaleAfterMs?: number;
|
|
290
|
+
followerRecoveryGraceMs?: number;
|
|
200
291
|
onFollowerPruned?: (
|
|
201
292
|
follower: TelegramBusFollowerView,
|
|
202
293
|
) => Promise<void> | void;
|
|
@@ -207,39 +298,61 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
207
298
|
) => void;
|
|
208
299
|
}
|
|
209
300
|
|
|
210
|
-
export function
|
|
211
|
-
deps:
|
|
212
|
-
): () => Promise<
|
|
301
|
+
export function createTelegramBusFollowerBindingRealityReconciler(
|
|
302
|
+
deps: TelegramBusFollowerBindingRealityDeps,
|
|
303
|
+
): () => Promise<number> {
|
|
213
304
|
return async () => {
|
|
214
305
|
await deps.topicTargetStore.load();
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
record.
|
|
222
|
-
record.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
connectedAtMs: nowMs,
|
|
234
|
-
});
|
|
235
|
-
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);
|
|
236
324
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
);
|
|
242
354
|
}
|
|
355
|
+
return removed;
|
|
243
356
|
};
|
|
244
357
|
}
|
|
245
358
|
|
|
@@ -339,6 +452,16 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
339
452
|
claimPendingTargets: false,
|
|
340
453
|
});
|
|
341
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;
|
|
342
465
|
const followerProfileKey =
|
|
343
466
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
344
467
|
const followerOwner =
|
|
@@ -368,7 +491,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
368
491
|
const runRegistration = async (): Promise<
|
|
369
492
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
370
493
|
> => {
|
|
371
|
-
let result =
|
|
494
|
+
let result = reconnectRecord
|
|
495
|
+
? { target: reconnectRecord.target, reused: true, record: reconnectRecord }
|
|
496
|
+
: await provisionTarget();
|
|
372
497
|
deps.setSyncState(
|
|
373
498
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
374
499
|
nowMs: getNowMs(),
|
|
@@ -944,12 +1069,32 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
944
1069
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
945
1070
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
946
1071
|
const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
|
|
1072
|
+
const followerRecoveryGraceMs = deps.followerRecoveryGraceMs ?? 5000;
|
|
947
1073
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
1074
|
+
let followerRealityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
948
1075
|
const stopPruning = () => {
|
|
949
1076
|
if (!pruneInterval) return;
|
|
950
1077
|
clearInterval(pruneInterval);
|
|
951
1078
|
pruneInterval = undefined;
|
|
952
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
|
+
};
|
|
953
1098
|
const pruneFollowers = async () => {
|
|
954
1099
|
const removed = deps.followerRegistry.pruneStale(
|
|
955
1100
|
getNowMs(),
|
|
@@ -998,25 +1143,21 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
998
1143
|
return {
|
|
999
1144
|
startPolling: async (ctx) => {
|
|
1000
1145
|
await localServer.start();
|
|
1001
|
-
|
|
1002
|
-
await deps.restoreFollowerRegistry?.();
|
|
1003
|
-
} catch (error) {
|
|
1004
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
1005
|
-
phase: "follower-registry-restore",
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1146
|
+
scheduleFollowerBindingReality();
|
|
1008
1147
|
startPruning();
|
|
1009
1148
|
try {
|
|
1010
1149
|
await deps.provisionLeaderTarget?.(ctx);
|
|
1011
1150
|
await deps.startPolling(ctx);
|
|
1012
1151
|
} catch (error) {
|
|
1013
1152
|
stopPruning();
|
|
1153
|
+
stopFollowerRealityTimer();
|
|
1014
1154
|
await localServer.stop();
|
|
1015
1155
|
throw error;
|
|
1016
1156
|
}
|
|
1017
1157
|
},
|
|
1018
1158
|
stopPolling: async () => {
|
|
1019
1159
|
stopPruning();
|
|
1160
|
+
stopFollowerRealityTimer();
|
|
1020
1161
|
try {
|
|
1021
1162
|
await deps.stopPolling();
|
|
1022
1163
|
} finally {
|
package/lib/bus.ts
CHANGED
|
@@ -34,6 +34,40 @@ import { resolveAgentDir } from "./paths.ts";
|
|
|
34
34
|
|
|
35
35
|
export type TelegramBusRole = "leader" | "follower";
|
|
36
36
|
|
|
37
|
+
export interface TelegramBusProcessRuntime {
|
|
38
|
+
instanceId: string;
|
|
39
|
+
manualFollowerOwnerId: string;
|
|
40
|
+
getLeaderSocketPath: () => string;
|
|
41
|
+
getFollowerSocketPath: () => string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createTelegramBusProcessRuntime(input: {
|
|
45
|
+
getActiveProfileName: () => string | undefined;
|
|
46
|
+
pid: number;
|
|
47
|
+
parentPid: number;
|
|
48
|
+
createdAtMs: number;
|
|
49
|
+
}): TelegramBusProcessRuntime {
|
|
50
|
+
const instanceId = `${input.pid}:${input.createdAtMs}`;
|
|
51
|
+
const manualFollowerOwnerId = String(input.parentPid || input.pid);
|
|
52
|
+
return {
|
|
53
|
+
instanceId,
|
|
54
|
+
manualFollowerOwnerId,
|
|
55
|
+
getLeaderSocketPath: () =>
|
|
56
|
+
getTelegramBusSocketPath(
|
|
57
|
+
undefined,
|
|
58
|
+
undefined,
|
|
59
|
+
input.getActiveProfileName(),
|
|
60
|
+
),
|
|
61
|
+
getFollowerSocketPath: () =>
|
|
62
|
+
getTelegramBusFollowerSocketPath(
|
|
63
|
+
instanceId,
|
|
64
|
+
undefined,
|
|
65
|
+
undefined,
|
|
66
|
+
input.getActiveProfileName(),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
export function createTelegramBusAuthSecret(): string {
|
|
38
72
|
return randomBytes(32).toString("base64url");
|
|
39
73
|
}
|
package/lib/paths.ts
CHANGED
|
@@ -71,6 +71,17 @@ export function resolveTelegramProfileTempFilePath(
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
|
|
75
|
+
state: string;
|
|
76
|
+
logs: string;
|
|
77
|
+
} {
|
|
78
|
+
const suffix = getTelegramProfilePathSuffix(profileName);
|
|
79
|
+
return {
|
|
80
|
+
state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
|
|
81
|
+
logs: `~/.pi/agent/tmp/telegram/logs${suffix}.jsonl`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
75
86
|
export function resolveTelegramRuntimeLogPath(): string {
|
|
76
87
|
return resolveTelegramProfileTempFilePath("logs", "jsonl");
|