@llblab/pi-kit 0.35.0 → 0.35.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/BACKLOG.md +1 -0
- package/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +40 -45
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +10 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +19 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +23 -11
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -55
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +22 -5
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +25 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -1171,7 +1171,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
1171
1171
|
shutdownGenerativeAppLiveSurfaces?: () => void;
|
|
1172
1172
|
resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
|
|
1173
1173
|
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
1174
|
-
callMultipart:
|
|
1174
|
+
callMultipart: (...args: Parameters<TelegramApi.TelegramBridgeApiRuntime["callMultipart"]>) => Promise<unknown>;
|
|
1175
1175
|
sendChatAction: NonNullable<
|
|
1176
1176
|
OutboundHandlers.TelegramVoiceReplySenderDeps["sendChatAction"]
|
|
1177
1177
|
>;
|
|
@@ -1198,6 +1198,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
1198
1198
|
ctx: Pi.ExtensionContext,
|
|
1199
1199
|
) => void;
|
|
1200
1200
|
answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
|
|
1201
|
+
editGuestInlineMessage?: TelegramApi.TelegramBridgeApiRuntime["editGuestInlineMessage"];
|
|
1201
1202
|
deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
|
|
1202
1203
|
sendGuestReply: NonNullable<
|
|
1203
1204
|
Queue.TelegramAgentEndHookRuntimeDeps<
|
|
@@ -1276,6 +1277,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1276
1277
|
onSteerTurnEnd,
|
|
1277
1278
|
onPromptHandedOff,
|
|
1278
1279
|
answerGuestQuery,
|
|
1280
|
+
editGuestInlineMessage,
|
|
1279
1281
|
deleteMessage,
|
|
1280
1282
|
sendGuestReply,
|
|
1281
1283
|
editGuestReply,
|
|
@@ -1318,6 +1320,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1318
1320
|
turn: Queue.PendingTelegramTurn,
|
|
1319
1321
|
attachment: Queue.QueuedAttachment,
|
|
1320
1322
|
caption?: string,
|
|
1323
|
+
options?: { isDeliveryActive?: () => boolean },
|
|
1321
1324
|
): Promise<void> => {
|
|
1322
1325
|
const stagingTarget = proactivePushTargetGetter();
|
|
1323
1326
|
const stagingChatId = stagingTarget?.chatId;
|
|
@@ -1328,11 +1331,19 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1328
1331
|
}
|
|
1329
1332
|
await OutboundAttachments.deliverTelegramGuestCachedAttachment({
|
|
1330
1333
|
guestQueryId: turn.guestQueryId!,
|
|
1334
|
+
guestInlineMessageId: turn.guestInlineMessageId,
|
|
1335
|
+
editGuestInlineMessage,
|
|
1336
|
+
isDeliveryActive: options?.isDeliveryActive,
|
|
1331
1337
|
stagingChatId,
|
|
1332
1338
|
stagingTarget,
|
|
1333
1339
|
attachment,
|
|
1334
1340
|
caption,
|
|
1335
|
-
sendMultipart:
|
|
1341
|
+
sendMultipart: (method, fields, fileField, path, name) =>
|
|
1342
|
+
callMultipart(method, fields, fileField, path, name, {
|
|
1343
|
+
assertAuthority() {
|
|
1344
|
+
if (options?.isDeliveryActive?.() === false) throw new Error("Guest attachment upload lost authority.");
|
|
1345
|
+
},
|
|
1346
|
+
}),
|
|
1336
1347
|
answerGuestQuery: (guestQueryId, result) =>
|
|
1337
1348
|
answerGuestQuery(guestQueryId, undefined, { result }),
|
|
1338
1349
|
answerGuestText: (guestQueryId, text) =>
|
|
@@ -536,9 +536,10 @@ export function createTelegramBusAwareApiRuntime(
|
|
|
536
536
|
editGuestInlineMessage(
|
|
537
537
|
inlineMessageId: string,
|
|
538
538
|
content: TelegramEditGuestInlineMessageContent,
|
|
539
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
539
540
|
): Promise<void> {
|
|
540
541
|
return deps.ownsDirect()
|
|
541
|
-
? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content)
|
|
542
|
+
? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content, options)
|
|
542
543
|
: rejectTelegramDirectOwnership("editGuestInlineMessage");
|
|
543
544
|
},
|
|
544
545
|
async deleteMessage(chatId: number, messageId: number): Promise<void> {
|
|
@@ -2281,6 +2281,8 @@ export interface TelegramSelectedCommandAdmission {
|
|
|
2281
2281
|
/** Independent exact recipient/target lifetime, including inside captured menu transport adapters. */
|
|
2282
2282
|
assertRecipientCurrent(): void;
|
|
2283
2283
|
reportCompleted(): boolean;
|
|
2284
|
+
/** Optional recipient release barrier after acceptance; rejection suppresses effects without replay. */
|
|
2285
|
+
beforeEffects?(): Promise<void>;
|
|
2284
2286
|
}
|
|
2285
2287
|
|
|
2286
2288
|
/**
|
|
@@ -3068,7 +3070,8 @@ export function createTelegramCommandHandler<
|
|
|
3068
3070
|
? copySelectedMenuCarrier(original)
|
|
3069
3071
|
: copySelectedReplyAnchor(original);
|
|
3070
3072
|
const assertRecipientCurrent = admission.assertRecipientCurrent,
|
|
3071
|
-
report = admission.reportCompleted
|
|
3073
|
+
report = admission.reportCompleted,
|
|
3074
|
+
beforeEffects = admission.beforeEffects;
|
|
3072
3075
|
const effects: TelegramHeldCommandEffects<TContext> | undefined =
|
|
3073
3076
|
parsed.name === "status"
|
|
3074
3077
|
? undefined
|
|
@@ -3100,8 +3103,11 @@ export function createTelegramCommandHandler<
|
|
|
3100
3103
|
throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
|
|
3101
3104
|
};
|
|
3102
3105
|
return issueSelectedOnce(assertCurrent, async () => {
|
|
3103
|
-
if (plan.completion === "before-effect"
|
|
3104
|
-
return false;
|
|
3106
|
+
if (plan.completion === "before-effect") {
|
|
3107
|
+
if (!report.call(admission)) return false;
|
|
3108
|
+
if (beforeEffects) await beforeEffects();
|
|
3109
|
+
assertRecipientCurrent();
|
|
3110
|
+
}
|
|
3105
3111
|
return prepared.run({
|
|
3106
3112
|
message,
|
|
3107
3113
|
command: parsed,
|
|
@@ -3137,7 +3143,8 @@ export function createTelegramCommandHandler<
|
|
|
3137
3143
|
from: { ...original.from },
|
|
3138
3144
|
};
|
|
3139
3145
|
const assertRecipientCurrent = admission.assertRecipientCurrent,
|
|
3140
|
-
reportCompleted = admission.reportCompleted
|
|
3146
|
+
reportCompleted = admission.reportCompleted,
|
|
3147
|
+
beforeEffects = admission.beforeEffects;
|
|
3141
3148
|
const assertAdmissionCurrent = createSelectedSemanticGuard(
|
|
3142
3149
|
admission,
|
|
3143
3150
|
() => captured.assertExecutionCurrent?.(original),
|
|
@@ -3163,7 +3170,7 @@ export function createTelegramCommandHandler<
|
|
|
3163
3170
|
);
|
|
3164
3171
|
},
|
|
3165
3172
|
},
|
|
3166
|
-
{ assertSemanticCurrent, assertRecipientCurrent, reportCompleted },
|
|
3173
|
+
{ assertSemanticCurrent, assertRecipientCurrent, reportCompleted, beforeEffects },
|
|
3167
3174
|
),
|
|
3168
3175
|
);
|
|
3169
3176
|
};
|
|
@@ -3607,6 +3614,7 @@ export function createTelegramCommandHandler<
|
|
|
3607
3614
|
const assertSourceCurrent = admission.assertSourceCurrent,
|
|
3608
3615
|
assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
3609
3616
|
const reportCompleted = admission.reportCompleted,
|
|
3617
|
+
beforeEffects = admission.beforeEffects,
|
|
3610
3618
|
assertExecutionCurrent = deps.assertExecutionCurrent;
|
|
3611
3619
|
const assertIssuanceCurrent = () => {
|
|
3612
3620
|
assertSourceCurrent.call(admission);
|
|
@@ -3639,9 +3647,12 @@ export function createTelegramCommandHandler<
|
|
|
3639
3647
|
};
|
|
3640
3648
|
return issueSelectedOnce(
|
|
3641
3649
|
assertIssuanceCurrent,
|
|
3642
|
-
async () =>
|
|
3643
|
-
reportCompleted.call(admission)
|
|
3644
|
-
|
|
3650
|
+
async () => {
|
|
3651
|
+
if (!reportCompleted.call(admission)) return false;
|
|
3652
|
+
if (beforeEffects) await beforeEffects();
|
|
3653
|
+
assertRecipientCurrent();
|
|
3654
|
+
return handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args);
|
|
3655
|
+
},
|
|
3645
3656
|
);
|
|
3646
3657
|
},
|
|
3647
3658
|
});
|
|
@@ -3766,6 +3777,7 @@ async function handleTelegramHelpCommand<
|
|
|
3766
3777
|
assertSemanticCurrent(): void;
|
|
3767
3778
|
assertRecipientCurrent(): void;
|
|
3768
3779
|
reportCompleted(): boolean;
|
|
3780
|
+
beforeEffects?(): Promise<void>;
|
|
3769
3781
|
},
|
|
3770
3782
|
): Promise<boolean> {
|
|
3771
3783
|
const assertSemanticCurrent =
|
|
@@ -3788,6 +3800,7 @@ async function handleTelegramHelpCommand<
|
|
|
3788
3800
|
if (admission) {
|
|
3789
3801
|
assertSemanticCurrent();
|
|
3790
3802
|
if (!admission.reportCompleted()) return false;
|
|
3803
|
+
if (admission.beforeEffects) await admission.beforeEffects();
|
|
3791
3804
|
admission.assertRecipientCurrent();
|
|
3792
3805
|
}
|
|
3793
3806
|
const assertEffectCurrent =
|
|
@@ -1302,6 +1302,32 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1302
1302
|
delivery: deliveryLifecycleRuntime,
|
|
1303
1303
|
api: telegramApiTargetActivityRuntime,
|
|
1304
1304
|
});
|
|
1305
|
+
const prepareTemporaryThreadJournalInspection = function (
|
|
1306
|
+
target: Targets.TelegramTarget,
|
|
1307
|
+
requiredJournalBindingKeys: readonly string[],
|
|
1308
|
+
ownInputs?: readonly Threads.TelegramTemporaryThreadInput[],
|
|
1309
|
+
): Parameters<typeof Journal.isTelegramThreadCleanupJournalNamespaceClear>[0] | undefined {
|
|
1310
|
+
const binding = resolveTelegramUpdateJournalBinding();
|
|
1311
|
+
const botToken = configStore.getBotToken();
|
|
1312
|
+
if (!binding || !botToken) return undefined;
|
|
1313
|
+
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1314
|
+
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({ botToken, botId: getTelegramBotId() });
|
|
1315
|
+
return {
|
|
1316
|
+
directory: Paths.resolveTelegramTempDir(), profile, botIdentity,
|
|
1317
|
+
pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
|
|
1318
|
+
requiredJournalBindingKeys: [binding.recoveryKey, ...requiredJournalBindingKeys],
|
|
1319
|
+
...(target.threadId !== undefined && ownInputs
|
|
1320
|
+
? { cleanup: { target: { chatId: target.chatId, threadId: target.threadId }, ownInputs } } : {}),
|
|
1321
|
+
limits: { maxDirectoryEntries: 10_000, maxFiles: 4096, maxBytes: 64 * 1024 * 1024,
|
|
1322
|
+
maxEntries: 10_000, maxWork: 100_000 },
|
|
1323
|
+
withSourceReference(path, operation) {
|
|
1324
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
1325
|
+
referenceClass: "operator-disposition",
|
|
1326
|
+
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity }),
|
|
1327
|
+
}, operation);
|
|
1328
|
+
},
|
|
1329
|
+
};
|
|
1330
|
+
};
|
|
1305
1331
|
const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
|
|
1306
1332
|
configStore,
|
|
1307
1333
|
callApi: callTelegramApi,
|
|
@@ -1407,64 +1433,25 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1407
1433
|
},
|
|
1408
1434
|
);
|
|
1409
1435
|
},
|
|
1410
|
-
inspectTemporaryThreadSources(
|
|
1411
|
-
target,
|
|
1412
|
-
|
|
1413
|
-
ownInputs,
|
|
1414
|
-
) {
|
|
1415
|
-
const binding = resolveTelegramUpdateJournalBinding();
|
|
1416
|
-
const botToken = configStore.getBotToken();
|
|
1417
|
-
if (!binding || !botToken) return undefined;
|
|
1418
|
-
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1419
|
-
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({
|
|
1420
|
-
botToken,
|
|
1421
|
-
botId: getTelegramBotId(),
|
|
1422
|
-
});
|
|
1436
|
+
inspectTemporaryThreadSources(target, requiredJournalBindingKeys, ownInputs) {
|
|
1437
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1438
|
+
if (!input) return undefined;
|
|
1423
1439
|
return withTelegramJournalSourceSerialization(function () {
|
|
1424
|
-
return Journal.isTelegramThreadCleanupJournalNamespaceClear(
|
|
1425
|
-
directory: Paths.resolveTelegramTempDir(),
|
|
1426
|
-
profile,
|
|
1427
|
-
botIdentity,
|
|
1428
|
-
pollingPath: telegramLeaderJournalPath.resolve(
|
|
1429
|
-
configStore.getActiveProfileName(),
|
|
1430
|
-
),
|
|
1431
|
-
requiredJournalBindingKeys: [
|
|
1432
|
-
binding.recoveryKey,
|
|
1433
|
-
...requiredJournalBindingKeys,
|
|
1434
|
-
],
|
|
1435
|
-
...(target.threadId !== undefined && ownInputs
|
|
1436
|
-
? {
|
|
1437
|
-
cleanup: {
|
|
1438
|
-
target: { chatId: target.chatId, threadId: target.threadId },
|
|
1439
|
-
ownInputs,
|
|
1440
|
-
},
|
|
1441
|
-
}
|
|
1442
|
-
: {}),
|
|
1443
|
-
limits: {
|
|
1444
|
-
maxDirectoryEntries: 10_000,
|
|
1445
|
-
maxFiles: 4096,
|
|
1446
|
-
maxBytes: 64 * 1024 * 1024,
|
|
1447
|
-
maxEntries: 10_000,
|
|
1448
|
-
maxWork: 100_000,
|
|
1449
|
-
},
|
|
1450
|
-
withSourceReference(path, operation) {
|
|
1451
|
-
return telegramJournalReferenceRegistry.withReference(
|
|
1452
|
-
{
|
|
1453
|
-
referenceClass: "operator-disposition",
|
|
1454
|
-
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({
|
|
1455
|
-
path,
|
|
1456
|
-
profileName: profile,
|
|
1457
|
-
botIdentity,
|
|
1458
|
-
}),
|
|
1459
|
-
},
|
|
1460
|
-
operation,
|
|
1461
|
-
);
|
|
1462
|
-
},
|
|
1463
|
-
})
|
|
1464
|
-
? []
|
|
1465
|
-
: undefined;
|
|
1440
|
+
return Journal.isTelegramThreadCleanupJournalNamespaceClear(input) ? [] : undefined;
|
|
1466
1441
|
});
|
|
1467
1442
|
},
|
|
1443
|
+
recoverTemporaryThreadDeadQueueOwners(target, requiredJournalBindingKeys, ownInputs, isCurrent) {
|
|
1444
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1445
|
+
if (!input || !isCurrent()) return 0;
|
|
1446
|
+
return Journal.recoverTelegramThreadCleanupDeadQueueOwners({
|
|
1447
|
+
...input, isCurrent, withSourceSerialization: withTelegramJournalSourceSerialization,
|
|
1448
|
+
recoveryOwner: {
|
|
1449
|
+
instanceId: telegramInstanceId, processId: telegramProcessId,
|
|
1450
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
1451
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
1452
|
+
},
|
|
1453
|
+
});
|
|
1454
|
+
},
|
|
1468
1455
|
hasWorkspaceRestoreAuthority() {
|
|
1469
1456
|
return (
|
|
1470
1457
|
lockRuntime.owns() &&
|
|
@@ -3071,6 +3058,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
3071
3058
|
deleteMessage: deleteTelegramMessage,
|
|
3072
3059
|
sendGuestReply,
|
|
3073
3060
|
editGuestReply,
|
|
3061
|
+
editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
|
|
3074
3062
|
stopGuestPlaceholder: guestPlaceholderRuntime.stop,
|
|
3075
3063
|
finalizeMarkdownPreview,
|
|
3076
3064
|
preparePreviewDelivery,
|
|
@@ -3741,6 +3741,86 @@ export function isTelegramThreadCleanupJournalNamespaceClear(
|
|
|
3741
3741
|
);
|
|
3742
3742
|
}
|
|
3743
3743
|
|
|
3744
|
+
/** Demand-driven recovery only; caller holds temporary-cleanup admission. Never a deletion grant. */
|
|
3745
|
+
export function recoverTelegramThreadCleanupDeadQueueOwners(
|
|
3746
|
+
input: Parameters<typeof isTelegramThreadCleanupJournalNamespaceClear>[0] & {
|
|
3747
|
+
recoveryOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3748
|
+
isCurrent: () => boolean;
|
|
3749
|
+
withSourceSerialization: NonNullable<TelegramUpdateJournalStoreOptions["withSourceSerialization"]>;
|
|
3750
|
+
getQueueProcessLiveness?: TelegramUpdateJournalStoreOptions["getQueueProcessLiveness"];
|
|
3751
|
+
},
|
|
3752
|
+
): number {
|
|
3753
|
+
return input.withSourceSerialization(() => {
|
|
3754
|
+
const assertCurrent = () => {
|
|
3755
|
+
if (!input.isCurrent()) throw new Error("Temporary cleanup queue recovery lost authority.");
|
|
3756
|
+
};
|
|
3757
|
+
assertCurrent();
|
|
3758
|
+
if (!input.cleanup) return 0;
|
|
3759
|
+
// Validate exact references and complete namespace coverage before considering any mutation.
|
|
3760
|
+
if (isTelegramThreadCleanupJournalNamespaceClear(input)) return 0;
|
|
3761
|
+
const namespace = inspectTelegramSessionJournalNamespace(input);
|
|
3762
|
+
if (namespace.retainedInputs?.some(value => value.state !== "committed") ||
|
|
3763
|
+
input.requiredJournalBindingKeys.some(key => !namespace.sources.some(source =>
|
|
3764
|
+
source.path === getTelegramUpdateJournalBindingPath(key) && source.evidence.kind === "present"))) return 0;
|
|
3765
|
+
const plans: Array<{ path: string; recovery: TelegramUpdateJournalDeadQueueOwnerRecoveryInput }> = [];
|
|
3766
|
+
for (const source of namespace.sources) {
|
|
3767
|
+
if (source.evidence.kind !== "present") continue;
|
|
3768
|
+
const file = source.evidence.file;
|
|
3769
|
+
if (file.version !== TELEGRAM_UPDATE_JOURNAL_VERSION) continue;
|
|
3770
|
+
const groups = new Map<string, TelegramUpdateJournalEntry[]>();
|
|
3771
|
+
for (const entry of file.entries) {
|
|
3772
|
+
if (!entry.queueReceiptId) continue;
|
|
3773
|
+
const group = groups.get(entry.queueReceiptId) ?? [];
|
|
3774
|
+
group.push(entry);
|
|
3775
|
+
groups.set(entry.queueReceiptId, group);
|
|
3776
|
+
}
|
|
3777
|
+
for (const group of groups.values()) {
|
|
3778
|
+
const entry = group[0]!;
|
|
3779
|
+
if (entry.state !== "queued" || !entry.queueReceiptId || !entry.queueOwner ||
|
|
3780
|
+
!entry.queueKind || entry.queueHandoff || entry.inputClaim) continue;
|
|
3781
|
+
if (group.some(value => value.state !== "queued" || value.queueHandoff || value.inputClaim ||
|
|
3782
|
+
value.queueKind !== entry.queueKind || !isDeepStrictEqual(value.queueOwner, entry.queueOwner))) continue;
|
|
3783
|
+
plans.push({ path: source.path, recovery: {
|
|
3784
|
+
queueKind: entry.queueKind, receiptId: entry.queueReceiptId,
|
|
3785
|
+
sourceUpdateIds: group.map(value => value.updateId).sort((a, b) => a - b),
|
|
3786
|
+
deadOwner: cloneJournalQueueOwner(entry.queueOwner), recoveryOwner: { ...input.recoveryOwner },
|
|
3787
|
+
} });
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
const liveness = input.getQueueProcessLiveness ?? getTelegramProcessLiveness;
|
|
3791
|
+
const dead = plans.filter(plan => {
|
|
3792
|
+
assertCurrent();
|
|
3793
|
+
const result = liveness(plan.recovery.deadOwner);
|
|
3794
|
+
assertCurrent();
|
|
3795
|
+
return result === "dead";
|
|
3796
|
+
});
|
|
3797
|
+
let removed = 0;
|
|
3798
|
+
for (const plan of dead) {
|
|
3799
|
+
assertCurrent();
|
|
3800
|
+
removed += input.withSourceReference(plan.path, () => {
|
|
3801
|
+
const journal = createTelegramUpdateJournalStore({
|
|
3802
|
+
path: plan.path, profileName: input.profile, botIdentity: input.botIdentity,
|
|
3803
|
+
queueRuntimeIdentity: input.recoveryOwner,
|
|
3804
|
+
// This private synchronous store stays inside the cross-family serializer already held above.
|
|
3805
|
+
withSourceSerialization(operation) { assertCurrent(); return operation(); },
|
|
3806
|
+
sourceAccess: { directory: dirname(plan.path), limits: input.limits },
|
|
3807
|
+
getQueueProcessLiveness(owner) {
|
|
3808
|
+
assertCurrent();
|
|
3809
|
+
const result = liveness(owner);
|
|
3810
|
+
assertCurrent();
|
|
3811
|
+
return result;
|
|
3812
|
+
},
|
|
3813
|
+
withWriterAdmission(operation) { assertCurrent(); return operation(); },
|
|
3814
|
+
});
|
|
3815
|
+
// The journal transaction rechecks whole-group identity, absence of handoff and fresh process-death proof.
|
|
3816
|
+
return journal.recoverDeadQueueOwner(plan.recovery).recoveredUpdateIds.length;
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
assertCurrent();
|
|
3820
|
+
return removed;
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3744
3824
|
function cloneEntry(
|
|
3745
3825
|
entry: TelegramUpdateJournalEntry,
|
|
3746
3826
|
): TelegramUpdateJournalEntry {
|
|
@@ -130,6 +130,7 @@ export interface TelegramQueuedOutboundAttachmentView {
|
|
|
130
130
|
export interface TelegramOutboundAttachmentQueueTargetView {
|
|
131
131
|
queuedAttachments: TelegramQueuedOutboundAttachmentView[];
|
|
132
132
|
guestQueryId?: string;
|
|
133
|
+
guestInlineMessageId?: string;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
@@ -718,6 +719,11 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
718
719
|
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
719
720
|
statPath: options.statPath,
|
|
720
721
|
});
|
|
722
|
+
if (options.activeTurn.guestInlineMessageId && pendingAttachments.some(
|
|
723
|
+
(attachment) => getTelegramGuestAttachmentTransport(attachment.path).fileField === "voice",
|
|
724
|
+
)) {
|
|
725
|
+
throw new Error("Telegram Guest Mode cannot replace an inline placeholder with a voice note; no attachment was queued. Use MP3 audio or a normal bot chat.");
|
|
726
|
+
}
|
|
721
727
|
options.activeTurn.queuedAttachments.push(...pendingAttachments);
|
|
722
728
|
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
723
729
|
return {
|
|
@@ -733,6 +739,13 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
733
739
|
|
|
734
740
|
export async function deliverTelegramGuestCachedAttachment(options: {
|
|
735
741
|
guestQueryId: string;
|
|
742
|
+
guestInlineMessageId?: string;
|
|
743
|
+
editGuestInlineMessage?: (
|
|
744
|
+
inlineMessageId: string,
|
|
745
|
+
content: { media: { type: "photo" | "document" | "audio"; media: string; caption?: string } },
|
|
746
|
+
options: { assertAuthority: () => void },
|
|
747
|
+
) => Promise<void>;
|
|
748
|
+
isDeliveryActive?: () => boolean;
|
|
736
749
|
stagingChatId: number;
|
|
737
750
|
stagingTarget?: TelegramTarget;
|
|
738
751
|
attachment: TelegramQueuedOutboundAttachmentView;
|
|
@@ -750,6 +763,14 @@ export async function deliverTelegramGuestCachedAttachment(options: {
|
|
|
750
763
|
const transport = getTelegramGuestAttachmentTransport(
|
|
751
764
|
options.attachment.path,
|
|
752
765
|
);
|
|
766
|
+
const isActive = () => options.isDeliveryActive?.() !== false;
|
|
767
|
+
const assertActive = () => {
|
|
768
|
+
if (!isActive()) throw new Error("Guest attachment delivery lost authority.");
|
|
769
|
+
};
|
|
770
|
+
if (!isActive()) return;
|
|
771
|
+
if (options.guestInlineMessageId && (!options.editGuestInlineMessage || transport.fileField === "voice")) {
|
|
772
|
+
throw new Error("Guest inline attachment editing is unavailable for this media.");
|
|
773
|
+
}
|
|
753
774
|
let stagingMessageId: number | undefined;
|
|
754
775
|
let answerAttempted = false;
|
|
755
776
|
try {
|
|
@@ -764,6 +785,7 @@ export async function deliverTelegramGuestCachedAttachment(options: {
|
|
|
764
785
|
options.attachment.fileName,
|
|
765
786
|
)) as TelegramGuestStagingMessage;
|
|
766
787
|
stagingMessageId = message.message_id;
|
|
788
|
+
if (!isActive()) return;
|
|
767
789
|
const caption = options.caption
|
|
768
790
|
? Array.from(options.caption).slice(0, 1024).join("")
|
|
769
791
|
: undefined;
|
|
@@ -810,17 +832,28 @@ export async function deliverTelegramGuestCachedAttachment(options: {
|
|
|
810
832
|
...(caption ? { caption } : {}),
|
|
811
833
|
};
|
|
812
834
|
}
|
|
835
|
+
if (!isActive()) return;
|
|
813
836
|
answerAttempted = true;
|
|
814
|
-
|
|
837
|
+
if (options.guestInlineMessageId && options.editGuestInlineMessage) {
|
|
838
|
+
if (result.type === "voice") throw new Error("Voice notes cannot replace inline placeholders.");
|
|
839
|
+
const media = result.type === "photo" ? result.photo_file_id
|
|
840
|
+
: result.type === "audio" ? result.audio_file_id : result.document_file_id;
|
|
841
|
+
await options.editGuestInlineMessage(options.guestInlineMessageId, {
|
|
842
|
+
media: { type: result.type, media, ...(caption ? { caption } : {}) },
|
|
843
|
+
}, { assertAuthority: assertActive });
|
|
844
|
+
} else {
|
|
845
|
+
await options.answerGuestQuery(options.guestQueryId, result);
|
|
846
|
+
}
|
|
815
847
|
} catch (error) {
|
|
816
|
-
if (!
|
|
848
|
+
if (!isActive()) return;
|
|
849
|
+
if (!options.guestInlineMessageId && !answerAttempted && options.answerGuestText && options.fallbackText) {
|
|
817
850
|
answerAttempted = true;
|
|
818
851
|
await options.answerGuestText(options.guestQueryId, options.fallbackText);
|
|
819
852
|
} else {
|
|
820
853
|
throw error;
|
|
821
854
|
}
|
|
822
855
|
} finally {
|
|
823
|
-
if (stagingMessageId !== undefined) {
|
|
856
|
+
if (stagingMessageId !== undefined && isActive()) {
|
|
824
857
|
try {
|
|
825
858
|
await options.deleteMessage(options.stagingChatId, stagingMessageId);
|
|
826
859
|
} catch (error) {
|
|
@@ -1983,13 +1983,18 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1983
1983
|
) => Promise<void>;
|
|
1984
1984
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
1985
1985
|
/** Replaces the early guest ACK with the final text. */
|
|
1986
|
-
editGuestReply?: (
|
|
1986
|
+
editGuestReply?: (
|
|
1987
|
+
inlineMessageId: string,
|
|
1988
|
+
markdown: string,
|
|
1989
|
+
options?: { assertAuthority?: () => void },
|
|
1990
|
+
) => Promise<void>;
|
|
1987
1991
|
/** Cancels the animated guest placeholder before the final edit. */
|
|
1988
1992
|
stopGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
1989
1993
|
sendGuestAttachment?: (
|
|
1990
1994
|
turn: TTurn,
|
|
1991
1995
|
attachment: QueuedAttachment,
|
|
1992
1996
|
caption?: string,
|
|
1997
|
+
options?: { isDeliveryActive?: () => boolean },
|
|
1993
1998
|
) => Promise<void>;
|
|
1994
1999
|
sendGuestVoiceReply?: (
|
|
1995
2000
|
turn: TTurn,
|
|
@@ -2350,13 +2355,46 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2350
2355
|
});
|
|
2351
2356
|
}
|
|
2352
2357
|
}
|
|
2353
|
-
if (
|
|
2354
|
-
|
|
2358
|
+
if (!isDeliveryActive()) return;
|
|
2359
|
+
if (turn.guestInlineMessageId) {
|
|
2360
|
+
let experimentText = assistant.errorMessage
|
|
2355
2361
|
? "Telegram bridge: Pi failed while processing the request."
|
|
2356
2362
|
: finalText;
|
|
2363
|
+
const [attachment] = turn.queuedAttachments;
|
|
2364
|
+
if (!assistant.errorMessage && attachment) {
|
|
2365
|
+
try {
|
|
2366
|
+
if (!deps.sendGuestAttachment) throw new Error("Guest attachment delivery is unavailable.");
|
|
2367
|
+
await deps.sendGuestAttachment(turn, attachment, finalText || undefined, { isDeliveryActive });
|
|
2368
|
+
if (isDeliveryActive() && endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2369
|
+
return;
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
2372
|
+
phase: "guest-attachment", guestQueryId: turn.guestQueryId,
|
|
2373
|
+
});
|
|
2374
|
+
if (!isDeliveryActive()) return;
|
|
2375
|
+
if (isTelegramApiCommitUnknownError(error)) {
|
|
2376
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
experimentText = "Telegram bridge could not deliver the requested attachment. Please retry in a normal bot chat.";
|
|
2380
|
+
}
|
|
2381
|
+
} else if (!assistant.errorMessage && hasOutboundArtifacts) {
|
|
2382
|
+
experimentText = [finalText, "Telegram Guest Mode cannot replace an inline placeholder with a voice note. Please request MP3 audio or use a normal bot chat."].filter(Boolean).join("\n\n");
|
|
2383
|
+
}
|
|
2384
|
+
if (!deps.editGuestReply) {
|
|
2385
|
+
deps.recordRuntimeEvent?.("delivery", new Error("Guest inline reply editing is unavailable."), {
|
|
2386
|
+
phase: "guest-ack-edit", guestQueryId: turn.guestQueryId,
|
|
2387
|
+
});
|
|
2388
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2357
2391
|
if (experimentText) {
|
|
2358
2392
|
try {
|
|
2359
|
-
await deps.editGuestReply(turn.guestInlineMessageId, experimentText
|
|
2393
|
+
await deps.editGuestReply(turn.guestInlineMessageId, experimentText, {
|
|
2394
|
+
assertAuthority() {
|
|
2395
|
+
if (!isDeliveryActive()) throw new Error("Guest reply edit lost authority.");
|
|
2396
|
+
},
|
|
2397
|
+
});
|
|
2360
2398
|
deps.recordRuntimeEvent?.(
|
|
2361
2399
|
"guest",
|
|
2362
2400
|
new Error("Guest ACK edited the guest answer"),
|
|
@@ -2401,6 +2439,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2401
2439
|
turn,
|
|
2402
2440
|
guestAttachment,
|
|
2403
2441
|
finalText || undefined,
|
|
2442
|
+
{ isDeliveryActive },
|
|
2404
2443
|
);
|
|
2405
2444
|
} catch (error) {
|
|
2406
2445
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
@@ -1033,13 +1033,14 @@ export function createGuestMarkdownReplyEditor(deps: {
|
|
|
1033
1033
|
editGuestInlineMessage: (
|
|
1034
1034
|
inlineMessageId: string,
|
|
1035
1035
|
content: { richMessage?: TelegramInputRichMessage; text?: string },
|
|
1036
|
+
options?: { assertAuthority?: () => void },
|
|
1036
1037
|
) => Promise<void>;
|
|
1037
1038
|
}) {
|
|
1038
|
-
return async (inlineMessageId: string, markdown: string) => {
|
|
1039
|
+
return async (inlineMessageId: string, markdown: string, options?: { assertAuthority?: () => void }) => {
|
|
1039
1040
|
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
1040
1041
|
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
1041
1042
|
richMessage: { markdown: richMarkdown },
|
|
1042
|
-
});
|
|
1043
|
+
}, options);
|
|
1043
1044
|
};
|
|
1044
1045
|
}
|
|
1045
1046
|
|