@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/dist/lib/commands.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: telegram controls, pi agent commands, queue controls
|
|
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
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
|
|
7
8
|
import { escapeHtml } from "./rendering.js";
|
|
8
9
|
import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
|
|
@@ -241,14 +242,6 @@ function parseTelegramProfileArg(args) {
|
|
|
241
242
|
return undefined;
|
|
242
243
|
return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word;
|
|
243
244
|
}
|
|
244
|
-
export function parseTelegramRequestedThreadName(args) {
|
|
245
|
-
const token = args
|
|
246
|
-
.trim()
|
|
247
|
-
.split(/\s+/)
|
|
248
|
-
.find((word) => /^as=/i.test(word));
|
|
249
|
-
const value = token?.slice(3).trim();
|
|
250
|
-
return value || undefined;
|
|
251
|
-
}
|
|
252
245
|
function formatTelegramTakeoverTitle(ctx) {
|
|
253
246
|
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
254
247
|
}
|
|
@@ -262,7 +255,7 @@ function formatTelegramTakeoverPrompt(ctx, owner) {
|
|
|
262
255
|
}
|
|
263
256
|
export function registerTelegramBridgeCommands(pi, deps) {
|
|
264
257
|
pi.registerCommand("telegram-setup", {
|
|
265
|
-
description: "Configure Telegram bot token
|
|
258
|
+
description: "<profile> — Configure Telegram bot token",
|
|
266
259
|
handler: async (args, ctx) => {
|
|
267
260
|
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
268
261
|
},
|
|
@@ -275,26 +268,14 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
275
268
|
},
|
|
276
269
|
});
|
|
277
270
|
pi.registerCommand("telegram-connect", {
|
|
278
|
-
description: "
|
|
271
|
+
description: "<profile> — Start Telegram bridge",
|
|
279
272
|
handler: async (args, ctx) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.trim()
|
|
283
|
-
.split(/\s+/)
|
|
284
|
-
.filter((word) => /^as=/i.test(word));
|
|
285
|
-
const requestedThreadName = parseTelegramRequestedThreadName(args);
|
|
286
|
-
const requestedNameError = requestedNameTokens.length > 1
|
|
287
|
-
? "Specify at most one as=Name Workspace Thread name."
|
|
288
|
-
: requestedNameTokens.length === 1 && !requestedThreadName
|
|
289
|
-
? "Usage: /telegram-connect [profile] as=Flightprice"
|
|
290
|
-
: requestedThreadName
|
|
291
|
-
? deps.validateThreadName?.(requestedThreadName)
|
|
292
|
-
: undefined;
|
|
293
|
-
if (requestedNameError) {
|
|
294
|
-
ctx.ui.notify(requestedNameError, "warning");
|
|
273
|
+
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
274
|
+
ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
|
|
295
275
|
deps.updateStatus(ctx);
|
|
296
276
|
return;
|
|
297
277
|
}
|
|
278
|
+
const profileName = parseTelegramProfileArg(args);
|
|
298
279
|
if (profileName && deps.activateProfileConfig) {
|
|
299
280
|
const ok = await deps.activateProfileConfig(ctx, profileName);
|
|
300
281
|
if (!ok) {
|
|
@@ -357,7 +338,6 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
357
338
|
};
|
|
358
339
|
let result = await startWithRecovery({
|
|
359
340
|
forceFreshLeaderThread: true,
|
|
360
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
361
341
|
});
|
|
362
342
|
if (result && !result.ok && result.canTakeover) {
|
|
363
343
|
const confirmed = await ctx.ui.confirm(formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner));
|
|
@@ -369,7 +349,6 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
369
349
|
result = await startWithRecovery({
|
|
370
350
|
force: true,
|
|
371
351
|
forceFreshLeaderThread: true,
|
|
372
|
-
...(requestedThreadName ? { requestedThreadName } : {}),
|
|
373
352
|
});
|
|
374
353
|
}
|
|
375
354
|
if (result?.message) {
|
|
@@ -382,7 +361,7 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
382
361
|
},
|
|
383
362
|
});
|
|
384
363
|
pi.registerCommand("telegram-disconnect", {
|
|
385
|
-
description: "Stop Telegram
|
|
364
|
+
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
386
365
|
handler: async (_args, ctx) => {
|
|
387
366
|
const threadName = deps.getDisconnectThreadName?.();
|
|
388
367
|
if (threadName) {
|
|
@@ -1233,7 +1212,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1233
1212
|
}, commandArgs);
|
|
1234
1213
|
}
|
|
1235
1214
|
export const TELEGRAM_INTERNAL_COMMAND_NAME = "telegram-internal";
|
|
1236
|
-
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "
|
|
1215
|
+
export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION = "Internal Telegram command cannot be run manually";
|
|
1237
1216
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE = "This internal Telegram command cannot be run manually.";
|
|
1238
1217
|
export function delayTelegramSessionAction(delayMs) {
|
|
1239
1218
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -1382,9 +1361,9 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1382
1361
|
registered = true;
|
|
1383
1362
|
deps.registerCommand(TELEGRAM_INTERNAL_COMMAND_NAME, {
|
|
1384
1363
|
description: TELEGRAM_INTERNAL_COMMAND_DESCRIPTION,
|
|
1385
|
-
handler: async (
|
|
1364
|
+
handler: async (args, ctx) => {
|
|
1386
1365
|
const action = pendingAction;
|
|
1387
|
-
if (!action) {
|
|
1366
|
+
if (!action || args.trim() !== action.token) {
|
|
1388
1367
|
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
1389
1368
|
return;
|
|
1390
1369
|
}
|
|
@@ -1421,9 +1400,10 @@ export function createTelegramSessionActionRuntime(deps) {
|
|
|
1421
1400
|
pendingTarget = undefined;
|
|
1422
1401
|
if (!target)
|
|
1423
1402
|
return;
|
|
1424
|
-
|
|
1403
|
+
const token = randomUUID();
|
|
1404
|
+
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
1425
1405
|
void Promise.resolve()
|
|
1426
|
-
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME}`, {
|
|
1406
|
+
.then(() => deps.sendUserMessage(`/${TELEGRAM_INTERNAL_COMMAND_NAME} ${token}`, {
|
|
1427
1407
|
expandPromptTemplates: true,
|
|
1428
1408
|
}))
|
|
1429
1409
|
.catch((error) => {
|
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,
|
|
@@ -689,6 +728,7 @@ export default function (pi) {
|
|
|
689
728
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
690
729
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
691
730
|
updateStatus,
|
|
731
|
+
dismissGuestPlaceholder: guestPlaceholderRuntime.dismiss,
|
|
692
732
|
});
|
|
693
733
|
const threadDisplaySettingsRuntime = ThreadDisplay.createTelegramThreadDisplaySettingsRuntime({
|
|
694
734
|
getTarget() {
|
|
@@ -1168,6 +1208,18 @@ export default function (pi) {
|
|
|
1168
1208
|
getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
|
|
1169
1209
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1170
1210
|
captureWorkspaceExternalProtection,
|
|
1211
|
+
workspaceRotation: {
|
|
1212
|
+
getAdmission: workspaceAdmissionRuntime.resolve,
|
|
1213
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
1214
|
+
deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
|
|
1215
|
+
reclaimDeadOwnerQueuedWork(binding, isCurrent) {
|
|
1216
|
+
return telegramWorkspaceOperationRuntime.run({
|
|
1217
|
+
operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
|
|
1218
|
+
operationKind: "workspace.reclaim-dead-owner-queue",
|
|
1219
|
+
scopes: [{ kind: "target", target: binding.target }],
|
|
1220
|
+
}, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1171
1223
|
callApi(method, body, options) {
|
|
1172
1224
|
return directTelegramApiRuntime.call(method, body, options);
|
|
1173
1225
|
},
|
|
@@ -1281,6 +1333,15 @@ export default function (pi) {
|
|
|
1281
1333
|
recordRuntimeEvent,
|
|
1282
1334
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1283
1335
|
});
|
|
1336
|
+
const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
|
|
1337
|
+
instanceId: telegramInstanceId,
|
|
1338
|
+
topicTargetStore: threadStore,
|
|
1339
|
+
getCurrentLeaderEpoch,
|
|
1340
|
+
getProfileName: configStore.getActiveProfileName,
|
|
1341
|
+
isPollingSuspended: lockedPollingRuntime.isSuspended,
|
|
1342
|
+
resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1343
|
+
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1344
|
+
});
|
|
1284
1345
|
const telegramBridgeSessionLifecycleDeps = Lifecycle.createTelegramBridgeSessionLifecycleDeps({
|
|
1285
1346
|
contextStore: telegramSessionContextStore,
|
|
1286
1347
|
queue: {
|
|
@@ -1334,6 +1395,7 @@ export default function (pi) {
|
|
|
1334
1395
|
capabilityMonitor: telegramThreadCapabilityMonitor,
|
|
1335
1396
|
queueWatchdog: queueDispatchWatchdogRuntime,
|
|
1336
1397
|
guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
|
|
1398
|
+
prepareThreadPreservationOnQuit,
|
|
1337
1399
|
},
|
|
1338
1400
|
});
|
|
1339
1401
|
const sessionLifecycleRuntime = Lifecycle.createTelegramBridgeSessionLifecycleAssembly(telegramBridgeSessionLifecycleDeps);
|
|
@@ -1449,10 +1511,6 @@ export default function (pi) {
|
|
|
1449
1511
|
return undefined;
|
|
1450
1512
|
return record.threadName ?? "current Telegram thread";
|
|
1451
1513
|
},
|
|
1452
|
-
setRequestedThreadNameForPollingStart: telegramThreadCapabilityState.setRequestedThreadName,
|
|
1453
|
-
validateThreadName(threadName) {
|
|
1454
|
-
return Threads.getTelegramTopicThreadNameValidationError(threadName, undefined);
|
|
1455
|
-
},
|
|
1456
1514
|
onTransportChanged() {
|
|
1457
1515
|
deliveryLifecycleRuntime.onSessionStart();
|
|
1458
1516
|
activityVerbosityRuntime.reset();
|
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/menu-queue.d.ts
CHANGED
|
@@ -33,5 +33,6 @@ export declare function createTelegramQueueMenuRuntime<Context, TModel extends M
|
|
|
33
33
|
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
34
34
|
updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: Context) => Promise<void>;
|
|
35
35
|
updateStatus: (ctx: Context) => void;
|
|
36
|
+
dismissGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
36
37
|
}): TelegramQueueMenuRuntime<Context>;
|
|
37
38
|
export {};
|
package/dist/lib/menu-queue.js
CHANGED
|
@@ -34,6 +34,8 @@ function toTelegramQueueMenuItems(items) {
|
|
|
34
34
|
return {
|
|
35
35
|
chatId: item.chatId,
|
|
36
36
|
replyToMessageId: item.replyToMessageId,
|
|
37
|
+
queueOrder: item.queueOrder,
|
|
38
|
+
isGuest: item.kind === "prompt" && item.guestQueryId !== undefined,
|
|
37
39
|
queuePosition: index + 1,
|
|
38
40
|
isPriority: item.queueLane === "priority",
|
|
39
41
|
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
@@ -72,7 +74,9 @@ function buildTelegramQueueMenuReplyMarkup(items, emptyRefreshIndex = 0) {
|
|
|
72
74
|
return [
|
|
73
75
|
{
|
|
74
76
|
text: label,
|
|
75
|
-
callback_data:
|
|
77
|
+
callback_data: item.isGuest
|
|
78
|
+
? `queue:guest-pick:${item.queueOrder}`
|
|
79
|
+
: `queue:pick:${item.chatId}:${item.replyToMessageId}`,
|
|
76
80
|
},
|
|
77
81
|
];
|
|
78
82
|
});
|
|
@@ -128,6 +132,19 @@ function getTelegramQueueMenuItemText(item) {
|
|
|
128
132
|
const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
|
|
129
133
|
return `${heading}\n${preview}`;
|
|
130
134
|
}
|
|
135
|
+
function buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder) {
|
|
136
|
+
return {
|
|
137
|
+
inline_keyboard: [
|
|
138
|
+
[{ text: "⬆️ Back", callback_data: "queue:list" }],
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
text: "🔴 Skip",
|
|
142
|
+
callback_data: `queue:guest-skip:${queueOrder}`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
131
148
|
function buildTelegramQueueItemSubmenuReplyMarkup(chatId, replyToMessageId, isPriority, isSkipped) {
|
|
132
149
|
return {
|
|
133
150
|
inline_keyboard: [
|
|
@@ -171,6 +188,21 @@ async function handleTelegramQueueMenuCallback(callbackQueryId, data, replyChatI
|
|
|
171
188
|
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, undefined, refreshMatch[1] === undefined ? 0 : Number(refreshMatch[1]));
|
|
172
189
|
return true;
|
|
173
190
|
}
|
|
191
|
+
const guestPickMatch = data.match(/^queue:guest-pick:(\d+)$/);
|
|
192
|
+
if (guestPickMatch) {
|
|
193
|
+
await handleTelegramGuestQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, Number(guestPickMatch[1]), deps);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const guestSkipMatch = data.match(/^queue:guest-skip:(\d+)$/);
|
|
197
|
+
if (guestSkipMatch) {
|
|
198
|
+
const skipped = await deps.skipGuest(Number(guestSkipMatch[1]), ctx);
|
|
199
|
+
if (!skipped) {
|
|
200
|
+
await refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, "Guest prompt skipped.");
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
174
206
|
const pickMatch = data.match(/^queue:pick:(\d+):(\d+)$/);
|
|
175
207
|
if (pickMatch) {
|
|
176
208
|
await handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, Number(pickMatch[1]), Number(pickMatch[2]), deps);
|
|
@@ -208,6 +240,14 @@ async function updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMe
|
|
|
208
240
|
async function refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps) {
|
|
209
241
|
await updateTelegramQueueMenuList(callbackQueryId, replyChatId, replyMessageId, deps, "Item no longer in queue.");
|
|
210
242
|
}
|
|
243
|
+
async function handleTelegramGuestQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, queueOrder, deps) {
|
|
244
|
+
const item = deps.findGuestItem(queueOrder);
|
|
245
|
+
if (!item) {
|
|
246
|
+
return refreshStaleTelegramQueueMenuItem(callbackQueryId, replyChatId, replyMessageId, deps);
|
|
247
|
+
}
|
|
248
|
+
await deps.updateQueueMessage(replyChatId, replyMessageId, getTelegramQueueMenuItemText(item), buildTelegramGuestQueueItemSubmenuReplyMarkup(queueOrder));
|
|
249
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
250
|
+
}
|
|
211
251
|
async function handleTelegramQueueMenuPick(callbackQueryId, replyChatId, replyMessageId, chatId, msgId, deps) {
|
|
212
252
|
const item = deps.findItem(chatId, msgId);
|
|
213
253
|
if (!item) {
|
|
@@ -272,6 +312,7 @@ export function createTelegramQueueMenuRuntime(deps) {
|
|
|
272
312
|
updateStatusMessage: deps.updateStatusMessage,
|
|
273
313
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
274
314
|
updateStatus: deps.updateStatus,
|
|
315
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
275
316
|
}),
|
|
276
317
|
};
|
|
277
318
|
}
|
|
@@ -318,9 +359,22 @@ function createQueueMenuCallbackHandler(deps) {
|
|
|
318
359
|
const findItem = (cId, rId) => {
|
|
319
360
|
return findTelegramQueueMenuItem(toMenuItems(), cId, rId);
|
|
320
361
|
};
|
|
362
|
+
const findGuestItem = (queueOrder) => {
|
|
363
|
+
return toMenuItems().find((item) => {
|
|
364
|
+
return item.isGuest && item.queueOrder === queueOrder;
|
|
365
|
+
});
|
|
366
|
+
};
|
|
321
367
|
return handleTelegramQueueMenuCallback(query.id, data, chatId, messageId, ctx, {
|
|
322
368
|
getQueuedItems: toMenuItems,
|
|
323
369
|
findItem,
|
|
370
|
+
findGuestItem,
|
|
371
|
+
skipGuest: (queueOrder, callbackContext) => {
|
|
372
|
+
return skipQueuedTelegramGuestPrompt(queueOrder, callbackContext, {
|
|
373
|
+
getQueueSnapshot,
|
|
374
|
+
queueMutationRuntime: deps.queueMutationRuntime,
|
|
375
|
+
dismissGuestPlaceholder: deps.dismissGuestPlaceholder,
|
|
376
|
+
});
|
|
377
|
+
},
|
|
324
378
|
togglePriority: (cId, rId) => {
|
|
325
379
|
return toggleQueuedTelegramPromptPriority(cId, rId, ctx, {
|
|
326
380
|
getQueueSnapshot,
|
|
@@ -345,6 +399,23 @@ function createQueueMenuCallbackHandler(deps) {
|
|
|
345
399
|
});
|
|
346
400
|
};
|
|
347
401
|
}
|
|
402
|
+
async function skipQueuedTelegramGuestPrompt(queueOrder, ctx, deps) {
|
|
403
|
+
const item = deps.getQueueSnapshot().find((candidate) => {
|
|
404
|
+
return candidate.kind === "prompt" &&
|
|
405
|
+
candidate.guestQueryId !== undefined &&
|
|
406
|
+
candidate.queueOrder === queueOrder;
|
|
407
|
+
});
|
|
408
|
+
if (!item || !deps.queueMutationRuntime.removeGuestPromptByQueueOrder) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const removed = deps.queueMutationRuntime.removeGuestPromptByQueueOrder(queueOrder, ctx);
|
|
412
|
+
if (!removed)
|
|
413
|
+
return false;
|
|
414
|
+
if (item.guestInlineMessageId && deps.dismissGuestPlaceholder) {
|
|
415
|
+
await deps.dismissGuestPlaceholder(item.guestInlineMessageId);
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
348
419
|
function getQueueMenuReactionDisposition(item, priority, skipped) {
|
|
349
420
|
if (priority && skipped) {
|
|
350
421
|
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). */
|