@llblab/pi-telegram 0.50.0 → 0.51.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 +13 -32
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/lib/bindings.d.ts +3 -5
- package/dist/lib/bindings.js +4 -11
- package/dist/lib/bus-follower.d.ts +1 -1
- package/dist/lib/bus-follower.js +93 -9
- package/dist/lib/bus-leader.d.ts +9 -3
- package/dist/lib/bus-leader.js +231 -71
- package/dist/lib/bus.d.ts +4 -0
- package/dist/lib/bus.js +34 -7
- package/dist/lib/commands.d.ts +1 -4
- package/dist/lib/commands.js +13 -33
- package/dist/lib/extension.js +64 -6
- package/dist/lib/journal.d.ts +3 -0
- package/dist/lib/journal.js +13 -0
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +25 -4
- package/dist/lib/locks.d.ts +1 -0
- package/dist/lib/locks.js +32 -12
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +72 -1
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.js +22 -1
- package/dist/lib/polling.js +2 -2
- package/dist/lib/queue.d.ts +8 -1
- package/dist/lib/queue.js +28 -2
- package/dist/lib/replies.d.ts +3 -0
- package/dist/lib/replies.js +17 -0
- package/dist/lib/sync.d.ts +9 -0
- package/dist/lib/sync.js +49 -2
- package/dist/lib/telegram-api.d.ts +9 -1
- package/dist/lib/telegram-api.js +23 -3
- package/dist/lib/thread-cleanup-manager.js +7 -2
- package/dist/lib/threads.d.ts +2 -0
- package/dist/lib/threads.js +68 -37
- package/dist/lib/updates.d.ts +6 -4
- package/dist/lib/updates.js +134 -61
- package/dist/lib/workspace-admission.d.ts +9 -0
- package/dist/lib/workspace-admission.js +49 -3
- package/dist/lib/workspace-retirement.d.ts +77 -6
- package/dist/lib/workspace-retirement.js +384 -8
- package/dist/lib/workspace-slots.d.ts +3 -0
- package/dist/lib/workspace-slots.js +6 -0
- package/dist/package.json +1 -1
- package/docs/architecture.md +23 -14
- package/docs/multi-instance-bus.md +32 -11
- package/docs/public-api.md +1 -1
- package/docs/updates.md +2 -0
- package/lib/bindings.ts +5 -17
- package/lib/bus-follower.ts +69 -12
- package/lib/bus-leader.ts +236 -94
- package/lib/bus.ts +39 -9
- package/lib/commands.ts +18 -41
- package/lib/extension.ts +70 -10
- package/lib/journal.ts +14 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/locks.ts +32 -13
- package/lib/menu-queue.ts +126 -1
- package/lib/paths.ts +29 -1
- package/lib/polling.ts +2 -2
- package/lib/queue.ts +48 -3
- package/lib/replies.ts +19 -0
- package/lib/sync.ts +53 -2
- package/lib/telegram-api.ts +30 -2
- package/lib/thread-cleanup-manager.ts +11 -3
- package/lib/threads.ts +73 -36
- package/lib/updates.ts +145 -78
- package/lib/workspace-admission.ts +67 -4
- package/lib/workspace-retirement.ts +465 -7
- package/lib/workspace-slots.ts +7 -0
- package/package.json +1 -1
package/lib/commands.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
pairTelegramUserIfNeeded,
|
|
9
11
|
type TelegramConfigStore,
|
|
@@ -366,7 +368,6 @@ export function createTelegramBotCommandRegistrar(
|
|
|
366
368
|
export interface TelegramBridgeCommandStartPollingOptions {
|
|
367
369
|
force?: boolean;
|
|
368
370
|
forceFreshLeaderThread?: boolean;
|
|
369
|
-
requestedThreadName?: string;
|
|
370
371
|
}
|
|
371
372
|
|
|
372
373
|
export interface TelegramBridgeCommandStartPollingResult {
|
|
@@ -407,7 +408,6 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
407
408
|
ctx: ExtensionCommandContext,
|
|
408
409
|
profileName: string,
|
|
409
410
|
) => Promise<boolean>;
|
|
410
|
-
validateThreadName?: (threadName: string) => string | undefined;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
export type TelegramThreadDisplayNameRenamePort = (
|
|
@@ -466,17 +466,6 @@ function parseTelegramProfileArg(args: string): string | undefined {
|
|
|
466
466
|
return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
export function parseTelegramRequestedThreadName(
|
|
470
|
-
args: string,
|
|
471
|
-
): string | undefined {
|
|
472
|
-
const token = args
|
|
473
|
-
.trim()
|
|
474
|
-
.split(/\s+/)
|
|
475
|
-
.find((word) => /^as=/i.test(word));
|
|
476
|
-
const value = token?.slice(3).trim();
|
|
477
|
-
return value || undefined;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
469
|
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
481
470
|
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
482
471
|
}
|
|
@@ -498,7 +487,7 @@ export function registerTelegramBridgeCommands(
|
|
|
498
487
|
deps: TelegramBridgeCommandRegistrationDeps,
|
|
499
488
|
): void {
|
|
500
489
|
pi.registerCommand("telegram-setup", {
|
|
501
|
-
description: "Configure Telegram bot token
|
|
490
|
+
description: "<profile> — Configure Telegram bot token",
|
|
502
491
|
handler: async (args, ctx) => {
|
|
503
492
|
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
504
493
|
},
|
|
@@ -513,28 +502,17 @@ export function registerTelegramBridgeCommands(
|
|
|
513
502
|
},
|
|
514
503
|
});
|
|
515
504
|
pi.registerCommand("telegram-connect", {
|
|
516
|
-
description:
|
|
517
|
-
"Start the Telegram bridge. Use /telegram-connect <profile> and optional as=Name for a fresh Workspace Thread.",
|
|
505
|
+
description: "<profile> — Start Telegram bridge",
|
|
518
506
|
handler: async (args, ctx) => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const requestedThreadName = parseTelegramRequestedThreadName(args);
|
|
525
|
-
const requestedNameError =
|
|
526
|
-
requestedNameTokens.length > 1
|
|
527
|
-
? "Specify at most one as=Name Workspace Thread name."
|
|
528
|
-
: requestedNameTokens.length === 1 && !requestedThreadName
|
|
529
|
-
? "Usage: /telegram-connect [profile] as=Flightprice"
|
|
530
|
-
: requestedThreadName
|
|
531
|
-
? deps.validateThreadName?.(requestedThreadName)
|
|
532
|
-
: undefined;
|
|
533
|
-
if (requestedNameError) {
|
|
534
|
-
ctx.ui.notify(requestedNameError, "warning");
|
|
507
|
+
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
508
|
+
ctx.ui.notify(
|
|
509
|
+
"Thread names are configured from Telegram, not from Pi commands.",
|
|
510
|
+
"warning",
|
|
511
|
+
);
|
|
535
512
|
deps.updateStatus(ctx);
|
|
536
513
|
return;
|
|
537
514
|
}
|
|
515
|
+
const profileName = parseTelegramProfileArg(args);
|
|
538
516
|
if (profileName && deps.activateProfileConfig) {
|
|
539
517
|
const ok = await deps.activateProfileConfig(ctx, profileName);
|
|
540
518
|
if (!ok) {
|
|
@@ -597,7 +575,6 @@ export function registerTelegramBridgeCommands(
|
|
|
597
575
|
};
|
|
598
576
|
let result = await startWithRecovery({
|
|
599
577
|
forceFreshLeaderThread: true,
|
|
600
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
601
578
|
});
|
|
602
579
|
if (result && !result.ok && result.canTakeover) {
|
|
603
580
|
const confirmed = await ctx.ui.confirm(
|
|
@@ -612,7 +589,6 @@ export function registerTelegramBridgeCommands(
|
|
|
612
589
|
result = await startWithRecovery({
|
|
613
590
|
force: true,
|
|
614
591
|
forceFreshLeaderThread: true,
|
|
615
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
616
592
|
});
|
|
617
593
|
}
|
|
618
594
|
if (result?.message) {
|
|
@@ -625,8 +601,7 @@ export function registerTelegramBridgeCommands(
|
|
|
625
601
|
},
|
|
626
602
|
});
|
|
627
603
|
pi.registerCommand("telegram-disconnect", {
|
|
628
|
-
description:
|
|
629
|
-
"Stop Telegram; in Threaded Mode, delete this instance's current thread",
|
|
604
|
+
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
630
605
|
handler: async (_args, ctx) => {
|
|
631
606
|
const threadName = deps.getDisconnectThreadName?.();
|
|
632
607
|
if (threadName) {
|
|
@@ -2337,7 +2312,7 @@ async function handleTelegramCommandRuntime<
|
|
|
2337
2312
|
|
|
2338
2313
|
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
2339
2314
|
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
|
|
2340
|
-
"
|
|
2315
|
+
"Internal Telegram command cannot be run manually";
|
|
2341
2316
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
|
|
2342
2317
|
"This internal Telegram command cannot be run manually.";
|
|
2343
2318
|
|
|
@@ -2542,6 +2517,7 @@ export function createTelegramSessionActionAssembly(
|
|
|
2542
2517
|
|
|
2543
2518
|
type TelegramPendingInternalAction = {
|
|
2544
2519
|
kind: "replace-session";
|
|
2520
|
+
token: string;
|
|
2545
2521
|
updateId: number;
|
|
2546
2522
|
target: { chatId: number; threadId?: number; messageId: number };
|
|
2547
2523
|
};
|
|
@@ -2582,9 +2558,9 @@ export function createTelegramSessionActionRuntime(
|
|
|
2582
2558
|
registered = true;
|
|
2583
2559
|
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
2584
2560
|
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
2585
|
-
handler: async (
|
|
2561
|
+
handler: async (args, ctx) => {
|
|
2586
2562
|
const action = pendingAction;
|
|
2587
|
-
if (!action) {
|
|
2563
|
+
if (!action || args.trim() !== action.token) {
|
|
2588
2564
|
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
2589
2565
|
return;
|
|
2590
2566
|
}
|
|
@@ -2616,10 +2592,11 @@ export function createTelegramSessionActionRuntime(
|
|
|
2616
2592
|
const target = pendingTarget;
|
|
2617
2593
|
pendingTarget = undefined;
|
|
2618
2594
|
if (!target) return;
|
|
2619
|
-
|
|
2595
|
+
const token = randomUUID();
|
|
2596
|
+
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
2620
2597
|
void Promise.resolve()
|
|
2621
2598
|
.then(() =>
|
|
2622
|
-
deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
2599
|
+
deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
|
|
2623
2600
|
expandPromptTemplates: true,
|
|
2624
2601
|
}),
|
|
2625
2602
|
)
|
package/lib/extension.ts
CHANGED
|
@@ -104,7 +104,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
104
104
|
WorkspaceAdmission.createTelegramWorkspaceAdmissionRuntimeBinding({
|
|
105
105
|
getProfileName: configStore.getActiveProfileName,
|
|
106
106
|
getBotToken: configStore.getBotToken,
|
|
107
|
-
getPath: Paths.
|
|
107
|
+
getPath: Paths.resolveTelegramWorkspaceAdmissionPathForProfile,
|
|
108
108
|
owner: {
|
|
109
109
|
processId: telegramProcessId,
|
|
110
110
|
processBirthId: telegramQueueProcessBirthId,
|
|
@@ -187,7 +187,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
187
187
|
},
|
|
188
188
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
189
189
|
},
|
|
190
|
-
getLeaderJournalPath: Paths.
|
|
190
|
+
getLeaderJournalPath: Paths.resolveTelegramUpdateJournalPathForProfile,
|
|
191
191
|
getFollowerJournalPath(bindingKey, profileName) {
|
|
192
192
|
return Paths.resolveTelegramFollowerJournalPath(
|
|
193
193
|
bindingKey,
|
|
@@ -387,6 +387,51 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
387
387
|
: "clear";
|
|
388
388
|
},
|
|
389
389
|
});
|
|
390
|
+
const reclaimTelegramWorkspaceDeadOwnerQueue =
|
|
391
|
+
WorkspaceRetirement.createTelegramWorkspaceDeadOwnerQueueReclaimer({
|
|
392
|
+
getExternalProtection: captureWorkspaceExternalProtection,
|
|
393
|
+
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
394
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
395
|
+
resolveLeaderJournal: resolveTelegramUpdateJournalBinding,
|
|
396
|
+
createFollowerJournalResolver:
|
|
397
|
+
telegramJournalBindingRuntime.createRecipientResolver,
|
|
398
|
+
discoverFollowerJournals() {
|
|
399
|
+
return Journal.discoverTelegramFollowerJournalPaths({
|
|
400
|
+
directory: Paths.resolveTelegramTempDir(),
|
|
401
|
+
profileName: configStore.getActiveProfileName(),
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
createJournalPathResolver: telegramJournalBindingRuntime.createPathResolver,
|
|
405
|
+
withJournalReference(binding, operation) {
|
|
406
|
+
if (!binding.recoveryKey) throw new Error(
|
|
407
|
+
"Telegram workspace journal reference identity is unavailable.",
|
|
408
|
+
);
|
|
409
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
410
|
+
referenceClass: "workspace-retirement",
|
|
411
|
+
recoveryKey: binding.recoveryKey,
|
|
412
|
+
}, operation);
|
|
413
|
+
},
|
|
414
|
+
getRecoveryOwner() {
|
|
415
|
+
return {
|
|
416
|
+
instanceId: telegramInstanceId,
|
|
417
|
+
processId: telegramProcessId,
|
|
418
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
419
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
getQueueOwnerLiveness: Bus.getTelegramProcessLiveness,
|
|
423
|
+
onMutationError(error) {
|
|
424
|
+
recordRuntimeEvent("Telegram dead-owner queue reclamation was refused.", {
|
|
425
|
+
error: error instanceof Error ? error.message : String(error),
|
|
426
|
+
});
|
|
427
|
+
},
|
|
428
|
+
isBindingCurrent(binding) {
|
|
429
|
+
return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(
|
|
430
|
+
threadStore,
|
|
431
|
+
binding,
|
|
432
|
+
);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
390
435
|
const inactiveThreadCleanupReviewRuntime =
|
|
391
436
|
ThreadCleanupManager.createTelegramInactiveThreadCleanupReviewRuntime({
|
|
392
437
|
getProfileName() { return configStore.getActiveProfileName() ?? "default"; },
|
|
@@ -871,6 +916,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
871
916
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
872
917
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
873
918
|
updateStatus,
|
|
919
|
+
dismissGuestPlaceholder: guestPlaceholderRuntime.dismiss,
|
|
874
920
|
});
|
|
875
921
|
const threadDisplaySettingsRuntime =
|
|
876
922
|
ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
|
|
@@ -1409,6 +1455,18 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1409
1455
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
1410
1456
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1411
1457
|
captureWorkspaceExternalProtection,
|
|
1458
|
+
workspaceRotation: {
|
|
1459
|
+
getAdmission: workspaceAdmissionRuntime.resolve,
|
|
1460
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
1461
|
+
deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
|
|
1462
|
+
reclaimDeadOwnerQueuedWork(binding, isCurrent) {
|
|
1463
|
+
return telegramWorkspaceOperationRuntime.run({
|
|
1464
|
+
operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
|
|
1465
|
+
operationKind: "workspace.reclaim-dead-owner-queue",
|
|
1466
|
+
scopes: [{ kind: "target", target: binding.target }],
|
|
1467
|
+
}, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1412
1470
|
callApi(method, body, options) {
|
|
1413
1471
|
return directTelegramApiRuntime.call(method, body, options);
|
|
1414
1472
|
},
|
|
@@ -1544,6 +1602,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1544
1602
|
recordRuntimeEvent,
|
|
1545
1603
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1546
1604
|
});
|
|
1605
|
+
const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
|
|
1606
|
+
instanceId: telegramInstanceId,
|
|
1607
|
+
topicTargetStore: threadStore,
|
|
1608
|
+
getCurrentLeaderEpoch,
|
|
1609
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1610
|
+
isPollingSuspended: lockedPollingRuntime.isSuspended,
|
|
1611
|
+
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1612
|
+
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1613
|
+
});
|
|
1547
1614
|
const telegramBridgeSessionLifecycleDeps =
|
|
1548
1615
|
Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1549
1616
|
contextStore: telegramSessionContextStore,
|
|
@@ -1598,6 +1665,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1598
1665
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1599
1666
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1600
1667
|
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1668
|
+
prepareThreadPreservationOnQuit,
|
|
1601
1669
|
},
|
|
1602
1670
|
});
|
|
1603
1671
|
const sessionLifecycleRuntime =
|
|
@@ -1731,14 +1799,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1731
1799
|
if (!record?.target.threadId) return undefined;
|
|
1732
1800
|
return record.threadName ?? "current Telegram thread";
|
|
1733
1801
|
},
|
|
1734
|
-
setRequestedThreadNameForPollingStart:
|
|
1735
|
-
telegramThreadCapabilityState.setRequestedThreadName,
|
|
1736
|
-
validateThreadName(threadName) {
|
|
1737
|
-
return Threads.getTelegramTopicThreadNameValidationError(
|
|
1738
|
-
threadName,
|
|
1739
|
-
undefined,
|
|
1740
|
-
);
|
|
1741
|
-
},
|
|
1742
1802
|
onTransportChanged() {
|
|
1743
1803
|
deliveryLifecycleRuntime.onSessionStart();
|
|
1744
1804
|
activityVerbosityRuntime.reset();
|
package/lib/journal.ts
CHANGED
|
@@ -2428,6 +2428,7 @@ export interface TelegramUpdateJournalRuntimeBinding {
|
|
|
2428
2428
|
runtimeKey: string;
|
|
2429
2429
|
recoveryKey: string;
|
|
2430
2430
|
journal: TelegramUpdateJournalStore;
|
|
2431
|
+
readForProtection?: () => { entries: readonly TelegramUpdateJournalEntry[] };
|
|
2431
2432
|
}
|
|
2432
2433
|
|
|
2433
2434
|
export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
|
|
@@ -2470,6 +2471,19 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(
|
|
|
2470
2471
|
profileName,
|
|
2471
2472
|
botIdentity,
|
|
2472
2473
|
}),
|
|
2474
|
+
readForProtection() {
|
|
2475
|
+
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
2476
|
+
const evidence = inspectTelegramUpdateJournalFamily({
|
|
2477
|
+
directory: dirname(path), path, profile: profileName, botIdentity,
|
|
2478
|
+
limits: {
|
|
2479
|
+
maxFiles: 1024,
|
|
2480
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
2481
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
2482
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
2483
|
+
},
|
|
2484
|
+
});
|
|
2485
|
+
return { entries: evidence.kind === "present" ? evidence.file.entries : [] };
|
|
2486
|
+
},
|
|
2473
2487
|
journal: createTelegramUpdateJournalStore({
|
|
2474
2488
|
path,
|
|
2475
2489
|
profileName,
|
package/lib/lifecycle.ts
CHANGED
|
@@ -244,7 +244,7 @@ export function createTelegramSessionGenerationFence(
|
|
|
244
244
|
const generation = store.getGeneration();
|
|
245
245
|
if (!store.isCurrent(ctx, generation)) return;
|
|
246
246
|
await hooks.onSessionShutdown(event, ctx);
|
|
247
|
-
store.clear(ctx);
|
|
247
|
+
if (store.isCurrent(ctx, generation)) store.clear(ctx);
|
|
248
248
|
},
|
|
249
249
|
};
|
|
250
250
|
}
|
|
@@ -266,6 +266,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
266
266
|
capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
|
|
267
267
|
queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
|
|
268
268
|
guestPlaceholder?: { stopAll(): void };
|
|
269
|
+
prepareThreadPreservationOnQuit?: (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
export interface TelegramBridgeSessionLifecycleAssemblyDeps<
|
|
@@ -322,6 +323,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
|
|
|
322
323
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
323
324
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
324
325
|
guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
|
|
326
|
+
prepareThreadPreservationOnQuit?: TelegramBridgeSessionServiceRuntime["prepareThreadPreservationOnQuit"];
|
|
325
327
|
};
|
|
326
328
|
}
|
|
327
329
|
|
|
@@ -350,6 +352,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
|
|
|
350
352
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
351
353
|
queueWatchdog: ports.services.queueWatchdog,
|
|
352
354
|
guestPlaceholder: ports.services.guestPlaceholder,
|
|
355
|
+
prepareThreadPreservationOnQuit: ports.services.prepareThreadPreservationOnQuit,
|
|
353
356
|
},
|
|
354
357
|
};
|
|
355
358
|
}
|
|
@@ -385,15 +388,31 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
385
388
|
deps.services.queueWatchdog.start(ctx);
|
|
386
389
|
},
|
|
387
390
|
async onSessionShutdown(event, ctx) {
|
|
388
|
-
|
|
391
|
+
const generation = deps.contextStore.getGeneration();
|
|
392
|
+
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
393
|
+
if (!isCurrent()) return;
|
|
394
|
+
let preserveThread: (() => Promise<void>) | undefined;
|
|
395
|
+
if (event.reason === "quit") {
|
|
396
|
+
try {
|
|
397
|
+
preserveThread = deps.services.prepareThreadPreservationOnQuit?.(isCurrent);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
389
402
|
deps.services.guestPlaceholder?.stopAll();
|
|
390
403
|
await deps.services.delivery.onSessionShutdown();
|
|
391
|
-
if (!
|
|
404
|
+
if (!isCurrent()) return;
|
|
392
405
|
deps.services.queueWatchdog.stop();
|
|
393
406
|
deps.services.capabilityMonitor.stop();
|
|
394
407
|
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
395
|
-
if (!
|
|
408
|
+
if (!isCurrent()) return;
|
|
396
409
|
await deps.services.inboundWorker.onSessionShutdown();
|
|
410
|
+
if (!isCurrent()) return;
|
|
411
|
+
try {
|
|
412
|
+
await preserveThread?.();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
415
|
+
}
|
|
397
416
|
},
|
|
398
417
|
};
|
|
399
418
|
const followerLifecycle = appendTelegramLifecycleHooks(
|
package/lib/locks.ts
CHANGED
|
@@ -739,7 +739,8 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
739
739
|
process.kill(pid, 0);
|
|
740
740
|
return true;
|
|
741
741
|
} catch (error) {
|
|
742
|
-
|
|
742
|
+
// Only an absent PID proves death; permission and unexpected failures do not.
|
|
743
|
+
return (error as { code?: string }).code !== "ESRCH";
|
|
743
744
|
}
|
|
744
745
|
}
|
|
745
746
|
|
|
@@ -1126,6 +1127,7 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1126
1127
|
) => Promise<TelegramLockedPollingStartResult>;
|
|
1127
1128
|
stop: () => Promise<string>;
|
|
1128
1129
|
suspend: () => Promise<void>;
|
|
1130
|
+
isSuspended: () => boolean;
|
|
1129
1131
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
1130
1132
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
1131
1133
|
registerFollowerWithOwner?: (
|
|
@@ -1190,6 +1192,9 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1190
1192
|
let takeoverCandidate: TelegramLockEntry | undefined;
|
|
1191
1193
|
let sessionAutoStartRun: Promise<void> | undefined;
|
|
1192
1194
|
let pollingGeneration = 0;
|
|
1195
|
+
let suspendedGeneration: number | undefined;
|
|
1196
|
+
let suspensionsInFlight = 0;
|
|
1197
|
+
let startupsInFlight = 0;
|
|
1193
1198
|
const ownershipCheckMs =
|
|
1194
1199
|
deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
1195
1200
|
const ownershipRefreshMs =
|
|
@@ -1201,20 +1206,29 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1201
1206
|
ownershipRefreshInterval = undefined;
|
|
1202
1207
|
};
|
|
1203
1208
|
const suspendPolling = async () => {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
if (sessionAutoStartRun) {
|
|
1210
|
-
await sessionAutoStartRun;
|
|
1209
|
+
const generation = ++pollingGeneration;
|
|
1210
|
+
suspensionsInFlight += 1;
|
|
1211
|
+
try {
|
|
1212
|
+
activeContext = undefined;
|
|
1213
|
+
deps.transportMonitor?.stop();
|
|
1211
1214
|
deps.stopFollowerRegistration?.();
|
|
1215
|
+
stopOwnershipWatcher();
|
|
1216
|
+
if (sessionAutoStartRun) {
|
|
1217
|
+
await sessionAutoStartRun;
|
|
1218
|
+
deps.stopFollowerRegistration?.();
|
|
1219
|
+
}
|
|
1220
|
+
if (ownershipStop) {
|
|
1221
|
+
await ownershipStop;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
await deps.stopPolling();
|
|
1225
|
+
// Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
|
|
1226
|
+
if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
|
|
1227
|
+
suspendedGeneration = generation;
|
|
1228
|
+
}
|
|
1229
|
+
} finally {
|
|
1230
|
+
suspensionsInFlight -= 1;
|
|
1212
1231
|
}
|
|
1213
|
-
if (ownershipStop) {
|
|
1214
|
-
await ownershipStop;
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
await deps.stopPolling();
|
|
1218
1232
|
};
|
|
1219
1233
|
const stopAfterOwnershipLoss = () => {
|
|
1220
1234
|
if (ownershipStop) return;
|
|
@@ -1261,6 +1275,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1261
1275
|
if (!isCurrent()) return false;
|
|
1262
1276
|
activeContext = ctx;
|
|
1263
1277
|
startOwnershipWatcher(ctx);
|
|
1278
|
+
startupsInFlight += 1;
|
|
1264
1279
|
try {
|
|
1265
1280
|
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
1266
1281
|
stopOwnershipWatcher();
|
|
@@ -1283,6 +1298,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1283
1298
|
deps.lock.release();
|
|
1284
1299
|
deps.onTransportAvailabilityChanged?.();
|
|
1285
1300
|
throw error;
|
|
1301
|
+
} finally {
|
|
1302
|
+
startupsInFlight -= 1;
|
|
1286
1303
|
}
|
|
1287
1304
|
if (!isCurrent()) return false;
|
|
1288
1305
|
if (deps.lock.owns(ctx)) {
|
|
@@ -1421,6 +1438,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
1421
1438
|
return "Telegram bridge disconnected.";
|
|
1422
1439
|
},
|
|
1423
1440
|
suspend: suspendPolling,
|
|
1441
|
+
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1442
|
+
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1424
1443
|
onPersistentConflict: async (ctx, count) => {
|
|
1425
1444
|
if (activeContext === undefined || ownershipStop) return;
|
|
1426
1445
|
if (!(deps.isContextCurrent?.(ctx) ?? activeContext === ctx)) return;
|
package/lib/menu-queue.ts
CHANGED
|
@@ -31,6 +31,8 @@ type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
|
31
31
|
interface TelegramQueueMenuItem {
|
|
32
32
|
chatId: number;
|
|
33
33
|
replyToMessageId: number;
|
|
34
|
+
queueOrder: number;
|
|
35
|
+
isGuest: boolean;
|
|
34
36
|
queuePosition: number;
|
|
35
37
|
isPriority: boolean;
|
|
36
38
|
priorityEmoji?: string;
|
|
@@ -60,6 +62,8 @@ function toTelegramQueueMenuItems<Context>(
|
|
|
60
62
|
return {
|
|
61
63
|
chatId: item.chatId,
|
|
62
64
|
replyToMessageId: item.replyToMessageId,
|
|
65
|
+
queueOrder: item.queueOrder,
|
|
66
|
+
isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
|
|
63
67
|
queuePosition: index + 1,
|
|
64
68
|
isPriority: item.queueLane === "priority",
|
|
65
69
|
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
@@ -106,7 +110,9 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
106
110
|
return [
|
|
107
111
|
{
|
|
108
112
|
text: label,
|
|
109
|
-
callback_data:
|
|
113
|
+
callback_data: item.isGuest
|
|
114
|
+
? `queue:guest-pick:${item.queueOrder}`
|
|
115
|
+
: `queue:pick:${item.chatId}:${item.replyToMessageId}`,
|
|
110
116
|
},
|
|
111
117
|
];
|
|
112
118
|
});
|
|
@@ -178,6 +184,22 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
|
178
184
|
return `${heading}\n${preview}`;
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
function buildTelegramGuestQueueItemSubmenuReplyMarkup(
|
|
188
|
+
queueOrder: number,
|
|
189
|
+
): TelegramQueueMenuReplyMarkup {
|
|
190
|
+
return {
|
|
191
|
+
inline_keyboard: [
|
|
192
|
+
[{ text: "⬆️ Back", callback_data: "queue:list" }],
|
|
193
|
+
[
|
|
194
|
+
{
|
|
195
|
+
text: "🔴 Skip",
|
|
196
|
+
callback_data: `queue:guest-skip:${queueOrder}`,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
182
204
|
chatId: number,
|
|
183
205
|
replyToMessageId: number,
|
|
@@ -217,6 +239,8 @@ interface TelegramQueueMenuCallbackDeps<Context = unknown> {
|
|
|
217
239
|
chatId: number,
|
|
218
240
|
replyToMessageId: number,
|
|
219
241
|
) => TelegramQueueMenuItem | undefined;
|
|
242
|
+
findGuestItem: (queueOrder: number) => TelegramQueueMenuItem | undefined;
|
|
243
|
+
skipGuest: (queueOrder: number, ctx: Context) => Promise<boolean>;
|
|
220
244
|
togglePriority: (chatId: number, replyToMessageId: number) => boolean;
|
|
221
245
|
setPriority: (
|
|
222
246
|
chatId: number,
|
|
@@ -275,6 +299,38 @@ async function handleTelegramQueueMenuCallback<Context>(
|
|
|
275
299
|
);
|
|
276
300
|
return true;
|
|
277
301
|
}
|
|
302
|
+
const guestPickMatch = data.match(/^queue:guest-pick:(\d+)$/);
|
|
303
|
+
if (guestPickMatch) {
|
|
304
|
+
await handleTelegramGuestQueueMenuPick(
|
|
305
|
+
callbackQueryId,
|
|
306
|
+
replyChatId,
|
|
307
|
+
replyMessageId,
|
|
308
|
+
Number(guestPickMatch[1]),
|
|
309
|
+
deps,
|
|
310
|
+
);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
|
|
314
|
+
if (guestSkipMatch) {
|
|
315
|
+
const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
|
|
316
|
+
if (!skipped) {
|
|
317
|
+
await refreshStaleTelegramQueueMenuItem(
|
|
318
|
+
callbackQueryId,
|
|
319
|
+
replyChatId,
|
|
320
|
+
replyMessageId,
|
|
321
|
+
deps,
|
|
322
|
+
);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
await updateTelegramQueueMenuList(
|
|
326
|
+
callbackQueryId,
|
|
327
|
+
replyChatId,
|
|
328
|
+
replyMessageId,
|
|
329
|
+
deps,
|
|
330
|
+
"Guest prompt skipped.",
|
|
331
|
+
);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
278
334
|
const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
|
|
279
335
|
if (pickMatch) {
|
|
280
336
|
await handleTelegramQueueMenuPick(
|
|
@@ -379,6 +435,31 @@ async function refreshStaleTelegramQueueMenuItem<Context>(
|
|
|
379
435
|
);
|
|
380
436
|
}
|
|
381
437
|
|
|
438
|
+
async function handleTelegramGuestQueueMenuPick<Context>(
|
|
439
|
+
callbackQueryId: string,
|
|
440
|
+
replyChatId: number,
|
|
441
|
+
replyMessageId: number,
|
|
442
|
+
queueOrder: number,
|
|
443
|
+
deps: TelegramQueueMenuCallbackDeps<Context>,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
const item = deps.findGuestItem(queueOrder);
|
|
446
|
+
if (!item) {
|
|
447
|
+
return refreshStaleTelegramQueueMenuItem(
|
|
448
|
+
callbackQueryId,
|
|
449
|
+
replyChatId,
|
|
450
|
+
replyMessageId,
|
|
451
|
+
deps,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
await deps.updateQueueMessage(
|
|
455
|
+
replyChatId,
|
|
456
|
+
replyMessageId,
|
|
457
|
+
getTelegramQueueMenuItemText(item),
|
|
458
|
+
buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder),
|
|
459
|
+
);
|
|
460
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
461
|
+
}
|
|
462
|
+
|
|
382
463
|
async function handleTelegramQueueMenuPick<Context>(
|
|
383
464
|
callbackQueryId: string,
|
|
384
465
|
replyChatId: number,
|
|
@@ -606,6 +687,7 @@ export function createTelegramQueueMenuRuntime<
|
|
|
606
687
|
ctx: Context,
|
|
607
688
|
) => Promise<void>;
|
|
608
689
|
updateStatus: (ctx: Context) => void;
|
|
690
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
609
691
|
}): TelegramQueueMenuRuntime<Context> {
|
|
610
692
|
const sendQueueMenuMessage = createQueueMenuSendMessageAdapter(
|
|
611
693
|
deps.sendInteractiveMessage,
|
|
@@ -628,6 +710,7 @@ export function createTelegramQueueMenuRuntime<
|
|
|
628
710
|
updateStatusMessage: deps.updateStatusMessage,
|
|
629
711
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
630
712
|
updateStatus: deps.updateStatus,
|
|
713
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
631
714
|
}),
|
|
632
715
|
};
|
|
633
716
|
}
|
|
@@ -695,6 +778,7 @@ function createQueueMenuCallbackHandler<
|
|
|
695
778
|
text?: string,
|
|
696
779
|
) => Promise<void>;
|
|
697
780
|
updateStatus: (ctx: Context) => void;
|
|
781
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
698
782
|
}) {
|
|
699
783
|
return async (
|
|
700
784
|
query: TelegramQueueMenuCallbackQuery,
|
|
@@ -737,6 +821,11 @@ function createQueueMenuCallbackHandler<
|
|
|
737
821
|
const findItem = (cId: number, rId: number) => {
|
|
738
822
|
return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
|
|
739
823
|
};
|
|
824
|
+
const findGuestItem = (queueOrder: number) => {
|
|
825
|
+
return toMenuItems().find((item) => {
|
|
826
|
+
return item.isGuest && item.queueOrder === queueOrder;
|
|
827
|
+
});
|
|
828
|
+
};
|
|
740
829
|
return handleTelegramQueueMenuCallback(
|
|
741
830
|
query.id,
|
|
742
831
|
data,
|
|
@@ -746,6 +835,14 @@ function createQueueMenuCallbackHandler<
|
|
|
746
835
|
{
|
|
747
836
|
getQueuedItems: toMenuItems,
|
|
748
837
|
findItem,
|
|
838
|
+
findGuestItem,
|
|
839
|
+
skipGuest: (queueOrder, callbackContext) => {
|
|
840
|
+
return skipQueuedTelegramGuestPrompt(queueOrder, callbackContext, {
|
|
841
|
+
getQueueSnapshot,
|
|
842
|
+
queueMutationRuntime: deps.queueMutationRuntime,
|
|
843
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
844
|
+
});
|
|
845
|
+
},
|
|
749
846
|
togglePriority: (cId, rId) => {
|
|
750
847
|
return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
|
|
751
848
|
getQueueSnapshot,
|
|
@@ -772,6 +869,34 @@ function createQueueMenuCallbackHandler<
|
|
|
772
869
|
};
|
|
773
870
|
}
|
|
774
871
|
|
|
872
|
+
async function skipQueuedTelegramGuestPrompt<Context>(
|
|
873
|
+
queueOrder: number,
|
|
874
|
+
ctx: Context,
|
|
875
|
+
deps: {
|
|
876
|
+
getQueueSnapshot: () => Queue.TelegramQueueItem<Context>[];
|
|
877
|
+
queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
|
|
878
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
879
|
+
},
|
|
880
|
+
): Promise<boolean> {
|
|
881
|
+
const item = deps.getQueueSnapshot().find((candidate) => {
|
|
882
|
+
return candidate.kind === "prompt" &&
|
|
883
|
+
candidate.guestQueryId !== undefined &&
|
|
884
|
+
candidate.queueOrder === queueOrder;
|
|
885
|
+
});
|
|
886
|
+
if (!item || !deps.queueMutationRuntime.removeGuestPromptByQueueOrder) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
const removed = deps.queueMutationRuntime.removeGuestPromptByQueueOrder(
|
|
890
|
+
queueOrder,
|
|
891
|
+
ctx,
|
|
892
|
+
);
|
|
893
|
+
if (!removed) return false;
|
|
894
|
+
if (item.guestInlineMessageId && deps.dismissGuestPlaceholder) {
|
|
895
|
+
await deps.dismissGuestPlaceholder(item.guestInlineMessageId);
|
|
896
|
+
}
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
|
|
775
900
|
function getQueueMenuReactionDisposition<Context>(
|
|
776
901
|
item: Queue.TelegramQueueItem<Context>,
|
|
777
902
|
priority: boolean,
|