@llblab/pi-telegram 0.52.2 → 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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- 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 +639 -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 +29 -3
- package/dist/lib/locks.js +289 -108
- 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 +21 -37
- package/dist/lib/polling.js +59 -50
- 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 +1 -3
- package/dist/lib/sync.js +67 -43
- 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 +148 -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 +31 -29
- 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 +1206 -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 +677 -203
- 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 +144 -147
- 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 +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -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";
|
|
@@ -180,9 +183,11 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
180
183
|
getNowMs?: () => number;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
|
-
function markTelegramConfigSyncChange<
|
|
184
|
-
TSyncState
|
|
185
|
-
|
|
186
|
+
function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(
|
|
187
|
+
state: TSyncState,
|
|
188
|
+
action: string,
|
|
189
|
+
options?: { nowMs?: number },
|
|
190
|
+
): TSyncState {
|
|
186
191
|
const nowMs = options?.nowMs ?? Date.now();
|
|
187
192
|
let nextState = markTelegramSyncSliceFresh(state, "pairing", {
|
|
188
193
|
nowMs,
|
|
@@ -201,8 +206,10 @@ function markTelegramConfigSyncChange<
|
|
|
201
206
|
|
|
202
207
|
export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
203
208
|
instanceId: string;
|
|
204
|
-
topicTargetStore: Pick<
|
|
205
|
-
|
|
209
|
+
topicTargetStore: Pick<
|
|
210
|
+
TelegramTopicTargetStore,
|
|
211
|
+
"load" | "list" | "listPendingCleanups" | "detachTargetOwner"
|
|
212
|
+
>;
|
|
206
213
|
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
207
214
|
getProfileName: () => string | undefined;
|
|
208
215
|
isPollingSuspended: () => boolean;
|
|
@@ -213,29 +220,60 @@ export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
|
213
220
|
const epoch = deps.getCurrentLeaderEpoch();
|
|
214
221
|
const profile = deps.getProfileName();
|
|
215
222
|
if (epoch === undefined || !isSessionCurrent()) return undefined;
|
|
216
|
-
const isCurrent = () =>
|
|
217
|
-
|
|
223
|
+
const isCurrent = () =>
|
|
224
|
+
isSessionCurrent() &&
|
|
225
|
+
deps.getCurrentLeaderEpoch() === epoch &&
|
|
226
|
+
deps.getProfileName() === profile &&
|
|
227
|
+
deps.isPollingSuspended();
|
|
218
228
|
return async () => {
|
|
219
|
-
if (
|
|
229
|
+
if (
|
|
230
|
+
!isCurrent() ||
|
|
231
|
+
(await deps.resolveAutomaticThreadCleanupEnabled()) !== false
|
|
232
|
+
)
|
|
233
|
+
return;
|
|
220
234
|
if (!isCurrent()) return;
|
|
221
|
-
await deps.runWorkspaceOperation(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
);
|
|
239
277
|
};
|
|
240
278
|
};
|
|
241
279
|
}
|
|
@@ -271,7 +309,10 @@ export function createTelegramThreadDisconnectAssembly<
|
|
|
271
309
|
>(
|
|
272
310
|
deps: Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> & {
|
|
273
311
|
stopPolling: () => Promise<string>;
|
|
274
|
-
captureStopPolling?: () => {
|
|
312
|
+
captureStopPolling?: () => {
|
|
313
|
+
isCurrent: () => boolean;
|
|
314
|
+
stop: () => Promise<string>;
|
|
315
|
+
};
|
|
275
316
|
suspendPolling: () => Promise<void>;
|
|
276
317
|
},
|
|
277
318
|
): TelegramThreadDisconnectAssembly {
|
|
@@ -294,26 +335,31 @@ export function createTelegramThreadDisconnectAssembly<
|
|
|
294
335
|
} catch (error) {
|
|
295
336
|
// Never retry a stop whose teardown/release outcome may already have committed.
|
|
296
337
|
if (stopAttempted) {
|
|
297
|
-
deps.recordRuntimeEvent("connection", error, {
|
|
338
|
+
deps.recordRuntimeEvent("connection", error, {
|
|
339
|
+
phase: "disconnect-stop",
|
|
340
|
+
});
|
|
298
341
|
throw error;
|
|
299
342
|
}
|
|
300
|
-
deps.recordRuntimeEvent("connection", error, {
|
|
343
|
+
deps.recordRuntimeEvent("connection", error, {
|
|
344
|
+
phase: "disconnect-cleanup",
|
|
345
|
+
});
|
|
301
346
|
let stopped: string;
|
|
302
347
|
try {
|
|
303
348
|
stopped = await stop();
|
|
304
349
|
} catch (stopError) {
|
|
305
|
-
deps.recordRuntimeEvent("connection", stopError, {
|
|
350
|
+
deps.recordRuntimeEvent("connection", stopError, {
|
|
351
|
+
phase: "disconnect-stop",
|
|
352
|
+
});
|
|
306
353
|
throw stopError;
|
|
307
354
|
}
|
|
308
355
|
const separator = /[.!?]$/u.test(stopped) ? " " : ". ";
|
|
309
356
|
return `${stopped}${separator}Thread cleanup was not confirmed. Check /telegram-status --debug.`;
|
|
310
357
|
}
|
|
311
358
|
},
|
|
312
|
-
cleanupForSessionRestart:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}),
|
|
359
|
+
cleanupForSessionRestart: createTelegramSessionRestartThreadCleanupHandler({
|
|
360
|
+
...deps,
|
|
361
|
+
suspendPolling: deps.suspendPolling,
|
|
362
|
+
}),
|
|
317
363
|
};
|
|
318
364
|
}
|
|
319
365
|
|
|
@@ -321,7 +367,10 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
321
367
|
TSyncState extends TelegramSyncState,
|
|
322
368
|
>(deps: TelegramManualThreadDisconnectDeps<TSyncState>): () => Promise<string> {
|
|
323
369
|
const assertCurrent = () => {
|
|
324
|
-
if (deps.isDisconnectCurrent?.() === false)
|
|
370
|
+
if (deps.isDisconnectCurrent?.() === false)
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Telegram disconnect was superseded by a new connection.",
|
|
373
|
+
);
|
|
325
374
|
};
|
|
326
375
|
const operation = async (): Promise<string> => {
|
|
327
376
|
assertCurrent();
|
|
@@ -359,9 +408,18 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
359
408
|
target,
|
|
360
409
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
361
410
|
};
|
|
362
|
-
const departingRecord = deps.topicTargetStore
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
);
|
|
365
423
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
366
424
|
await deps.topicTargetStore.persist();
|
|
367
425
|
assertCurrent();
|
|
@@ -400,7 +458,11 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
400
458
|
},
|
|
401
459
|
);
|
|
402
460
|
assertCurrent();
|
|
403
|
-
if (
|
|
461
|
+
if (
|
|
462
|
+
cleanupPlan.actions.some((action) =>
|
|
463
|
+
isCleanupTargetProtected(action.target, action),
|
|
464
|
+
)
|
|
465
|
+
) {
|
|
404
466
|
return "Thread disconnect superseded by a new binding.";
|
|
405
467
|
}
|
|
406
468
|
cleanupPending = Boolean(cleanup.incompleteActions?.length);
|
|
@@ -425,14 +487,16 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
425
487
|
? `${stopped} Telegram thread cleanup remains pending for the next leader.`
|
|
426
488
|
: stopped;
|
|
427
489
|
};
|
|
428
|
-
return () =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
);
|
|
436
500
|
}
|
|
437
501
|
|
|
438
502
|
export function createTelegramLeaderHealthRuntime<
|
|
@@ -539,7 +603,8 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
539
603
|
topicTargetStore: Pick<
|
|
540
604
|
TelegramTopicTargetStore,
|
|
541
605
|
"load" | "markStaleByTarget" | "persist"
|
|
542
|
-
> &
|
|
606
|
+
> &
|
|
607
|
+
Partial<Pick<TelegramTopicTargetStore, "invalidateTarget">>;
|
|
543
608
|
getSyncState: () => TSyncState;
|
|
544
609
|
setSyncState: (state: TSyncState) => void;
|
|
545
610
|
recordEvent: (
|
|
@@ -550,16 +615,23 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
550
615
|
getNowMs?: () => number;
|
|
551
616
|
isCurrent?: () => boolean;
|
|
552
617
|
isAuthorityCurrent?: () => boolean;
|
|
553
|
-
getWorkspaceAdmission?: () =>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
618
|
+
getWorkspaceAdmission?: () =>
|
|
619
|
+
| Pick<
|
|
620
|
+
TelegramWorkspaceAdmissionLedger,
|
|
621
|
+
"acquireAdmission" | "releaseAdmission"
|
|
622
|
+
>
|
|
623
|
+
| undefined;
|
|
557
624
|
}
|
|
558
625
|
|
|
559
|
-
export function captureTelegramStaleTargetRequestRecovery<
|
|
626
|
+
export function captureTelegramStaleTargetRequestRecovery<
|
|
627
|
+
TSyncState extends TelegramSyncState,
|
|
628
|
+
>(
|
|
560
629
|
body: Record<string, unknown>,
|
|
561
630
|
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> & {
|
|
562
|
-
topicTargetStore: Pick<
|
|
631
|
+
topicTargetStore: Pick<
|
|
632
|
+
TelegramTopicTargetStore,
|
|
633
|
+
"load" | "list" | "markStaleByTarget" | "persist" | "invalidateTarget"
|
|
634
|
+
>;
|
|
563
635
|
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
564
636
|
getSessionGeneration: () => number;
|
|
565
637
|
getProfileName: () => string | undefined;
|
|
@@ -570,25 +642,44 @@ export function captureTelegramStaleTargetRequestRecovery<TSyncState extends Tel
|
|
|
570
642
|
const epoch = deps.getCurrentLeaderEpoch();
|
|
571
643
|
if (!target || epoch === undefined) return undefined;
|
|
572
644
|
const key = getTelegramTargetKey(target);
|
|
573
|
-
const record = deps.topicTargetStore
|
|
645
|
+
const record = deps.topicTargetStore
|
|
646
|
+
.list()
|
|
647
|
+
.find((candidate) => getTelegramTargetKey(candidate.target) === key);
|
|
574
648
|
if (!record) return undefined;
|
|
575
649
|
const generation = deps.getSessionGeneration();
|
|
576
650
|
const profile = deps.getProfileName();
|
|
577
|
-
const isAuthorityCurrent = (): boolean =>
|
|
578
|
-
deps.
|
|
651
|
+
const isAuthorityCurrent = (): boolean =>
|
|
652
|
+
deps.getCurrentLeaderEpoch() === epoch &&
|
|
653
|
+
deps.getSessionGeneration() === generation &&
|
|
654
|
+
deps.getProfileName() === profile;
|
|
579
655
|
const isCurrent = (): boolean => {
|
|
580
|
-
const current = deps.topicTargetStore
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
);
|
|
584
666
|
};
|
|
585
667
|
return async (error) => {
|
|
586
668
|
const requestTarget = getTelegramApiErrorRequestTarget(error);
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
) {
|
|
592
683
|
deps.onRecovered();
|
|
593
684
|
}
|
|
594
685
|
};
|
|
@@ -631,17 +722,19 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
631
722
|
!target ||
|
|
632
723
|
!isTelegramTopicTargetStaleError(error) ||
|
|
633
724
|
deps.isCurrent?.() === false
|
|
634
|
-
)
|
|
725
|
+
)
|
|
726
|
+
return false;
|
|
635
727
|
const recover = async (): Promise<boolean> => {
|
|
636
728
|
if (deps.isCurrent) {
|
|
637
729
|
if (
|
|
638
730
|
!deps.topicTargetStore.invalidateTarget ||
|
|
639
|
-
!await deps.topicTargetStore.invalidateTarget(
|
|
731
|
+
!(await deps.topicTargetStore.invalidateTarget(
|
|
640
732
|
target,
|
|
641
733
|
deps.isCurrent,
|
|
642
734
|
String(error),
|
|
643
|
-
)
|
|
644
|
-
)
|
|
735
|
+
))
|
|
736
|
+
)
|
|
737
|
+
return false;
|
|
645
738
|
if (deps.isAuthorityCurrent?.() === false) return false;
|
|
646
739
|
} else {
|
|
647
740
|
await deps.topicTargetStore.load();
|
|
@@ -651,7 +744,8 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
651
744
|
"deleted",
|
|
652
745
|
String(error),
|
|
653
746
|
)
|
|
654
|
-
)
|
|
747
|
+
)
|
|
748
|
+
return false;
|
|
655
749
|
await deps.topicTargetStore.persist();
|
|
656
750
|
}
|
|
657
751
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
@@ -740,7 +834,12 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
740
834
|
normalizedLeaderCwd,
|
|
741
835
|
deps.instanceId,
|
|
742
836
|
legacyLeaderRecord?.instanceId,
|
|
743
|
-
{
|
|
837
|
+
{
|
|
838
|
+
sessionId: deps.sessionId,
|
|
839
|
+
onCapacityUnavailable() {
|
|
840
|
+
capacityUnavailable = true;
|
|
841
|
+
},
|
|
842
|
+
},
|
|
744
843
|
)
|
|
745
844
|
: undefined;
|
|
746
845
|
if (deps.cwd && !workspaceIdentity) {
|
|
@@ -770,270 +869,290 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
770
869
|
assertLeaderEpoch("before-workspace-persist");
|
|
771
870
|
await deps.topicTargetStore.persist();
|
|
772
871
|
assertLeaderEpoch("after-workspace-persist");
|
|
773
|
-
const {
|
|
872
|
+
const {
|
|
873
|
+
target: _target,
|
|
874
|
+
slot: _slot,
|
|
875
|
+
threadName: _threadName,
|
|
876
|
+
displayTitle: _displayTitle,
|
|
877
|
+
...rest
|
|
878
|
+
} = result;
|
|
774
879
|
return {
|
|
775
880
|
...rest,
|
|
776
881
|
target: { ...committed.target },
|
|
777
882
|
slot: committed.slot ?? result.slot,
|
|
778
883
|
...(committed.threadName ? { threadName: committed.threadName } : {}),
|
|
779
|
-
...(committed.displayTitle
|
|
884
|
+
...(committed.displayTitle
|
|
885
|
+
? { displayTitle: committed.displayTitle }
|
|
886
|
+
: {}),
|
|
780
887
|
};
|
|
781
888
|
};
|
|
782
889
|
try {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
+
));
|
|
794
960
|
if (
|
|
795
|
-
|
|
796
|
-
!unavailableTargetKeys.has(
|
|
961
|
+
recoverableWorkspaceBinding &&
|
|
962
|
+
!unavailableTargetKeys.has(recoverableTargetKey ?? "") &&
|
|
963
|
+
(!deps.forceFreshUnnamed || recoverableWorkspaceBinding.threadName) &&
|
|
964
|
+
(sameProcessWorkspaceBinding || deps.probeWorkspaceBinding)
|
|
797
965
|
) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
!persistedWorkspaceBinding &&
|
|
820
|
-
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
821
|
-
? {
|
|
822
|
-
...workspaceIdentity,
|
|
823
|
-
target: { ...legacyLeaderRecord.target },
|
|
824
|
-
...(legacyLeaderRecord.threadName
|
|
825
|
-
? { threadName: legacyLeaderRecord.threadName }
|
|
826
|
-
: {}),
|
|
827
|
-
...(legacyLeaderRecord.slot ? { slot: legacyLeaderRecord.slot } : {}),
|
|
828
|
-
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
|
+
});
|
|
829
987
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
!unavailableTargetKeys.has(recoverableTargetKey ?? "") &&
|
|
854
|
-
(!deps.forceFreshUnnamed || recoverableWorkspaceBinding.threadName) &&
|
|
855
|
-
(sameProcessWorkspaceBinding || deps.probeWorkspaceBinding)
|
|
856
|
-
) {
|
|
857
|
-
let workspaceTargetVisible = sameProcessWorkspaceBinding;
|
|
858
|
-
if (!workspaceTargetVisible && deps.probeWorkspaceBinding) {
|
|
859
|
-
assertLeaderEpoch("before-workspace-probe");
|
|
860
|
-
try {
|
|
861
|
-
await deps.probeWorkspaceBinding(recoverableWorkspaceBinding);
|
|
862
|
-
assertLeaderEpoch("after-workspace-probe");
|
|
863
|
-
workspaceTargetVisible = true;
|
|
864
|
-
} catch (error) {
|
|
865
|
-
if (!isTelegramTopicTargetStaleError(error)) throw error;
|
|
866
|
-
deps.topicTargetStore.markStaleByTarget(
|
|
867
|
-
recoverableWorkspaceBinding.target,
|
|
868
|
-
"deleted",
|
|
869
|
-
error instanceof Error ? error.message : String(error),
|
|
870
|
-
);
|
|
871
|
-
await deps.topicTargetStore.persist();
|
|
872
|
-
deps.recordEvent("bus", error, {
|
|
873
|
-
phase: "leader-workspace-target-stale",
|
|
874
|
-
chatId: recoverableWorkspaceBinding.target.chatId,
|
|
875
|
-
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 } : {}),
|
|
876
1011
|
instanceId: deps.instanceId,
|
|
1012
|
+
slot: recovered.slot,
|
|
1013
|
+
syncStatus: "open",
|
|
1014
|
+
lastSyncObservedAtMs: nowMs,
|
|
1015
|
+
lastReconcileAction: "leader-workspace-binding-recovered",
|
|
877
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;
|
|
878
1032
|
}
|
|
879
1033
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
deps.topicTargetStore.upsert({
|
|
895
|
-
profileKey: leaderProfileKey,
|
|
896
|
-
owner: leaderOwner,
|
|
897
|
-
target: { ...recovered.target },
|
|
898
|
-
status: "active",
|
|
899
|
-
createdAtMs: recoverableRecord?.createdAtMs ?? nowMs,
|
|
900
|
-
updatedAtMs: nowMs,
|
|
901
|
-
...(recovered.threadName ? { threadName: recovered.threadName } : {}),
|
|
902
|
-
instanceId: deps.instanceId,
|
|
903
|
-
slot: recovered.slot,
|
|
904
|
-
syncStatus: "open",
|
|
905
|
-
lastSyncObservedAtMs: nowMs,
|
|
906
|
-
lastReconcileAction: "leader-workspace-binding-recovered",
|
|
907
|
-
});
|
|
908
|
-
assertLeaderEpoch("before-recovered-record-persist");
|
|
909
|
-
await deps.topicTargetStore.persist();
|
|
910
|
-
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];
|
|
911
1048
|
deps.recordEvent(
|
|
912
1049
|
"telegram",
|
|
913
|
-
"Leader thread preserved after
|
|
1050
|
+
"Leader thread preserved after session lifecycle change",
|
|
914
1051
|
{
|
|
915
1052
|
phase: "leader-thread-reused",
|
|
916
1053
|
instanceId: deps.instanceId,
|
|
917
|
-
chatId:
|
|
918
|
-
threadId:
|
|
919
|
-
slot:
|
|
1054
|
+
chatId: record.target.chatId,
|
|
1055
|
+
threadId: record.target.threadId,
|
|
1056
|
+
slot: record.slot,
|
|
920
1057
|
},
|
|
921
1058
|
);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
(record.status === "active" || record.status === "starting")
|
|
929
|
-
);
|
|
930
|
-
});
|
|
931
|
-
// Legacy callers without session identity may reuse the active instance target.
|
|
932
|
-
// Session-aware callers must resolve their exact Workspace binding instead.
|
|
933
|
-
if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
934
|
-
const record = priorTargets[0];
|
|
935
|
-
deps.recordEvent(
|
|
936
|
-
"telegram",
|
|
937
|
-
"Leader thread preserved after session lifecycle change",
|
|
938
|
-
{
|
|
939
|
-
phase: "leader-thread-reused",
|
|
940
|
-
instanceId: deps.instanceId,
|
|
941
|
-
chatId: record.target.chatId,
|
|
942
|
-
threadId: record.target.threadId,
|
|
943
|
-
slot: record.slot,
|
|
944
|
-
},
|
|
945
|
-
);
|
|
946
|
-
assertLeaderEpoch("before-reuse");
|
|
947
|
-
if (!record.slot) {
|
|
948
|
-
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
949
|
-
}
|
|
950
|
-
return await commitWorkspaceBinding({
|
|
951
|
-
target: record.target,
|
|
952
|
-
slot: record.slot,
|
|
953
|
-
...(record.threadName ? { threadName: record.threadName } : {}),
|
|
954
|
-
reused: true,
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
let forcedUnnamedStale = false;
|
|
958
|
-
if (deps.forceFreshUnnamed) {
|
|
959
|
-
for (const record of priorTargets) {
|
|
960
|
-
const isLeaderOwned =
|
|
961
|
-
record.owner?.kind === "leader" ||
|
|
962
|
-
(!record.owner &&
|
|
963
|
-
(record.profileKey.startsWith("cwd:") ||
|
|
964
|
-
record.profileKey.startsWith("leader:")));
|
|
965
|
-
if (!isLeaderOwned) continue;
|
|
966
|
-
if (record.threadName) continue;
|
|
967
|
-
forcedUnnamedStale =
|
|
968
|
-
deps.topicTargetStore.markStaleByTarget(record.target) ||
|
|
969
|
-
forcedUnnamedStale;
|
|
970
|
-
deps.recordEvent("telegram", "Unnamed leader thread binding refreshed", {
|
|
971
|
-
phase: "leader-thread-force-fresh-unnamed",
|
|
972
|
-
instanceId: deps.instanceId,
|
|
973
|
-
chatId: record.target.chatId,
|
|
974
|
-
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,
|
|
975
1065
|
slot: record.slot,
|
|
1066
|
+
...(record.threadName ? { threadName: record.threadName } : {}),
|
|
1067
|
+
reused: true,
|
|
976
1068
|
});
|
|
977
1069
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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,
|
|
1026
1138
|
),
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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);
|
|
1037
1156
|
} finally {
|
|
1038
1157
|
deps.topicTargetStore.releaseWorkspaceClaim(deps.instanceId);
|
|
1039
1158
|
}
|
|
@@ -1055,20 +1174,6 @@ export const TELEGRAM_SYNC_SLICES = [
|
|
|
1055
1174
|
|
|
1056
1175
|
export type TelegramSyncSlice = (typeof TELEGRAM_SYNC_SLICES)[number];
|
|
1057
1176
|
|
|
1058
|
-
export type TelegramSyncTrigger =
|
|
1059
|
-
| "startup"
|
|
1060
|
-
| "reload"
|
|
1061
|
-
| "topic-lifecycle"
|
|
1062
|
-
| "stale-api-error"
|
|
1063
|
-
| "setup-change"
|
|
1064
|
-
| "pairing-change"
|
|
1065
|
-
| "follower-register"
|
|
1066
|
-
| "follower-prune"
|
|
1067
|
-
| "status-request"
|
|
1068
|
-
| "leader-health-tick"
|
|
1069
|
-
| "ordinary-message"
|
|
1070
|
-
| "ordinary-send";
|
|
1071
|
-
|
|
1072
1177
|
export interface TelegramSyncSliceState {
|
|
1073
1178
|
status: "fresh" | "suspect" | "unknown";
|
|
1074
1179
|
updatedAtMs?: number;
|
|
@@ -1144,25 +1249,6 @@ export function createTelegramProvisioningActivityRuntime(): TelegramProvisionin
|
|
|
1144
1249
|
};
|
|
1145
1250
|
}
|
|
1146
1251
|
|
|
1147
|
-
const RECONCILE_TRIGGERS = new Set<TelegramSyncTrigger>([
|
|
1148
|
-
"startup",
|
|
1149
|
-
"reload",
|
|
1150
|
-
"topic-lifecycle",
|
|
1151
|
-
"stale-api-error",
|
|
1152
|
-
"setup-change",
|
|
1153
|
-
"pairing-change",
|
|
1154
|
-
"follower-register",
|
|
1155
|
-
"follower-prune",
|
|
1156
|
-
"status-request",
|
|
1157
|
-
"leader-health-tick",
|
|
1158
|
-
]);
|
|
1159
|
-
|
|
1160
|
-
export function shouldReconcileTelegramSync(
|
|
1161
|
-
trigger: TelegramSyncTrigger,
|
|
1162
|
-
): boolean {
|
|
1163
|
-
return RECONCILE_TRIGGERS.has(trigger);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
1252
|
export function markTelegramSyncSliceSuspect(
|
|
1167
1253
|
state: TelegramSyncState,
|
|
1168
1254
|
slice: TelegramSyncSlice,
|
|
@@ -1231,14 +1317,15 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
1231
1317
|
}) as TSyncState,
|
|
1232
1318
|
);
|
|
1233
1319
|
};
|
|
1234
|
-
return (lifecycle) =>
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
+
);
|
|
1242
1329
|
}
|
|
1243
1330
|
|
|
1244
1331
|
export function createTelegramTopicLifecycleSyncHandler<TMessage = unknown>(
|