@llblab/pi-telegram 0.52.1 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -8
- package/BACKLOG.md +1 -16
- package/CHANGELOG.md +17 -0
- package/README.md +8 -10
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +640 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +34 -3
- package/dist/lib/locks.js +318 -119
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +23 -37
- package/dist/lib/polling.js +71 -52
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +6 -3
- package/dist/lib/sync.js +112 -45
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +165 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +36 -30
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1207 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +703 -212
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +165 -149
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +488 -359
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +322 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/lib/sync.ts
CHANGED
|
@@ -4,19 +4,15 @@
|
|
|
4
4
|
* Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.ts";
|
|
8
7
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
|
-
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
|
-
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
11
|
-
import { normalizeTelegramWorkspacePath } from "./workspace-identity.ts";
|
|
12
8
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
getTelegramApiErrorRequestTarget,
|
|
10
|
+
isTelegramStaleTargetHttpError,
|
|
11
|
+
} from "./telegram-api.ts";
|
|
12
|
+
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
17
13
|
import {
|
|
18
|
-
createTelegramCleanupTargetProtection,
|
|
19
14
|
commitTelegramWorkspaceProvisionBinding,
|
|
15
|
+
createTelegramCleanupTargetProtection,
|
|
20
16
|
getTelegramTargetFromApiBody,
|
|
21
17
|
getTelegramThreadOwnerKey,
|
|
22
18
|
isSameTelegramProcessInstance,
|
|
@@ -27,6 +23,13 @@ import {
|
|
|
27
23
|
type TelegramWorkspaceDisplayBinding,
|
|
28
24
|
type TelegramWorkspaceThreadBinding,
|
|
29
25
|
} from "./threads.ts";
|
|
26
|
+
import {
|
|
27
|
+
createTelegramWorkspaceAdmissionOperationId,
|
|
28
|
+
runWithTelegramWorkspaceAdmissionsAsync,
|
|
29
|
+
type TelegramWorkspaceAdmissionLedger,
|
|
30
|
+
} from "./workspace-admission.ts";
|
|
31
|
+
import { normalizeTelegramWorkspacePath } from "./workspace-identity.ts";
|
|
32
|
+
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
30
33
|
|
|
31
34
|
export interface TelegramTopicLifecycleSyncUpdate<TMessage = unknown> {
|
|
32
35
|
kind: "created" | "closed" | "reopened";
|
|
@@ -176,12 +179,15 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
176
179
|
) => void;
|
|
177
180
|
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
178
181
|
workspaceOperationKind?: string;
|
|
182
|
+
isDisconnectCurrent?: () => boolean;
|
|
179
183
|
getNowMs?: () => number;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
function markTelegramConfigSyncChange<
|
|
183
|
-
TSyncState
|
|
184
|
-
|
|
186
|
+
function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(
|
|
187
|
+
state: TSyncState,
|
|
188
|
+
action: string,
|
|
189
|
+
options?: { nowMs?: number },
|
|
190
|
+
): TSyncState {
|
|
185
191
|
const nowMs = options?.nowMs ?? Date.now();
|
|
186
192
|
let nextState = markTelegramSyncSliceFresh(state, "pairing", {
|
|
187
193
|
nowMs,
|
|
@@ -200,8 +206,10 @@ function markTelegramConfigSyncChange<
|
|
|
200
206
|
|
|
201
207
|
export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
202
208
|
instanceId: string;
|
|
203
|
-
topicTargetStore: Pick<
|
|
204
|
-
|
|
209
|
+
topicTargetStore: Pick<
|
|
210
|
+
TelegramTopicTargetStore,
|
|
211
|
+
"load" | "list" | "listPendingCleanups" | "detachTargetOwner"
|
|
212
|
+
>;
|
|
205
213
|
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
206
214
|
getProfileName: () => string | undefined;
|
|
207
215
|
isPollingSuspended: () => boolean;
|
|
@@ -212,29 +220,60 @@ export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
|
212
220
|
const epoch = deps.getCurrentLeaderEpoch();
|
|
213
221
|
const profile = deps.getProfileName();
|
|
214
222
|
if (epoch === undefined || !isSessionCurrent()) return undefined;
|
|
215
|
-
const isCurrent = () =>
|
|
216
|
-
|
|
223
|
+
const isCurrent = () =>
|
|
224
|
+
isSessionCurrent() &&
|
|
225
|
+
deps.getCurrentLeaderEpoch() === epoch &&
|
|
226
|
+
deps.getProfileName() === profile &&
|
|
227
|
+
deps.isPollingSuspended();
|
|
217
228
|
return async () => {
|
|
218
|
-
if (
|
|
229
|
+
if (
|
|
230
|
+
!isCurrent() ||
|
|
231
|
+
(await deps.resolveAutomaticThreadCleanupEnabled()) !== false
|
|
232
|
+
)
|
|
233
|
+
return;
|
|
219
234
|
if (!isCurrent()) return;
|
|
220
|
-
await deps.runWorkspaceOperation(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
await deps.runWorkspaceOperation(
|
|
236
|
+
{
|
|
237
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
238
|
+
operationKind: "workspace.preserve-leader-quit",
|
|
239
|
+
scopes: [{ kind: "profile" }],
|
|
240
|
+
},
|
|
241
|
+
async () => {
|
|
242
|
+
if (!isCurrent()) return;
|
|
243
|
+
await deps.topicTargetStore.load();
|
|
244
|
+
if (!isCurrent()) return;
|
|
245
|
+
const records = deps.topicTargetStore
|
|
246
|
+
.list()
|
|
247
|
+
.filter(
|
|
248
|
+
(record) =>
|
|
249
|
+
record.instanceId === deps.instanceId &&
|
|
250
|
+
(record.status === "active" || record.status === "starting"),
|
|
251
|
+
);
|
|
252
|
+
if (records.length !== 1) return;
|
|
253
|
+
const record = records[0]!;
|
|
254
|
+
if (
|
|
255
|
+
deps.topicTargetStore
|
|
256
|
+
.listPendingCleanups()
|
|
257
|
+
.some(
|
|
258
|
+
(intent) =>
|
|
259
|
+
intent.target.chatId === record.target.chatId &&
|
|
260
|
+
intent.target.threadId === record.target.threadId,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
return;
|
|
264
|
+
if (
|
|
265
|
+
!(await deps.topicTargetStore.detachTargetOwner(
|
|
266
|
+
record,
|
|
267
|
+
isCurrent,
|
|
268
|
+
)) &&
|
|
269
|
+
isCurrent()
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"Telegram preserved leader detachment was not committed.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
);
|
|
238
277
|
};
|
|
239
278
|
};
|
|
240
279
|
}
|
|
@@ -270,27 +309,71 @@ export function createTelegramThreadDisconnectAssembly<
|
|
|
270
309
|
>(
|
|
271
310
|
deps: Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> & {
|
|
272
311
|
stopPolling: () => Promise<string>;
|
|
312
|
+
captureStopPolling?: () => {
|
|
313
|
+
isCurrent: () => boolean;
|
|
314
|
+
stop: () => Promise<string>;
|
|
315
|
+
};
|
|
273
316
|
suspendPolling: () => Promise<void>;
|
|
274
317
|
},
|
|
275
318
|
): TelegramThreadDisconnectAssembly {
|
|
276
319
|
return {
|
|
277
|
-
disconnect
|
|
320
|
+
async disconnect() {
|
|
321
|
+
const captured = deps.captureStopPolling?.();
|
|
322
|
+
const stop = captured?.stop ?? deps.stopPolling;
|
|
323
|
+
let stopAttempted = false;
|
|
324
|
+
const disconnect = createTelegramManualThreadDisconnectHandler({
|
|
325
|
+
...deps,
|
|
326
|
+
workspaceOperationKind: "workspace.disconnect-thread",
|
|
327
|
+
isDisconnectCurrent: captured?.isCurrent ?? deps.isDisconnectCurrent,
|
|
328
|
+
stopPolling() {
|
|
329
|
+
stopAttempted = true;
|
|
330
|
+
return stop();
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
try {
|
|
334
|
+
return await disconnect();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
// Never retry a stop whose teardown/release outcome may already have committed.
|
|
337
|
+
if (stopAttempted) {
|
|
338
|
+
deps.recordRuntimeEvent("connection", error, {
|
|
339
|
+
phase: "disconnect-stop",
|
|
340
|
+
});
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
deps.recordRuntimeEvent("connection", error, {
|
|
344
|
+
phase: "disconnect-cleanup",
|
|
345
|
+
});
|
|
346
|
+
let stopped: string;
|
|
347
|
+
try {
|
|
348
|
+
stopped = await stop();
|
|
349
|
+
} catch (stopError) {
|
|
350
|
+
deps.recordRuntimeEvent("connection", stopError, {
|
|
351
|
+
phase: "disconnect-stop",
|
|
352
|
+
});
|
|
353
|
+
throw stopError;
|
|
354
|
+
}
|
|
355
|
+
const separator = /[.!?]$/u.test(stopped) ? " " : ". ";
|
|
356
|
+
return `${stopped}${separator}Thread cleanup was not confirmed. Check /telegram-status --debug.`;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
cleanupForSessionRestart: createTelegramSessionRestartThreadCleanupHandler({
|
|
278
360
|
...deps,
|
|
279
|
-
|
|
280
|
-
stopPolling: deps.stopPolling,
|
|
361
|
+
suspendPolling: deps.suspendPolling,
|
|
281
362
|
}),
|
|
282
|
-
cleanupForSessionRestart:
|
|
283
|
-
createTelegramSessionRestartThreadCleanupHandler({
|
|
284
|
-
...deps,
|
|
285
|
-
suspendPolling: deps.suspendPolling,
|
|
286
|
-
}),
|
|
287
363
|
};
|
|
288
364
|
}
|
|
289
365
|
|
|
290
366
|
export function createTelegramManualThreadDisconnectHandler<
|
|
291
367
|
TSyncState extends TelegramSyncState,
|
|
292
368
|
>(deps: TelegramManualThreadDisconnectDeps<TSyncState>): () => Promise<string> {
|
|
369
|
+
const assertCurrent = () => {
|
|
370
|
+
if (deps.isDisconnectCurrent?.() === false)
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Telegram disconnect was superseded by a new connection.",
|
|
373
|
+
);
|
|
374
|
+
};
|
|
293
375
|
const operation = async (): Promise<string> => {
|
|
376
|
+
assertCurrent();
|
|
294
377
|
const currentRecord = deps.getCurrentThreadRecord();
|
|
295
378
|
let cleanupPending = false;
|
|
296
379
|
if (currentRecord?.target.threadId) {
|
|
@@ -302,6 +385,7 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
302
385
|
if (isManualFollower && !ownsLeader) {
|
|
303
386
|
if (deps.disconnectFollowerThread) {
|
|
304
387
|
const disconnected = await deps.disconnectFollowerThread();
|
|
388
|
+
assertCurrent();
|
|
305
389
|
if (!disconnected) {
|
|
306
390
|
throw new Error(
|
|
307
391
|
"Telegram follower thread deletion requires a live leader registration.",
|
|
@@ -324,11 +408,21 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
324
408
|
target,
|
|
325
409
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
326
410
|
};
|
|
327
|
-
const departingRecord = deps.topicTargetStore
|
|
328
|
-
|
|
329
|
-
|
|
411
|
+
const departingRecord = deps.topicTargetStore
|
|
412
|
+
.list()
|
|
413
|
+
.find(
|
|
414
|
+
(record) =>
|
|
415
|
+
record.instanceId === currentRecord.instanceId &&
|
|
416
|
+
record.target.chatId === target.chatId &&
|
|
417
|
+
record.target.threadId === target.threadId,
|
|
418
|
+
);
|
|
419
|
+
const isCleanupTargetProtected = createTelegramCleanupTargetProtection(
|
|
420
|
+
deps.topicTargetStore,
|
|
421
|
+
departingRecord,
|
|
422
|
+
);
|
|
330
423
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
331
424
|
await deps.topicTargetStore.persist();
|
|
425
|
+
assertCurrent();
|
|
332
426
|
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
333
427
|
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
334
428
|
currentLeaderEpoch: leaderEpoch,
|
|
@@ -340,9 +434,11 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
340
434
|
{
|
|
341
435
|
isCleanupTargetProtected,
|
|
342
436
|
callApi(method, body) {
|
|
437
|
+
assertCurrent();
|
|
343
438
|
return deps.callApi(method, body);
|
|
344
439
|
},
|
|
345
440
|
markStaleByTarget(targetToMark, syncStatus, lastSyncError) {
|
|
441
|
+
assertCurrent();
|
|
346
442
|
return deps.topicTargetStore.markStaleByTarget(
|
|
347
443
|
targetToMark,
|
|
348
444
|
syncStatus,
|
|
@@ -350,16 +446,23 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
350
446
|
);
|
|
351
447
|
},
|
|
352
448
|
removeCleanupIntentById(id) {
|
|
449
|
+
assertCurrent();
|
|
353
450
|
return deps.topicTargetStore.removePendingCleanup(id);
|
|
354
451
|
},
|
|
355
452
|
persist() {
|
|
453
|
+
assertCurrent();
|
|
356
454
|
return deps.topicTargetStore.persist();
|
|
357
455
|
},
|
|
358
456
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
359
457
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
360
458
|
},
|
|
361
459
|
);
|
|
362
|
-
|
|
460
|
+
assertCurrent();
|
|
461
|
+
if (
|
|
462
|
+
cleanupPlan.actions.some((action) =>
|
|
463
|
+
isCleanupTargetProtected(action.target, action),
|
|
464
|
+
)
|
|
465
|
+
) {
|
|
363
466
|
return "Thread disconnect superseded by a new binding.";
|
|
364
467
|
}
|
|
365
468
|
cleanupPending = Boolean(cleanup.incompleteActions?.length);
|
|
@@ -378,19 +481,22 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
378
481
|
}) as TSyncState,
|
|
379
482
|
);
|
|
380
483
|
}
|
|
484
|
+
assertCurrent();
|
|
381
485
|
const stopped = await deps.stopPolling();
|
|
382
486
|
return cleanupPending
|
|
383
487
|
? `${stopped} Telegram thread cleanup remains pending for the next leader.`
|
|
384
488
|
: stopped;
|
|
385
489
|
};
|
|
386
|
-
return () =>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
490
|
+
return () =>
|
|
491
|
+
deps.runWorkspaceOperation(
|
|
492
|
+
{
|
|
493
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
494
|
+
operationKind:
|
|
495
|
+
deps.workspaceOperationKind ?? "workspace.disconnect-thread",
|
|
496
|
+
scopes: [{ kind: "profile" }],
|
|
497
|
+
},
|
|
498
|
+
operation,
|
|
499
|
+
);
|
|
394
500
|
}
|
|
395
501
|
|
|
396
502
|
export function createTelegramLeaderHealthRuntime<
|
|
@@ -497,7 +603,8 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
497
603
|
topicTargetStore: Pick<
|
|
498
604
|
TelegramTopicTargetStore,
|
|
499
605
|
"load" | "markStaleByTarget" | "persist"
|
|
500
|
-
> &
|
|
606
|
+
> &
|
|
607
|
+
Partial<Pick<TelegramTopicTargetStore, "invalidateTarget">>;
|
|
501
608
|
getSyncState: () => TSyncState;
|
|
502
609
|
setSyncState: (state: TSyncState) => void;
|
|
503
610
|
recordEvent: (
|
|
@@ -508,16 +615,23 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
508
615
|
getNowMs?: () => number;
|
|
509
616
|
isCurrent?: () => boolean;
|
|
510
617
|
isAuthorityCurrent?: () => boolean;
|
|
511
|
-
getWorkspaceAdmission?: () =>
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
618
|
+
getWorkspaceAdmission?: () =>
|
|
619
|
+
| Pick<
|
|
620
|
+
TelegramWorkspaceAdmissionLedger,
|
|
621
|
+
"acquireAdmission" | "releaseAdmission"
|
|
622
|
+
>
|
|
623
|
+
| undefined;
|
|
515
624
|
}
|
|
516
625
|
|
|
517
|
-
export function captureTelegramStaleTargetRequestRecovery<
|
|
626
|
+
export function captureTelegramStaleTargetRequestRecovery<
|
|
627
|
+
TSyncState extends TelegramSyncState,
|
|
628
|
+
>(
|
|
518
629
|
body: Record<string, unknown>,
|
|
519
630
|
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> & {
|
|
520
|
-
topicTargetStore: Pick<
|
|
631
|
+
topicTargetStore: Pick<
|
|
632
|
+
TelegramTopicTargetStore,
|
|
633
|
+
"load" | "list" | "markStaleByTarget" | "persist" | "invalidateTarget"
|
|
634
|
+
>;
|
|
521
635
|
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
522
636
|
getSessionGeneration: () => number;
|
|
523
637
|
getProfileName: () => string | undefined;
|
|
@@ -528,25 +642,44 @@ export function captureTelegramStaleTargetRequestRecovery<TSyncState extends Tel
|
|
|
528
642
|
const epoch = deps.getCurrentLeaderEpoch();
|
|
529
643
|
if (!target || epoch === undefined) return undefined;
|
|
530
644
|
const key = getTelegramTargetKey(target);
|
|
531
|
-
const record = deps.topicTargetStore
|
|
645
|
+
const record = deps.topicTargetStore
|
|
646
|
+
.list()
|
|
647
|
+
.find((candidate) => getTelegramTargetKey(candidate.target) === key);
|
|
532
648
|
if (!record) return undefined;
|
|
533
649
|
const generation = deps.getSessionGeneration();
|
|
534
650
|
const profile = deps.getProfileName();
|
|
535
|
-
const isAuthorityCurrent = (): boolean =>
|
|
536
|
-
deps.
|
|
651
|
+
const isAuthorityCurrent = (): boolean =>
|
|
652
|
+
deps.getCurrentLeaderEpoch() === epoch &&
|
|
653
|
+
deps.getSessionGeneration() === generation &&
|
|
654
|
+
deps.getProfileName() === profile;
|
|
537
655
|
const isCurrent = (): boolean => {
|
|
538
|
-
const current = deps.topicTargetStore
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
656
|
+
const current = deps.topicTargetStore
|
|
657
|
+
.list()
|
|
658
|
+
.find((candidate) => getTelegramTargetKey(candidate.target) === key);
|
|
659
|
+
return (
|
|
660
|
+
isAuthorityCurrent() &&
|
|
661
|
+
current?.instanceId === record.instanceId &&
|
|
662
|
+
current?.profileKey === record.profileKey &&
|
|
663
|
+
current?.updatedAtMs === record.updatedAtMs &&
|
|
664
|
+
current?.createdAtMs === record.createdAtMs
|
|
665
|
+
);
|
|
542
666
|
};
|
|
543
667
|
return async (error) => {
|
|
544
668
|
const requestTarget = getTelegramApiErrorRequestTarget(error);
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
669
|
+
if (
|
|
670
|
+
!isTelegramStaleTargetHttpError(error) ||
|
|
671
|
+
!requestTarget ||
|
|
672
|
+
getTelegramTargetKey(requestTarget) !== key ||
|
|
673
|
+
!isCurrent()
|
|
674
|
+
)
|
|
675
|
+
return;
|
|
676
|
+
if (
|
|
677
|
+
await recoverStaleTelegramTopicApiError(
|
|
678
|
+
{ chat_id: target.chatId, message_thread_id: target.threadId },
|
|
679
|
+
error,
|
|
680
|
+
{ ...deps, isCurrent, isAuthorityCurrent },
|
|
681
|
+
)
|
|
682
|
+
) {
|
|
550
683
|
deps.onRecovered();
|
|
551
684
|
}
|
|
552
685
|
};
|
|
@@ -589,17 +722,19 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
589
722
|
!target ||
|
|
590
723
|
!isTelegramTopicTargetStaleError(error) ||
|
|
591
724
|
deps.isCurrent?.() === false
|
|
592
|
-
)
|
|
725
|
+
)
|
|
726
|
+
return false;
|
|
593
727
|
const recover = async (): Promise<boolean> => {
|
|
594
728
|
if (deps.isCurrent) {
|
|
595
729
|
if (
|
|
596
730
|
!deps.topicTargetStore.invalidateTarget ||
|
|
597
|
-
!await deps.topicTargetStore.invalidateTarget(
|
|
731
|
+
!(await deps.topicTargetStore.invalidateTarget(
|
|
598
732
|
target,
|
|
599
733
|
deps.isCurrent,
|
|
600
734
|
String(error),
|
|
601
|
-
)
|
|
602
|
-
)
|
|
735
|
+
))
|
|
736
|
+
)
|
|
737
|
+
return false;
|
|
603
738
|
if (deps.isAuthorityCurrent?.() === false) return false;
|
|
604
739
|
} else {
|
|
605
740
|
await deps.topicTargetStore.load();
|
|
@@ -609,7 +744,8 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
609
744
|
"deleted",
|
|
610
745
|
String(error),
|
|
611
746
|
)
|
|
612
|
-
)
|
|
747
|
+
)
|
|
748
|
+
return false;
|
|
613
749
|
await deps.topicTargetStore.persist();
|
|
614
750
|
}
|
|
615
751
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
@@ -698,7 +834,12 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
698
834
|
normalizedLeaderCwd,
|
|
699
835
|
deps.instanceId,
|
|
700
836
|
legacyLeaderRecord?.instanceId,
|
|
701
|
-
{
|
|
837
|
+
{
|
|
838
|
+
sessionId: deps.sessionId,
|
|
839
|
+
onCapacityUnavailable() {
|
|
840
|
+
capacityUnavailable = true;
|
|
841
|
+
},
|
|
842
|
+
},
|
|
702
843
|
)
|
|
703
844
|
: undefined;
|
|
704
845
|
if (deps.cwd && !workspaceIdentity) {
|
|
@@ -728,270 +869,290 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
728
869
|
assertLeaderEpoch("before-workspace-persist");
|
|
729
870
|
await deps.topicTargetStore.persist();
|
|
730
871
|
assertLeaderEpoch("after-workspace-persist");
|
|
731
|
-
const {
|
|
872
|
+
const {
|
|
873
|
+
target: _target,
|
|
874
|
+
slot: _slot,
|
|
875
|
+
threadName: _threadName,
|
|
876
|
+
displayTitle: _displayTitle,
|
|
877
|
+
...rest
|
|
878
|
+
} = result;
|
|
732
879
|
return {
|
|
733
880
|
...rest,
|
|
734
881
|
target: { ...committed.target },
|
|
735
882
|
slot: committed.slot ?? result.slot,
|
|
736
883
|
...(committed.threadName ? { threadName: committed.threadName } : {}),
|
|
737
|
-
...(committed.displayTitle
|
|
884
|
+
...(committed.displayTitle
|
|
885
|
+
? { displayTitle: committed.displayTitle }
|
|
886
|
+
: {}),
|
|
738
887
|
};
|
|
739
888
|
};
|
|
740
889
|
try {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
890
|
+
const unavailableTargetKeys = new Set([
|
|
891
|
+
...deps.topicTargetStore
|
|
892
|
+
.listSyncObservations()
|
|
893
|
+
.filter((observation) => observation.syncStatus === "deleted")
|
|
894
|
+
.map((observation) => getTelegramTargetKey(observation.target)),
|
|
895
|
+
...deps.topicTargetStore
|
|
896
|
+
.listPendingCleanups()
|
|
897
|
+
.map((intent) => getTelegramTargetKey(intent.target)),
|
|
898
|
+
]);
|
|
899
|
+
let invalidatedUnavailableTarget = false;
|
|
900
|
+
for (const record of deps.topicTargetStore.list()) {
|
|
901
|
+
if (
|
|
902
|
+
record.instanceId !== deps.instanceId ||
|
|
903
|
+
!unavailableTargetKeys.has(getTelegramTargetKey(record.target))
|
|
904
|
+
) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
invalidatedUnavailableTarget =
|
|
908
|
+
deps.topicTargetStore.markStaleByTarget(record.target) ||
|
|
909
|
+
invalidatedUnavailableTarget;
|
|
910
|
+
}
|
|
911
|
+
if (invalidatedUnavailableTarget) {
|
|
912
|
+
assertLeaderEpoch("before-unavailable-persist");
|
|
913
|
+
await deps.topicTargetStore.persist();
|
|
914
|
+
assertLeaderEpoch("after-unavailable-persist");
|
|
915
|
+
}
|
|
916
|
+
const persistedWorkspaceBinding = workspaceIdentity
|
|
917
|
+
? deps.topicTargetStore.getWorkspaceBinding(
|
|
918
|
+
workspaceIdentity.cwd,
|
|
919
|
+
workspaceIdentity.instanceSlot,
|
|
920
|
+
workspaceIdentity.sessionId,
|
|
921
|
+
)
|
|
922
|
+
: undefined;
|
|
923
|
+
const legacyWorkspaceBinding =
|
|
924
|
+
workspaceIdentity?.instanceSlot === "a" &&
|
|
925
|
+
deps.sessionId === undefined &&
|
|
926
|
+
!persistedWorkspaceBinding &&
|
|
927
|
+
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
928
|
+
? {
|
|
929
|
+
...workspaceIdentity,
|
|
930
|
+
target: { ...legacyLeaderRecord.target },
|
|
931
|
+
...(legacyLeaderRecord.threadName
|
|
932
|
+
? { threadName: legacyLeaderRecord.threadName }
|
|
933
|
+
: {}),
|
|
934
|
+
...(legacyLeaderRecord.slot
|
|
935
|
+
? { slot: legacyLeaderRecord.slot }
|
|
936
|
+
: {}),
|
|
937
|
+
updatedAtMs: legacyLeaderRecord.updatedAtMs,
|
|
938
|
+
}
|
|
939
|
+
: undefined;
|
|
940
|
+
const recoverableWorkspaceBinding =
|
|
941
|
+
persistedWorkspaceBinding ?? legacyWorkspaceBinding;
|
|
942
|
+
const recoverableTargetKey = recoverableWorkspaceBinding
|
|
943
|
+
? getTelegramTargetKey(recoverableWorkspaceBinding.target)
|
|
944
|
+
: undefined;
|
|
945
|
+
const recoverableRecord = recoverableWorkspaceBinding
|
|
946
|
+
? deps.topicTargetStore
|
|
947
|
+
.list()
|
|
948
|
+
.find(
|
|
949
|
+
(record) =>
|
|
950
|
+
getTelegramTargetKey(record.target) === recoverableTargetKey,
|
|
951
|
+
)
|
|
952
|
+
: undefined;
|
|
953
|
+
const sameProcessWorkspaceBinding =
|
|
954
|
+
!!recoverableRecord &&
|
|
955
|
+
(recoverableRecord.instanceId === deps.instanceId ||
|
|
956
|
+
isSameTelegramProcessInstance(
|
|
957
|
+
recoverableRecord.instanceId,
|
|
958
|
+
deps.instanceId,
|
|
959
|
+
));
|
|
752
960
|
if (
|
|
753
|
-
|
|
754
|
-
!unavailableTargetKeys.has(
|
|
961
|
+
recoverableWorkspaceBinding &&
|
|
962
|
+
!unavailableTargetKeys.has(recoverableTargetKey ?? "") &&
|
|
963
|
+
(!deps.forceFreshUnnamed || recoverableWorkspaceBinding.threadName) &&
|
|
964
|
+
(sameProcessWorkspaceBinding || deps.probeWorkspaceBinding)
|
|
755
965
|
) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
!persistedWorkspaceBinding &&
|
|
778
|
-
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
779
|
-
? {
|
|
780
|
-
...workspaceIdentity,
|
|
781
|
-
target: { ...legacyLeaderRecord.target },
|
|
782
|
-
...(legacyLeaderRecord.threadName
|
|
783
|
-
? { threadName: legacyLeaderRecord.threadName }
|
|
784
|
-
: {}),
|
|
785
|
-
...(legacyLeaderRecord.slot ? { slot: legacyLeaderRecord.slot } : {}),
|
|
786
|
-
updatedAtMs: legacyLeaderRecord.updatedAtMs,
|
|
966
|
+
let workspaceTargetVisible = sameProcessWorkspaceBinding;
|
|
967
|
+
if (!workspaceTargetVisible && deps.probeWorkspaceBinding) {
|
|
968
|
+
assertLeaderEpoch("before-workspace-probe");
|
|
969
|
+
try {
|
|
970
|
+
await deps.probeWorkspaceBinding(recoverableWorkspaceBinding);
|
|
971
|
+
assertLeaderEpoch("after-workspace-probe");
|
|
972
|
+
workspaceTargetVisible = true;
|
|
973
|
+
} catch (error) {
|
|
974
|
+
if (!isTelegramTopicTargetStaleError(error)) throw error;
|
|
975
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
976
|
+
recoverableWorkspaceBinding.target,
|
|
977
|
+
"deleted",
|
|
978
|
+
error instanceof Error ? error.message : String(error),
|
|
979
|
+
);
|
|
980
|
+
await deps.topicTargetStore.persist();
|
|
981
|
+
deps.recordEvent("bus", error, {
|
|
982
|
+
phase: "leader-workspace-target-stale",
|
|
983
|
+
chatId: recoverableWorkspaceBinding.target.chatId,
|
|
984
|
+
threadId: recoverableWorkspaceBinding.target.threadId,
|
|
985
|
+
instanceId: deps.instanceId,
|
|
986
|
+
});
|
|
787
987
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
!unavailableTargetKeys.has(recoverableTargetKey ?? "") &&
|
|
812
|
-
(!deps.forceFreshUnnamed || recoverableWorkspaceBinding.threadName) &&
|
|
813
|
-
(sameProcessWorkspaceBinding || deps.probeWorkspaceBinding)
|
|
814
|
-
) {
|
|
815
|
-
let workspaceTargetVisible = sameProcessWorkspaceBinding;
|
|
816
|
-
if (!workspaceTargetVisible && deps.probeWorkspaceBinding) {
|
|
817
|
-
assertLeaderEpoch("before-workspace-probe");
|
|
818
|
-
try {
|
|
819
|
-
await deps.probeWorkspaceBinding(recoverableWorkspaceBinding);
|
|
820
|
-
assertLeaderEpoch("after-workspace-probe");
|
|
821
|
-
workspaceTargetVisible = true;
|
|
822
|
-
} catch (error) {
|
|
823
|
-
if (!isTelegramTopicTargetStaleError(error)) throw error;
|
|
824
|
-
deps.topicTargetStore.markStaleByTarget(
|
|
825
|
-
recoverableWorkspaceBinding.target,
|
|
826
|
-
"deleted",
|
|
827
|
-
error instanceof Error ? error.message : String(error),
|
|
828
|
-
);
|
|
829
|
-
await deps.topicTargetStore.persist();
|
|
830
|
-
deps.recordEvent("bus", error, {
|
|
831
|
-
phase: "leader-workspace-target-stale",
|
|
832
|
-
chatId: recoverableWorkspaceBinding.target.chatId,
|
|
833
|
-
threadId: recoverableWorkspaceBinding.target.threadId,
|
|
988
|
+
}
|
|
989
|
+
if (workspaceTargetVisible) {
|
|
990
|
+
const nowMs = deps.getNowMs?.() ?? Date.now();
|
|
991
|
+
const slot = workspaceIdentity?.slot;
|
|
992
|
+
if (!slot) {
|
|
993
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
994
|
+
}
|
|
995
|
+
const recovered = await commitWorkspaceBinding({
|
|
996
|
+
target: { ...recoverableWorkspaceBinding.target },
|
|
997
|
+
slot,
|
|
998
|
+
...(recoverableWorkspaceBinding.threadName
|
|
999
|
+
? { threadName: recoverableWorkspaceBinding.threadName }
|
|
1000
|
+
: {}),
|
|
1001
|
+
reused: true,
|
|
1002
|
+
});
|
|
1003
|
+
deps.topicTargetStore.upsert({
|
|
1004
|
+
profileKey: leaderProfileKey,
|
|
1005
|
+
owner: leaderOwner,
|
|
1006
|
+
target: { ...recovered.target },
|
|
1007
|
+
status: "active",
|
|
1008
|
+
createdAtMs: recoverableRecord?.createdAtMs ?? nowMs,
|
|
1009
|
+
updatedAtMs: nowMs,
|
|
1010
|
+
...(recovered.threadName ? { threadName: recovered.threadName } : {}),
|
|
834
1011
|
instanceId: deps.instanceId,
|
|
1012
|
+
slot: recovered.slot,
|
|
1013
|
+
syncStatus: "open",
|
|
1014
|
+
lastSyncObservedAtMs: nowMs,
|
|
1015
|
+
lastReconcileAction: "leader-workspace-binding-recovered",
|
|
835
1016
|
});
|
|
1017
|
+
assertLeaderEpoch("before-recovered-record-persist");
|
|
1018
|
+
await deps.topicTargetStore.persist();
|
|
1019
|
+
assertLeaderEpoch("after-recovered-record-persist");
|
|
1020
|
+
deps.recordEvent(
|
|
1021
|
+
"telegram",
|
|
1022
|
+
"Leader thread preserved after Workspace recovery",
|
|
1023
|
+
{
|
|
1024
|
+
phase: "leader-thread-reused",
|
|
1025
|
+
instanceId: deps.instanceId,
|
|
1026
|
+
chatId: recovered.target.chatId,
|
|
1027
|
+
threadId: recovered.target.threadId,
|
|
1028
|
+
slot: recovered.slot,
|
|
1029
|
+
},
|
|
1030
|
+
);
|
|
1031
|
+
return recovered;
|
|
836
1032
|
}
|
|
837
1033
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
deps.topicTargetStore.upsert({
|
|
853
|
-
profileKey: leaderProfileKey,
|
|
854
|
-
owner: leaderOwner,
|
|
855
|
-
target: { ...recovered.target },
|
|
856
|
-
status: "active",
|
|
857
|
-
createdAtMs: recoverableRecord?.createdAtMs ?? nowMs,
|
|
858
|
-
updatedAtMs: nowMs,
|
|
859
|
-
...(recovered.threadName ? { threadName: recovered.threadName } : {}),
|
|
860
|
-
instanceId: deps.instanceId,
|
|
861
|
-
slot: recovered.slot,
|
|
862
|
-
syncStatus: "open",
|
|
863
|
-
lastSyncObservedAtMs: nowMs,
|
|
864
|
-
lastReconcileAction: "leader-workspace-binding-recovered",
|
|
865
|
-
});
|
|
866
|
-
assertLeaderEpoch("before-recovered-record-persist");
|
|
867
|
-
await deps.topicTargetStore.persist();
|
|
868
|
-
assertLeaderEpoch("after-recovered-record-persist");
|
|
1034
|
+
const priorTargets = deps.topicTargetStore.list().filter((record) => {
|
|
1035
|
+
return (
|
|
1036
|
+
record.instanceId === deps.instanceId &&
|
|
1037
|
+
(record.status === "active" || record.status === "starting")
|
|
1038
|
+
);
|
|
1039
|
+
});
|
|
1040
|
+
// Legacy callers without session identity may reuse the active instance target.
|
|
1041
|
+
// Session-aware callers must resolve their exact Workspace binding instead.
|
|
1042
|
+
if (
|
|
1043
|
+
deps.sessionId === undefined &&
|
|
1044
|
+
!deps.forceFreshUnnamed &&
|
|
1045
|
+
priorTargets.length > 0
|
|
1046
|
+
) {
|
|
1047
|
+
const record = priorTargets[0];
|
|
869
1048
|
deps.recordEvent(
|
|
870
1049
|
"telegram",
|
|
871
|
-
"Leader thread preserved after
|
|
1050
|
+
"Leader thread preserved after session lifecycle change",
|
|
872
1051
|
{
|
|
873
1052
|
phase: "leader-thread-reused",
|
|
874
1053
|
instanceId: deps.instanceId,
|
|
875
|
-
chatId:
|
|
876
|
-
threadId:
|
|
877
|
-
slot:
|
|
1054
|
+
chatId: record.target.chatId,
|
|
1055
|
+
threadId: record.target.threadId,
|
|
1056
|
+
slot: record.slot,
|
|
878
1057
|
},
|
|
879
1058
|
);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
(record.status === "active" || record.status === "starting")
|
|
887
|
-
);
|
|
888
|
-
});
|
|
889
|
-
// Legacy callers without session identity may reuse the active instance target.
|
|
890
|
-
// Session-aware callers must resolve their exact Workspace binding instead.
|
|
891
|
-
if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
892
|
-
const record = priorTargets[0];
|
|
893
|
-
deps.recordEvent(
|
|
894
|
-
"telegram",
|
|
895
|
-
"Leader thread preserved after session lifecycle change",
|
|
896
|
-
{
|
|
897
|
-
phase: "leader-thread-reused",
|
|
898
|
-
instanceId: deps.instanceId,
|
|
899
|
-
chatId: record.target.chatId,
|
|
900
|
-
threadId: record.target.threadId,
|
|
901
|
-
slot: record.slot,
|
|
902
|
-
},
|
|
903
|
-
);
|
|
904
|
-
assertLeaderEpoch("before-reuse");
|
|
905
|
-
if (!record.slot) {
|
|
906
|
-
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
907
|
-
}
|
|
908
|
-
return await commitWorkspaceBinding({
|
|
909
|
-
target: record.target,
|
|
910
|
-
slot: record.slot,
|
|
911
|
-
...(record.threadName ? { threadName: record.threadName } : {}),
|
|
912
|
-
reused: true,
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
let forcedUnnamedStale = false;
|
|
916
|
-
if (deps.forceFreshUnnamed) {
|
|
917
|
-
for (const record of priorTargets) {
|
|
918
|
-
const isLeaderOwned =
|
|
919
|
-
record.owner?.kind === "leader" ||
|
|
920
|
-
(!record.owner &&
|
|
921
|
-
(record.profileKey.startsWith("cwd:") ||
|
|
922
|
-
record.profileKey.startsWith("leader:")));
|
|
923
|
-
if (!isLeaderOwned) continue;
|
|
924
|
-
if (record.threadName) continue;
|
|
925
|
-
forcedUnnamedStale =
|
|
926
|
-
deps.topicTargetStore.markStaleByTarget(record.target) ||
|
|
927
|
-
forcedUnnamedStale;
|
|
928
|
-
deps.recordEvent("telegram", "Unnamed leader thread binding refreshed", {
|
|
929
|
-
phase: "leader-thread-force-fresh-unnamed",
|
|
930
|
-
instanceId: deps.instanceId,
|
|
931
|
-
chatId: record.target.chatId,
|
|
932
|
-
threadId: record.target.threadId,
|
|
1059
|
+
assertLeaderEpoch("before-reuse");
|
|
1060
|
+
if (!record.slot) {
|
|
1061
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
1062
|
+
}
|
|
1063
|
+
return await commitWorkspaceBinding({
|
|
1064
|
+
target: record.target,
|
|
933
1065
|
slot: record.slot,
|
|
1066
|
+
...(record.threadName ? { threadName: record.threadName } : {}),
|
|
1067
|
+
reused: true,
|
|
934
1068
|
});
|
|
935
1069
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1070
|
+
let forcedUnnamedStale = false;
|
|
1071
|
+
if (deps.forceFreshUnnamed) {
|
|
1072
|
+
for (const record of priorTargets) {
|
|
1073
|
+
const isLeaderOwned =
|
|
1074
|
+
record.owner?.kind === "leader" ||
|
|
1075
|
+
(!record.owner &&
|
|
1076
|
+
(record.profileKey.startsWith("cwd:") ||
|
|
1077
|
+
record.profileKey.startsWith("leader:")));
|
|
1078
|
+
if (!isLeaderOwned) continue;
|
|
1079
|
+
if (record.threadName) continue;
|
|
1080
|
+
forcedUnnamedStale =
|
|
1081
|
+
deps.topicTargetStore.markStaleByTarget(record.target) ||
|
|
1082
|
+
forcedUnnamedStale;
|
|
1083
|
+
deps.recordEvent(
|
|
1084
|
+
"telegram",
|
|
1085
|
+
"Unnamed leader thread binding refreshed",
|
|
1086
|
+
{
|
|
1087
|
+
phase: "leader-thread-force-fresh-unnamed",
|
|
1088
|
+
instanceId: deps.instanceId,
|
|
1089
|
+
chatId: record.target.chatId,
|
|
1090
|
+
threadId: record.target.threadId,
|
|
1091
|
+
slot: record.slot,
|
|
1092
|
+
},
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (forcedUnnamedStale) await deps.topicTargetStore.persist();
|
|
1096
|
+
}
|
|
1097
|
+
assertLeaderEpoch("before-provision");
|
|
1098
|
+
const ownTarget = await provisionOwnBusTopic({
|
|
1099
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
1100
|
+
instanceId: deps.instanceId,
|
|
1101
|
+
cwd: normalizedLeaderCwd,
|
|
1102
|
+
telegramProfile: deps.telegramProfile,
|
|
1103
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1104
|
+
getThreadReconciliationMachineState:
|
|
1105
|
+
deps.getThreadReconciliationMachineState,
|
|
1106
|
+
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
1107
|
+
store: deps.topicTargetStore,
|
|
1108
|
+
callApi: deps.callApi,
|
|
1109
|
+
getNowMs: deps.getNowMs,
|
|
1110
|
+
getRandom: deps.getRandom,
|
|
1111
|
+
requestedThreadName:
|
|
1112
|
+
recoverableWorkspaceBinding?.threadName ?? deps.requestedThreadName,
|
|
1113
|
+
workspaceBindingKey: workspaceIdentity?.bindingKey,
|
|
1114
|
+
preferredSlot: workspaceIdentity?.slot,
|
|
1115
|
+
resolveInitialWorkspaceDisplayTitle:
|
|
1116
|
+
deps.resolveInitialWorkspaceDisplayTitle,
|
|
1117
|
+
recordEvent: deps.recordEvent,
|
|
1118
|
+
});
|
|
1119
|
+
assertLeaderEpoch("after-provision");
|
|
1120
|
+
if (!ownTarget) return undefined;
|
|
1121
|
+
const replacementPlan = ThreadReconciler.planThreadReconciliation({
|
|
1122
|
+
nowMs: Date.now(),
|
|
1123
|
+
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
1124
|
+
previousState: deps.getThreadReconciliationMachineState?.(),
|
|
1125
|
+
records: priorTargets,
|
|
1126
|
+
pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
|
|
1127
|
+
replacedBindings: [
|
|
1128
|
+
{
|
|
1129
|
+
instanceId: deps.instanceId,
|
|
1130
|
+
replacementTarget: ownTarget.target,
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
1134
|
+
deps.recordThreadReconciliationPlan?.(replacementPlan);
|
|
1135
|
+
await ThreadReconciler.applyThreadReconciliationPlan(replacementPlan, {
|
|
1136
|
+
isCleanupTargetProtected: createTelegramCleanupTargetProtection(
|
|
1137
|
+
deps.topicTargetStore,
|
|
984
1138
|
),
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1139
|
+
callApi: deps.callApi,
|
|
1140
|
+
markStaleByTarget: (target, syncStatus, lastSyncError) =>
|
|
1141
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
1142
|
+
target,
|
|
1143
|
+
syncStatus,
|
|
1144
|
+
lastSyncError,
|
|
1145
|
+
),
|
|
1146
|
+
persist: () => deps.topicTargetStore.persist(),
|
|
1147
|
+
removePendingProvisionById: (id) =>
|
|
1148
|
+
deps.topicTargetStore.removePendingProvision(id),
|
|
1149
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1150
|
+
recordRuntimeEvent: deps.recordEvent,
|
|
1151
|
+
});
|
|
1152
|
+
assertLeaderEpoch("before-final-persist");
|
|
1153
|
+
await deps.topicTargetStore.persist();
|
|
1154
|
+
assertLeaderEpoch("after-final-persist");
|
|
1155
|
+
return await commitWorkspaceBinding(ownTarget);
|
|
995
1156
|
} finally {
|
|
996
1157
|
deps.topicTargetStore.releaseWorkspaceClaim(deps.instanceId);
|
|
997
1158
|
}
|
|
@@ -1013,20 +1174,6 @@ export const TELEGRAM_SYNC_SLICES = [
|
|
|
1013
1174
|
|
|
1014
1175
|
export type TelegramSyncSlice = (typeof TELEGRAM_SYNC_SLICES)[number];
|
|
1015
1176
|
|
|
1016
|
-
export type TelegramSyncTrigger =
|
|
1017
|
-
| "startup"
|
|
1018
|
-
| "reload"
|
|
1019
|
-
| "topic-lifecycle"
|
|
1020
|
-
| "stale-api-error"
|
|
1021
|
-
| "setup-change"
|
|
1022
|
-
| "pairing-change"
|
|
1023
|
-
| "follower-register"
|
|
1024
|
-
| "follower-prune"
|
|
1025
|
-
| "status-request"
|
|
1026
|
-
| "leader-health-tick"
|
|
1027
|
-
| "ordinary-message"
|
|
1028
|
-
| "ordinary-send";
|
|
1029
|
-
|
|
1030
1177
|
export interface TelegramSyncSliceState {
|
|
1031
1178
|
status: "fresh" | "suspect" | "unknown";
|
|
1032
1179
|
updatedAtMs?: number;
|
|
@@ -1102,25 +1249,6 @@ export function createTelegramProvisioningActivityRuntime(): TelegramProvisionin
|
|
|
1102
1249
|
};
|
|
1103
1250
|
}
|
|
1104
1251
|
|
|
1105
|
-
const RECONCILE_TRIGGERS = new Set<TelegramSyncTrigger>([
|
|
1106
|
-
"startup",
|
|
1107
|
-
"reload",
|
|
1108
|
-
"topic-lifecycle",
|
|
1109
|
-
"stale-api-error",
|
|
1110
|
-
"setup-change",
|
|
1111
|
-
"pairing-change",
|
|
1112
|
-
"follower-register",
|
|
1113
|
-
"follower-prune",
|
|
1114
|
-
"status-request",
|
|
1115
|
-
"leader-health-tick",
|
|
1116
|
-
]);
|
|
1117
|
-
|
|
1118
|
-
export function shouldReconcileTelegramSync(
|
|
1119
|
-
trigger: TelegramSyncTrigger,
|
|
1120
|
-
): boolean {
|
|
1121
|
-
return RECONCILE_TRIGGERS.has(trigger);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
1252
|
export function markTelegramSyncSliceSuspect(
|
|
1125
1253
|
state: TelegramSyncState,
|
|
1126
1254
|
slice: TelegramSyncSlice,
|
|
@@ -1189,14 +1317,15 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
1189
1317
|
}) as TSyncState,
|
|
1190
1318
|
);
|
|
1191
1319
|
};
|
|
1192
|
-
return (lifecycle) =>
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1320
|
+
return (lifecycle) =>
|
|
1321
|
+
deps.runWorkspaceOperation(
|
|
1322
|
+
{
|
|
1323
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1324
|
+
operationKind: "workspace.sync-topic-lifecycle",
|
|
1325
|
+
scopes: [{ kind: "profile" }],
|
|
1326
|
+
},
|
|
1327
|
+
() => operation(lifecycle),
|
|
1328
|
+
);
|
|
1200
1329
|
}
|
|
1201
1330
|
|
|
1202
1331
|
export function createTelegramTopicLifecycleSyncHandler<TMessage = unknown>(
|