@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/dist/lib/extension.js
CHANGED
|
@@ -79,7 +79,7 @@ export default function (pi) {
|
|
|
79
79
|
const workspaceAdmissionRuntime = WorkspaceAdmission.createTelegramWorkspaceAdmissionRuntimeBinding({
|
|
80
80
|
getProfileName: configStore.getActiveProfileName,
|
|
81
81
|
getBotToken: configStore.getBotToken,
|
|
82
|
-
getPath: Paths.
|
|
82
|
+
getPath: Paths.resolveTelegramWorkspaceAdmissionPathForProfile,
|
|
83
83
|
owner: {
|
|
84
84
|
processId: telegramProcessId,
|
|
85
85
|
processBirthId: telegramQueueProcessBirthId,
|
|
@@ -146,7 +146,7 @@ export default function (pi) {
|
|
|
146
146
|
},
|
|
147
147
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
148
148
|
},
|
|
149
|
-
getLeaderJournalPath: Paths.
|
|
149
|
+
getLeaderJournalPath: Paths.resolveTelegramUpdateJournalPathForProfile,
|
|
150
150
|
getFollowerJournalPath(bindingKey, profileName) {
|
|
151
151
|
return Paths.resolveTelegramFollowerJournalPath(bindingKey, undefined, profileName);
|
|
152
152
|
},
|
|
@@ -295,6 +295,45 @@ export default function (pi) {
|
|
|
295
295
|
: "clear";
|
|
296
296
|
},
|
|
297
297
|
});
|
|
298
|
+
const reclaimTelegramWorkspaceDeadOwnerQueue = WorkspaceRetirement.createTelegramWorkspaceDeadOwnerQueueReclaimer({
|
|
299
|
+
getExternalProtection: captureWorkspaceExternalProtection,
|
|
300
|
+
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
301
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
302
|
+
resolveLeaderJournal: resolveTelegramUpdateJournalBinding,
|
|
303
|
+
createFollowerJournalResolver: telegramJournalBindingRuntime.createRecipientResolver,
|
|
304
|
+
discoverFollowerJournals() {
|
|
305
|
+
return Journal.discoverTelegramFollowerJournalPaths({
|
|
306
|
+
directory: Paths.resolveTelegramTempDir(),
|
|
307
|
+
profileName: configStore.getActiveProfileName(),
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
createJournalPathResolver: telegramJournalBindingRuntime.createPathResolver,
|
|
311
|
+
withJournalReference(binding, operation) {
|
|
312
|
+
if (!binding.recoveryKey)
|
|
313
|
+
throw new Error("Telegram workspace journal reference identity is unavailable.");
|
|
314
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
315
|
+
referenceClass: "workspace-retirement",
|
|
316
|
+
recoveryKey: binding.recoveryKey,
|
|
317
|
+
}, operation);
|
|
318
|
+
},
|
|
319
|
+
getRecoveryOwner() {
|
|
320
|
+
return {
|
|
321
|
+
instanceId: telegramInstanceId,
|
|
322
|
+
processId: telegramProcessId,
|
|
323
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
324
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
getQueueOwnerLiveness: Bus.getTelegramProcessLiveness,
|
|
328
|
+
onMutationError(error) {
|
|
329
|
+
recordRuntimeEvent("Telegram dead-owner queue reclamation was refused.", {
|
|
330
|
+
error: error instanceof Error ? error.message : String(error),
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
isBindingCurrent(binding) {
|
|
334
|
+
return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(threadStore, binding);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
298
337
|
const inactiveThreadCleanupReviewRuntime = ThreadCleanupManager.createTelegramInactiveThreadCleanupReviewRuntime({
|
|
299
338
|
getProfileName() { return configStore.getActiveProfileName() ?? "default"; },
|
|
300
339
|
listBindings: threadStore.listWorkspaceBindings,
|
|
@@ -603,7 +642,7 @@ export default function (pi) {
|
|
|
603
642
|
editMessageText: editTelegramMessageText,
|
|
604
643
|
},
|
|
605
644
|
});
|
|
606
|
-
const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
|
|
645
|
+
const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
|
|
607
646
|
store: telegramQueueStore,
|
|
608
647
|
queue,
|
|
609
648
|
lifecycle,
|
|
@@ -617,6 +656,9 @@ export default function (pi) {
|
|
|
617
656
|
updateStatus,
|
|
618
657
|
sendTextReply,
|
|
619
658
|
sendUserMessage,
|
|
659
|
+
reconcileNextDispatchAnnouncementReplyOwnership(item) {
|
|
660
|
+
Replies.preserveTransportReplyDedupOnNextReset(item.chatId, item.replyToMessageId, item.target);
|
|
661
|
+
},
|
|
620
662
|
recordRuntimeEvent,
|
|
621
663
|
});
|
|
622
664
|
const { finalizeMarkdownPreview, preparePreviewDelivery } = Outbound.createTelegramOutboundTextPreviewRuntime({
|
|
@@ -848,6 +890,7 @@ export default function (pi) {
|
|
|
848
890
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
849
891
|
dispatchNextQueuedTelegramTurn,
|
|
850
892
|
requestNextDispatchAnnouncement,
|
|
893
|
+
cancelNextDispatchAnnouncement,
|
|
851
894
|
requestDeferredDispatchNextQueuedTelegramTurn: deferredQueueDispatchRuntime.request,
|
|
852
895
|
hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
853
896
|
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
@@ -1169,6 +1212,18 @@ export default function (pi) {
|
|
|
1169
1212
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
1170
1213
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1171
1214
|
captureWorkspaceExternalProtection,
|
|
1215
|
+
workspaceRotation: {
|
|
1216
|
+
getAdmission: workspaceAdmissionRuntime.resolve,
|
|
1217
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
1218
|
+
deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
|
|
1219
|
+
reclaimDeadOwnerQueuedWork(binding, isCurrent) {
|
|
1220
|
+
return telegramWorkspaceOperationRuntime.run({
|
|
1221
|
+
operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
|
|
1222
|
+
operationKind: "workspace.reclaim-dead-owner-queue",
|
|
1223
|
+
scopes: [{ kind: "target", target: binding.target }],
|
|
1224
|
+
}, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1172
1227
|
callApi(method, body, options) {
|
|
1173
1228
|
return directTelegramApiRuntime.call(method, body, options);
|
|
1174
1229
|
},
|
|
@@ -1282,6 +1337,15 @@ export default function (pi) {
|
|
|
1282
1337
|
recordRuntimeEvent,
|
|
1283
1338
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1284
1339
|
});
|
|
1340
|
+
const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
|
|
1341
|
+
instanceId: telegramInstanceId,
|
|
1342
|
+
topicTargetStore: threadStore,
|
|
1343
|
+
getCurrentLeaderEpoch,
|
|
1344
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1345
|
+
isPollingSuspended: lockedPollingRuntime.isSuspended,
|
|
1346
|
+
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1347
|
+
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1348
|
+
});
|
|
1285
1349
|
const telegramBridgeSessionLifecycleDeps = Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1286
1350
|
contextStore: telegramSessionContextStore,
|
|
1287
1351
|
queue: {
|
|
@@ -1335,6 +1399,7 @@ export default function (pi) {
|
|
|
1335
1399
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1336
1400
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1337
1401
|
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1402
|
+
prepareThreadPreservationOnQuit,
|
|
1338
1403
|
},
|
|
1339
1404
|
});
|
|
1340
1405
|
const sessionLifecycleRuntime = Lifecycle.createTelegramBridgeSessionLifecycleAssembly(telegramBridgeSessionLifecycleDeps);
|
package/dist/lib/journal.d.ts
CHANGED
|
@@ -610,6 +610,9 @@ export interface TelegramUpdateJournalRuntimeBinding {
|
|
|
610
610
|
runtimeKey: string;
|
|
611
611
|
recoveryKey: string;
|
|
612
612
|
journal: TelegramUpdateJournalStore;
|
|
613
|
+
readForProtection?: () => {
|
|
614
|
+
entries: readonly TelegramUpdateJournalEntry[];
|
|
615
|
+
};
|
|
613
616
|
}
|
|
614
617
|
export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
|
|
615
618
|
getProfileName: () => string | undefined;
|
package/dist/lib/journal.js
CHANGED
|
@@ -1443,6 +1443,19 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1443
1443
|
profileName,
|
|
1444
1444
|
botIdentity,
|
|
1445
1445
|
}),
|
|
1446
|
+
readForProtection() {
|
|
1447
|
+
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
1448
|
+
const evidence = inspectTelegramUpdateJournalFamily({
|
|
1449
|
+
directory: dirname(path), path, profile: profileName, botIdentity,
|
|
1450
|
+
limits: {
|
|
1451
|
+
maxFiles: 1024,
|
|
1452
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
1453
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
1454
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
return { entries: evidence.kind === "present" ? evidence.file.entries : [] };
|
|
1458
|
+
},
|
|
1446
1459
|
journal: createTelegramUpdateJournalStore({
|
|
1447
1460
|
path,
|
|
1448
1461
|
profileName,
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export interface TelegramBridgeSessionServiceRuntime {
|
|
|
85
85
|
guestPlaceholder?: {
|
|
86
86
|
stopAll(): void;
|
|
87
87
|
};
|
|
88
|
+
prepareThreadPreservationOnQuit?: (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
88
89
|
}
|
|
89
90
|
export interface TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel = unknown> {
|
|
90
91
|
contextStore: TelegramSessionContextStore<ExtensionContext>;
|
|
@@ -111,6 +112,7 @@ export interface TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel = unknow
|
|
|
111
112
|
capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
|
|
112
113
|
queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
|
|
113
114
|
guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
|
|
115
|
+
prepareThreadPreservationOnQuit?: TelegramBridgeSessionServiceRuntime["prepareThreadPreservationOnQuit"];
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
export declare function createTelegramBridgeSessionLifecycleDeps<TQueueItem, TModel = unknown>(ports: TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel>): TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>;
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -91,7 +91,8 @@ export function createTelegramSessionGenerationFence(store, hooks) {
|
|
|
91
91
|
if (!store.isCurrent(ctx, generation))
|
|
92
92
|
return;
|
|
93
93
|
await hooks.onSessionShutdown(event, ctx);
|
|
94
|
-
store.
|
|
94
|
+
if (store.isCurrent(ctx, generation))
|
|
95
|
+
store.clear(ctx);
|
|
95
96
|
},
|
|
96
97
|
};
|
|
97
98
|
}
|
|
@@ -115,6 +116,7 @@ export function createTelegramBridgeSessionLifecycleDeps(ports) {
|
|
|
115
116
|
capabilityMonitor: ports.services.capabilityMonitor,
|
|
116
117
|
queueWatchdog: ports.services.queueWatchdog,
|
|
117
118
|
guestPlaceholder: ports.services.guestPlaceholder,
|
|
119
|
+
prepareThreadPreservationOnQuit: ports.services.prepareThreadPreservationOnQuit,
|
|
118
120
|
},
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -144,18 +146,37 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
|
|
|
144
146
|
deps.services.queueWatchdog.start(ctx);
|
|
145
147
|
},
|
|
146
148
|
async onSessionShutdown(event, ctx) {
|
|
147
|
-
|
|
149
|
+
const generation = deps.contextStore.getGeneration();
|
|
150
|
+
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
151
|
+
if (!isCurrent())
|
|
148
152
|
return;
|
|
153
|
+
let preserveThread;
|
|
154
|
+
if (event.reason === "quit") {
|
|
155
|
+
try {
|
|
156
|
+
preserveThread = deps.services.prepareThreadPreservationOnQuit?.(isCurrent);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
149
162
|
deps.services.guestPlaceholder?.stopAll();
|
|
150
163
|
await deps.services.delivery.onSessionShutdown();
|
|
151
|
-
if (!
|
|
164
|
+
if (!isCurrent())
|
|
152
165
|
return;
|
|
153
166
|
deps.services.queueWatchdog.stop();
|
|
154
167
|
deps.services.capabilityMonitor.stop();
|
|
155
168
|
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
156
|
-
if (!
|
|
169
|
+
if (!isCurrent())
|
|
157
170
|
return;
|
|
158
171
|
await deps.services.inboundWorker.onSessionShutdown();
|
|
172
|
+
if (!isCurrent())
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
await preserveThread?.();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
|
|
179
|
+
}
|
|
159
180
|
},
|
|
160
181
|
};
|
|
161
182
|
const followerLifecycle = appendTelegramLifecycleHooks(servicesLifecycle, {
|
package/dist/lib/locks.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface TelegramLockedPollingRuntime<TContext extends TelegramLockConte
|
|
|
133
133
|
start: (ctx: TContext, options?: TelegramLockedPollingStartOptions) => Promise<TelegramLockedPollingStartResult>;
|
|
134
134
|
stop: () => Promise<string>;
|
|
135
135
|
suspend: () => Promise<void>;
|
|
136
|
+
isSuspended: () => boolean;
|
|
136
137
|
onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
|
|
137
138
|
onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
|
|
138
139
|
registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
|
package/dist/lib/locks.js
CHANGED
|
@@ -551,7 +551,8 @@ export function isProcessAlive(pid) {
|
|
|
551
551
|
return true;
|
|
552
552
|
}
|
|
553
553
|
catch (error) {
|
|
554
|
-
|
|
554
|
+
// Only an absent PID proves death; permission and unexpected failures do not.
|
|
555
|
+
return error.code !== "ESRCH";
|
|
555
556
|
}
|
|
556
557
|
}
|
|
557
558
|
export function formatTelegramLockEntry(lock) {
|
|
@@ -835,6 +836,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
835
836
|
let takeoverCandidate;
|
|
836
837
|
let sessionAutoStartRun;
|
|
837
838
|
let pollingGeneration = 0;
|
|
839
|
+
let suspendedGeneration;
|
|
840
|
+
let suspensionsInFlight = 0;
|
|
841
|
+
let startupsInFlight = 0;
|
|
838
842
|
const ownershipCheckMs = deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
|
|
839
843
|
const ownershipRefreshMs = deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
|
|
840
844
|
const stopOwnershipWatcher = () => {
|
|
@@ -846,20 +850,30 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
846
850
|
ownershipRefreshInterval = undefined;
|
|
847
851
|
};
|
|
848
852
|
const suspendPolling = async () => {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (sessionAutoStartRun) {
|
|
855
|
-
await sessionAutoStartRun;
|
|
853
|
+
const generation = ++pollingGeneration;
|
|
854
|
+
suspensionsInFlight += 1;
|
|
855
|
+
try {
|
|
856
|
+
activeContext = undefined;
|
|
857
|
+
deps.transportMonitor?.stop();
|
|
856
858
|
deps.stopFollowerRegistration?.();
|
|
859
|
+
stopOwnershipWatcher();
|
|
860
|
+
if (sessionAutoStartRun) {
|
|
861
|
+
await sessionAutoStartRun;
|
|
862
|
+
deps.stopFollowerRegistration?.();
|
|
863
|
+
}
|
|
864
|
+
if (ownershipStop) {
|
|
865
|
+
await ownershipStop;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
await deps.stopPolling();
|
|
869
|
+
// Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
|
|
870
|
+
if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
|
|
871
|
+
suspendedGeneration = generation;
|
|
872
|
+
}
|
|
857
873
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return;
|
|
874
|
+
finally {
|
|
875
|
+
suspensionsInFlight -= 1;
|
|
861
876
|
}
|
|
862
|
-
await deps.stopPolling();
|
|
863
877
|
};
|
|
864
878
|
const stopAfterOwnershipLoss = () => {
|
|
865
879
|
if (ownershipStop)
|
|
@@ -906,6 +920,7 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
906
920
|
return false;
|
|
907
921
|
activeContext = ctx;
|
|
908
922
|
startOwnershipWatcher(ctx);
|
|
923
|
+
startupsInFlight += 1;
|
|
909
924
|
try {
|
|
910
925
|
if (!deps.lock.refresh(snapshotLockContext(ctx))) {
|
|
911
926
|
stopOwnershipWatcher();
|
|
@@ -934,6 +949,9 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
934
949
|
deps.onTransportAvailabilityChanged?.();
|
|
935
950
|
throw error;
|
|
936
951
|
}
|
|
952
|
+
finally {
|
|
953
|
+
startupsInFlight -= 1;
|
|
954
|
+
}
|
|
937
955
|
if (!isCurrent())
|
|
938
956
|
return false;
|
|
939
957
|
if (deps.lock.owns(ctx)) {
|
|
@@ -1075,6 +1093,8 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1075
1093
|
return "Telegram bridge disconnected.";
|
|
1076
1094
|
},
|
|
1077
1095
|
suspend: suspendPolling,
|
|
1096
|
+
isSuspended: () => suspendedGeneration === pollingGeneration &&
|
|
1097
|
+
suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
|
|
1078
1098
|
onPersistentConflict: async (ctx, count) => {
|
|
1079
1099
|
if (activeContext === undefined || ownershipStop)
|
|
1080
1100
|
return;
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface TelegramAgentDirResolutionInput {
|
|
|
14
14
|
* 3. Fallback: `~/.pi/agent`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function resolveAgentDir(input?: TelegramAgentDirResolutionInput): string;
|
|
17
|
+
/**
|
|
18
|
+
* Pure reference preflight against an independently approved resource path.
|
|
19
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
20
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
21
|
+
*/
|
|
22
|
+
export declare function requireTelegramStoragePathReference(path: string, expectedPath: string): string;
|
|
17
23
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
18
24
|
export declare function resolveTelegramConfigPath(): string;
|
|
19
25
|
/** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
|
|
@@ -28,12 +34,16 @@ export declare function getTelegramDiagnosticsDisplayPaths(profileName?: string)
|
|
|
28
34
|
};
|
|
29
35
|
/** Durable Workspace admission ledger (<agentDir>/tmp/telegram/workspace-admission[.<profile>].json). */
|
|
30
36
|
export declare function resolveTelegramWorkspaceAdmissionPath(agentDir?: string, profileName?: string): string;
|
|
37
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
38
|
+
export declare function resolveTelegramWorkspaceAdmissionPathForProfile(profileName?: string): string;
|
|
31
39
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
32
40
|
export declare function resolveTelegramThreadCleanupWorkPath(agentDir?: string, profileName?: string): string;
|
|
33
41
|
/** Durable agent-authored channel post journal. */
|
|
34
42
|
export declare function resolveTelegramChannelPostJournalPath(agentDir?: string, profileName?: string): string;
|
|
35
43
|
/** Durable inbound update journal (<agentDir>/tmp/telegram/inbox[.<profile>].json). */
|
|
36
44
|
export declare function resolveTelegramUpdateJournalPath(agentDir?: string, profileName?: string): string;
|
|
45
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
46
|
+
export declare function resolveTelegramUpdateJournalPathForProfile(profileName?: string): string;
|
|
37
47
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
38
48
|
export declare function resolveTelegramFollowerJournalPath(recipientBindingKey: string, agentDir?: string, profileName?: string): string;
|
|
39
49
|
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
package/dist/lib/paths.js
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
|
export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
|
|
14
14
|
/**
|
|
15
15
|
* Resolve the agent data directory for the current Pi-compatible runtime.
|
|
@@ -33,6 +33,19 @@ export function resolveAgentDir(input = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
return join(homedir(), ".pi", "agent");
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Pure reference preflight against an independently approved resource path.
|
|
38
|
+
* Reject aliases; never repair relative historical references or redirect storage.
|
|
39
|
+
* Equality proves spelling only, not file identity, consumer closure or migration readiness.
|
|
40
|
+
*/
|
|
41
|
+
export function requireTelegramStoragePathReference(path, expectedPath) {
|
|
42
|
+
if (typeof path !== "string" || typeof expectedPath !== "string" ||
|
|
43
|
+
!isAbsolute(path) || resolve(path) !== path ||
|
|
44
|
+
!isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
|
|
45
|
+
throw new Error("Telegram storage reference does not match its approved absolute path.");
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
36
49
|
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
37
50
|
export function resolveTelegramConfigPath() {
|
|
38
51
|
return join(resolveAgentDir(), "telegram.json");
|
|
@@ -65,6 +78,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName) {
|
|
|
65
78
|
export function resolveTelegramWorkspaceAdmissionPath(agentDir = resolveAgentDir(), profileName) {
|
|
66
79
|
return resolveTelegramProfileTempFilePath("workspace-admission", "json", agentDir, profileName);
|
|
67
80
|
}
|
|
81
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
82
|
+
export function resolveTelegramWorkspaceAdmissionPathForProfile(profileName) {
|
|
83
|
+
return resolveTelegramWorkspaceAdmissionPath(resolveAgentDir(), profileName);
|
|
84
|
+
}
|
|
68
85
|
/** Durable inactive Thread cleanup work-set journal. */
|
|
69
86
|
export function resolveTelegramThreadCleanupWorkPath(agentDir = resolveAgentDir(), profileName) {
|
|
70
87
|
return resolveTelegramProfileTempFilePath("thread-cleanup", "json", agentDir, profileName);
|
|
@@ -77,6 +94,10 @@ export function resolveTelegramChannelPostJournalPath(agentDir = resolveAgentDir
|
|
|
77
94
|
export function resolveTelegramUpdateJournalPath(agentDir = resolveAgentDir(), profileName) {
|
|
78
95
|
return resolveTelegramProfileTempFilePath("inbox", "json", agentDir, profileName);
|
|
79
96
|
}
|
|
97
|
+
/** Profile-only callback shape; binds storage to the configured agent directory. */
|
|
98
|
+
export function resolveTelegramUpdateJournalPathForProfile(profileName) {
|
|
99
|
+
return resolveTelegramUpdateJournalPath(resolveAgentDir(), profileName);
|
|
100
|
+
}
|
|
80
101
|
/** Durable follower delivery journal, isolated by stable recipient binding. */
|
|
81
102
|
export function resolveTelegramFollowerJournalPath(recipientBindingKey, agentDir = resolveAgentDir(), profileName) {
|
|
82
103
|
if (!recipientBindingKey) {
|
package/dist/lib/polling.js
CHANGED
|
@@ -16,8 +16,8 @@ const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
|
16
16
|
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
17
17
|
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
18
18
|
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
19
|
-
// Standard Telegram DM polling does not expose ordinary message-deletion events
|
|
20
|
-
//
|
|
19
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events.
|
|
20
|
+
// Business deletions belong to a separate namespace and default routing ignores them.
|
|
21
21
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
22
22
|
"message",
|
|
23
23
|
"edited_message",
|
package/dist/lib/queue.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
84
84
|
historyText: string;
|
|
85
85
|
priorityEmoji?: string;
|
|
86
86
|
reactionSuppressionEmoji?: string;
|
|
87
|
+
/** Emit the explicit aborted-turn notice when /next settles this active turn. */
|
|
88
|
+
announceNextAbortOnEnd?: boolean;
|
|
87
89
|
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
88
90
|
voiceReplyPreferred?: boolean;
|
|
89
91
|
/** Turn must be delivered as voice (voice mode) */
|
|
@@ -165,6 +167,8 @@ export interface TelegramActiveTurnStore<TTurn extends PendingTelegramTurn = Pen
|
|
|
165
167
|
has: () => boolean;
|
|
166
168
|
set: (turn: TTurn) => void;
|
|
167
169
|
clear: () => void;
|
|
170
|
+
markNextAbortAnnouncement: () => boolean;
|
|
171
|
+
clearNextAbortAnnouncement: () => boolean;
|
|
168
172
|
getChatId: () => number | undefined;
|
|
169
173
|
getTarget: () => TelegramQueueTarget | undefined;
|
|
170
174
|
getReplyToMessageId: () => number | undefined;
|
|
@@ -391,6 +395,7 @@ export interface TelegramAgentEndPlan {
|
|
|
391
395
|
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
392
396
|
shouldClearPreview: boolean;
|
|
393
397
|
shouldDispatchNext: boolean;
|
|
398
|
+
shouldSendAbortMessage: boolean;
|
|
394
399
|
shouldSendErrorMessage: boolean;
|
|
395
400
|
shouldSendAttachmentNotice: boolean;
|
|
396
401
|
}
|
|
@@ -474,7 +479,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
|
|
|
474
479
|
getActiveTurn: () => TTurn | undefined;
|
|
475
480
|
loadConfig?: () => Promise<void>;
|
|
476
481
|
extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
|
|
477
|
-
|
|
482
|
+
isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
|
|
478
483
|
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
479
484
|
resetRuntimeState: () => void;
|
|
480
485
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
@@ -517,6 +522,7 @@ export declare function buildTelegramAgentEndPlan(options: {
|
|
|
517
522
|
hasFinalText: boolean;
|
|
518
523
|
hasQueuedAttachments: boolean;
|
|
519
524
|
foldQueuedPromptsIntoHistory: boolean;
|
|
525
|
+
announceNextAbortOnEnd?: boolean;
|
|
520
526
|
}): TelegramAgentEndPlan;
|
|
521
527
|
export declare function createTelegramAgentEndHook<TTurn extends PendingTelegramTurn, TContext, TMessage, TReplyMarkup = unknown>(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup>): (event: TelegramAgentEndHookEvent<TMessage>, ctx: TContext, assistantOverride?: TelegramAgentEndAssistantResult) => Promise<void>;
|
|
522
528
|
export declare function handleTelegramAgentEndRuntime<TTurn extends PendingTelegramTurn, TReplyMarkup = unknown>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void>;
|
|
@@ -759,6 +765,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
|
|
|
759
765
|
}>["item"], ctx: TContext) => boolean;
|
|
760
766
|
sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
761
767
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
768
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (item: PendingTelegramTurn) => void;
|
|
762
769
|
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
763
770
|
hasPendingInboundQueueMutationForItem?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
764
771
|
isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
@@ -768,6 +775,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
|
|
|
768
775
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
769
776
|
dispatchNext: (ctx: TContext) => void;
|
|
770
777
|
requestNextDispatchAnnouncement: () => void;
|
|
778
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
771
779
|
}
|
|
772
780
|
export declare function executeTelegramQueueDispatchPlan<TContext = unknown>(plan: TelegramQueueDispatchAction<TContext>, deps: TelegramDispatchRuntimeDeps<TContext>): void;
|
|
773
781
|
export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<TelegramQueueDispatchControllerDeps<TContext>, "canDispatch"> & TelegramDispatchReadinessDeps<TContext>;
|