@llblab/pi-telegram 0.50.1 → 0.51.1
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 +13 -32
- package/CHANGELOG.md +16 -0
- package/README.md +4 -3
- package/dist/lib/bindings.d.ts +4 -2
- package/dist/lib/bindings.js +5 -5
- package/dist/lib/bus-follower.d.ts +4 -2
- package/dist/lib/bus-follower.js +99 -10
- 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 +5 -0
- package/dist/lib/commands.js +9 -0
- package/dist/lib/extension.js +68 -3
- 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/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 +9 -1
- package/dist/lib/queue.js +64 -13
- package/dist/lib/replies.d.ts +4 -0
- package/dist/lib/replies.js +14 -0
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +5 -0
- package/dist/lib/sync.d.ts +9 -0
- package/dist/lib/sync.js +41 -1
- 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 +22 -15
- 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 +12 -7
- package/lib/bus-follower.ts +82 -14
- package/lib/bus-leader.ts +236 -94
- package/lib/bus.ts +39 -9
- package/lib/commands.ts +19 -0
- package/lib/extension.ts +78 -2
- package/lib/journal.ts +14 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/locks.ts +32 -13
- package/lib/paths.ts +29 -1
- package/lib/polling.ts +2 -2
- package/lib/queue.ts +77 -19
- package/lib/replies.ts +18 -0
- package/lib/routing.ts +7 -0
- package/lib/sync.ts +45 -1
- 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
|
@@ -711,6 +711,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
|
711
711
|
export interface TelegramStopCommandDeps {
|
|
712
712
|
hasAbortHandler: () => boolean;
|
|
713
713
|
clearPendingModelSwitch: () => void;
|
|
714
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
714
715
|
clearQueuedTelegramItems: () => number;
|
|
715
716
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
716
717
|
abortCurrentTurn: () => void;
|
|
@@ -1146,6 +1147,8 @@ export interface TelegramCommandRuntimeDeps<
|
|
|
1146
1147
|
isContextActive?: (ctx: TContext) => boolean;
|
|
1147
1148
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
1148
1149
|
requestNextDispatchAnnouncement?: () => void;
|
|
1150
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
1151
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
1149
1152
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
1150
1153
|
dispatch: (ctx: TContext) => void,
|
|
1151
1154
|
) => void;
|
|
@@ -1352,6 +1355,7 @@ export async function handleTelegramStopCommand(
|
|
|
1352
1355
|
deps: TelegramStopCommandDeps,
|
|
1353
1356
|
): Promise<void> {
|
|
1354
1357
|
deps.clearPendingModelSwitch();
|
|
1358
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
1355
1359
|
const clearedCount = deps.clearQueuedTelegramItems();
|
|
1356
1360
|
deps.setFoldQueuedPromptsIntoHistory(false);
|
|
1357
1361
|
if (!deps.hasAbortHandler()) {
|
|
@@ -1385,6 +1389,7 @@ export async function handleTelegramAbortCommand(deps: {
|
|
|
1385
1389
|
hasAbortHandler: () => boolean;
|
|
1386
1390
|
hasActiveTelegramTurn: () => boolean;
|
|
1387
1391
|
clearPendingModelSwitch: () => void;
|
|
1392
|
+
cancelNextTransitionAnnouncements?: () => void;
|
|
1388
1393
|
abortCurrentTurn: () => void;
|
|
1389
1394
|
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
1390
1395
|
updateStatus: () => void;
|
|
@@ -1394,6 +1399,7 @@ export async function handleTelegramAbortCommand(deps: {
|
|
|
1394
1399
|
) => Promise<void>;
|
|
1395
1400
|
}): Promise<void> {
|
|
1396
1401
|
deps.clearPendingModelSwitch();
|
|
1402
|
+
deps.cancelNextTransitionAnnouncements?.();
|
|
1397
1403
|
if (!deps.hasAbortHandler()) {
|
|
1398
1404
|
await deps.sendTextReply(
|
|
1399
1405
|
formatTelegramInformationHeading("💤", "No active turn."),
|
|
@@ -1418,6 +1424,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1418
1424
|
abortCurrentTurn: () => void;
|
|
1419
1425
|
dispatchNextQueuedTurn: () => void;
|
|
1420
1426
|
requestNextDispatchAnnouncement?: () => void;
|
|
1427
|
+
markActiveTurnNextAbortAnnouncement?: () => boolean;
|
|
1421
1428
|
clearFoldForDispatch: () => void;
|
|
1422
1429
|
updateStatus: () => void;
|
|
1423
1430
|
sendTextReply: (
|
|
@@ -1439,6 +1446,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1439
1446
|
if (!deps.isIdle() && deps.hasAbortHandler()) {
|
|
1440
1447
|
deps.clearFoldForDispatch();
|
|
1441
1448
|
deps.requestNextDispatchAnnouncement?.();
|
|
1449
|
+
deps.markActiveTurnNextAbortAnnouncement?.();
|
|
1442
1450
|
deps.abortCurrentTurn();
|
|
1443
1451
|
deps.updateStatus();
|
|
1444
1452
|
return;
|
|
@@ -1880,6 +1888,11 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
1880
1888
|
updateStatus: deps.updateStatus,
|
|
1881
1889
|
isContextActive: deps.isContextActive,
|
|
1882
1890
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1891
|
+
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1892
|
+
markActiveTurnNextAbortAnnouncement:
|
|
1893
|
+
deps.markActiveTurnNextAbortAnnouncement,
|
|
1894
|
+
cancelNextTransitionAnnouncements:
|
|
1895
|
+
deps.cancelNextTransitionAnnouncements,
|
|
1883
1896
|
startTypingLoop: deps.startTypingLoop,
|
|
1884
1897
|
stopTypingLoop: deps.stopTypingLoop,
|
|
1885
1898
|
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
@@ -2037,6 +2050,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2037
2050
|
await handleTelegramStopCommand({
|
|
2038
2051
|
hasAbortHandler: deps.hasAbortHandler,
|
|
2039
2052
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2053
|
+
cancelNextTransitionAnnouncements:
|
|
2054
|
+
deps.cancelNextTransitionAnnouncements,
|
|
2040
2055
|
clearQueuedTelegramItems: () =>
|
|
2041
2056
|
deps.clearQueuedTelegramItems(commandCtx),
|
|
2042
2057
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
@@ -2113,6 +2128,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2113
2128
|
hasAbortHandler: deps.hasAbortHandler,
|
|
2114
2129
|
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
2115
2130
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
2131
|
+
cancelNextTransitionAnnouncements:
|
|
2132
|
+
deps.cancelNextTransitionAnnouncements,
|
|
2116
2133
|
abortCurrentTurn: deps.abortCurrentTurn,
|
|
2117
2134
|
setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
|
|
2118
2135
|
updateStatus: updateStatusFor(commandCtx),
|
|
@@ -2129,6 +2146,8 @@ async function handleTelegramCommandRuntime<
|
|
|
2129
2146
|
dispatchNextQueuedTurn: () =>
|
|
2130
2147
|
deps.dispatchNextQueuedTelegramTurn(commandCtx),
|
|
2131
2148
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2149
|
+
markActiveTurnNextAbortAnnouncement:
|
|
2150
|
+
deps.markActiveTurnNextAbortAnnouncement,
|
|
2132
2151
|
clearFoldForDispatch: () =>
|
|
2133
2152
|
deps.setFoldQueuedPromptsIntoHistory(false),
|
|
2134
2153
|
updateStatus: updateStatusFor(commandCtx),
|
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"; },
|
|
@@ -775,6 +820,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
775
820
|
mutation: queueMutationRuntime,
|
|
776
821
|
dispatchNext: dispatchNextQueuedTelegramTurn,
|
|
777
822
|
requestNextDispatchAnnouncement,
|
|
823
|
+
cancelNextDispatchAnnouncement,
|
|
778
824
|
watchdog: queueDispatchWatchdogRuntime,
|
|
779
825
|
} = Bindings.createTelegramQueueBindingRuntime({
|
|
780
826
|
store: telegramQueueStore,
|
|
@@ -790,6 +836,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
790
836
|
updateStatus,
|
|
791
837
|
sendTextReply,
|
|
792
838
|
sendUserMessage,
|
|
839
|
+
reconcileNextDispatchAnnouncementReplyOwnership(item) {
|
|
840
|
+
Replies.preserveTransportReplyDedupOnNextReset(
|
|
841
|
+
item.chatId,
|
|
842
|
+
item.replyToMessageId,
|
|
843
|
+
item.target,
|
|
844
|
+
);
|
|
845
|
+
},
|
|
793
846
|
recordRuntimeEvent,
|
|
794
847
|
});
|
|
795
848
|
const { finalizeMarkdownPreview, preparePreviewDelivery } =
|
|
@@ -1048,6 +1101,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1048
1101
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1049
1102
|
dispatchNextQueuedTelegramTurn,
|
|
1050
1103
|
requestNextDispatchAnnouncement,
|
|
1104
|
+
cancelNextDispatchAnnouncement,
|
|
1051
1105
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
1052
1106
|
deferredQueueDispatchRuntime.request,
|
|
1053
1107
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
@@ -1410,6 +1464,18 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1410
1464
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
1411
1465
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1412
1466
|
captureWorkspaceExternalProtection,
|
|
1467
|
+
workspaceRotation: {
|
|
1468
|
+
getAdmission: workspaceAdmissionRuntime.resolve,
|
|
1469
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
1470
|
+
deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
|
|
1471
|
+
reclaimDeadOwnerQueuedWork(binding, isCurrent) {
|
|
1472
|
+
return telegramWorkspaceOperationRuntime.run({
|
|
1473
|
+
operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
|
|
1474
|
+
operationKind: "workspace.reclaim-dead-owner-queue",
|
|
1475
|
+
scopes: [{ kind: "target", target: binding.target }],
|
|
1476
|
+
}, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1413
1479
|
callApi(method, body, options) {
|
|
1414
1480
|
return directTelegramApiRuntime.call(method, body, options);
|
|
1415
1481
|
},
|
|
@@ -1545,6 +1611,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1545
1611
|
recordRuntimeEvent,
|
|
1546
1612
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1547
1613
|
});
|
|
1614
|
+
const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
|
|
1615
|
+
instanceId: telegramInstanceId,
|
|
1616
|
+
topicTargetStore: threadStore,
|
|
1617
|
+
getCurrentLeaderEpoch,
|
|
1618
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1619
|
+
isPollingSuspended: lockedPollingRuntime.isSuspended,
|
|
1620
|
+
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1621
|
+
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1622
|
+
});
|
|
1548
1623
|
const telegramBridgeSessionLifecycleDeps =
|
|
1549
1624
|
Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1550
1625
|
contextStore: telegramSessionContextStore,
|
|
@@ -1599,6 +1674,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1599
1674
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1600
1675
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1601
1676
|
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1677
|
+
prepareThreadPreservationOnQuit,
|
|
1602
1678
|
},
|
|
1603
1679
|
});
|
|
1604
1680
|
const sessionLifecycleRuntime =
|
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/paths.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { join, resolve } from "node:path";
|
|
12
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
13
13
|
|
|
14
14
|
export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
|
|
15
15
|
|
|
@@ -43,6 +43,20 @@ export function resolveAgentDir(
|
|
|
43
43
|
return join(homedir(), ".pi", "agent");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Pure reference preflight against an independently approved resource path.
|
|
48
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
49
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
50
|
+
*/
|
|
51
|
+
export function requireTelegramStoragePathReference(path: string, expectedPath: string): string {
|
|
52
|
+
if (typeof path !== "string" || typeof expectedPath !== "string" ||
|
|
53
|
+
!isAbsolute(path) || resolve(path) !== path ||
|
|
54
|
+
!isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
|
|
55
|
+
throw new Error("Telegram storage reference does not match its approved absolute path.");
|
|
56
|
+
}
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
47
61
|
export function resolveTelegramConfigPath(): string {
|
|
48
62
|
return join(resolveAgentDir(), "telegram.json");
|
|
@@ -100,6 +114,13 @@ export function resolveTelegramWorkspaceAdmissionPath(
|
|
|
100
114
|
);
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
118
|
+
export function resolveTelegramWorkspaceAdmissionPathForProfile(
|
|
119
|
+
profileName?: string,
|
|
120
|
+
): string {
|
|
121
|
+
return resolveTelegramWorkspaceAdmissionPath(resolveAgentDir(), profileName);
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
104
125
|
export function resolveTelegramThreadCleanupWorkPath(
|
|
105
126
|
agentDir = resolveAgentDir(),
|
|
@@ -129,6 +150,13 @@ export function resolveTelegramUpdateJournalPath(
|
|
|
129
150
|
);
|
|
130
151
|
}
|
|
131
152
|
|
|
153
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
154
|
+
export function resolveTelegramUpdateJournalPathForProfile(
|
|
155
|
+
profileName?: string,
|
|
156
|
+
): string {
|
|
157
|
+
return resolveTelegramUpdateJournalPath(resolveAgentDir(), profileName);
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
133
161
|
export function resolveTelegramFollowerJournalPath(
|
|
134
162
|
recipientBindingKey: string,
|
package/lib/polling.ts
CHANGED
|
@@ -28,8 +28,8 @@ const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
|
28
28
|
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
29
29
|
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
30
30
|
|
|
31
|
-
// Standard Telegram DM polling does not expose ordinary message-deletion events
|
|
32
|
-
//
|
|
31
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events.
|
|
32
|
+
// Business deletions belong to a separate namespace and default routing ignores them.
|
|
33
33
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
34
34
|
"message",
|
|
35
35
|
"edited_message",
|