@llblab/pi-kit 0.34.3 → 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 +2 -0
- package/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -3
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +12 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +39 -23
- 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/config.d.ts +6 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +6 -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.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +41 -14
- 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 +5 -3
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +45 -24
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
- package/node_modules/@llblab/pi-telegram/lib/config.ts +11 -17
- 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 +54 -18
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -1033,6 +1033,29 @@ export default function (pi) {
|
|
|
1033
1033
|
delivery: deliveryLifecycleRuntime,
|
|
1034
1034
|
api: telegramApiTargetActivityRuntime,
|
|
1035
1035
|
});
|
|
1036
|
+
const prepareTemporaryThreadJournalInspection = function (target, requiredJournalBindingKeys, ownInputs) {
|
|
1037
|
+
const binding = resolveTelegramUpdateJournalBinding();
|
|
1038
|
+
const botToken = configStore.getBotToken();
|
|
1039
|
+
if (!binding || !botToken)
|
|
1040
|
+
return undefined;
|
|
1041
|
+
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1042
|
+
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({ botToken, botId: getTelegramBotId() });
|
|
1043
|
+
return {
|
|
1044
|
+
directory: Paths.resolveTelegramTempDir(), profile, botIdentity,
|
|
1045
|
+
pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
|
|
1046
|
+
requiredJournalBindingKeys: [binding.recoveryKey, ...requiredJournalBindingKeys],
|
|
1047
|
+
...(target.threadId !== undefined && ownInputs
|
|
1048
|
+
? { cleanup: { target: { chatId: target.chatId, threadId: target.threadId }, ownInputs } } : {}),
|
|
1049
|
+
limits: { maxDirectoryEntries: 10_000, maxFiles: 4096, maxBytes: 64 * 1024 * 1024,
|
|
1050
|
+
maxEntries: 10_000, maxWork: 100_000 },
|
|
1051
|
+
withSourceReference(path, operation) {
|
|
1052
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
1053
|
+
referenceClass: "operator-disposition",
|
|
1054
|
+
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity }),
|
|
1055
|
+
}, operation);
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1036
1059
|
const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
|
|
1037
1060
|
configStore,
|
|
1038
1061
|
callApi: callTelegramApi,
|
|
@@ -1112,53 +1135,24 @@ export default function (pi) {
|
|
|
1112
1135
|
});
|
|
1113
1136
|
},
|
|
1114
1137
|
inspectTemporaryThreadSources(target, requiredJournalBindingKeys, ownInputs) {
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
if (!binding || !botToken)
|
|
1138
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1139
|
+
if (!input)
|
|
1118
1140
|
return undefined;
|
|
1119
|
-
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1120
|
-
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({
|
|
1121
|
-
botToken,
|
|
1122
|
-
botId: getTelegramBotId(),
|
|
1123
|
-
});
|
|
1124
1141
|
return withTelegramJournalSourceSerialization(function () {
|
|
1125
|
-
return Journal.isTelegramThreadCleanupJournalNamespaceClear(
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
},
|
|
1140
|
-
}
|
|
1141
|
-
: {}),
|
|
1142
|
-
limits: {
|
|
1143
|
-
maxDirectoryEntries: 10_000,
|
|
1144
|
-
maxFiles: 4096,
|
|
1145
|
-
maxBytes: 64 * 1024 * 1024,
|
|
1146
|
-
maxEntries: 10_000,
|
|
1147
|
-
maxWork: 100_000,
|
|
1148
|
-
},
|
|
1149
|
-
withSourceReference(path, operation) {
|
|
1150
|
-
return telegramJournalReferenceRegistry.withReference({
|
|
1151
|
-
referenceClass: "operator-disposition",
|
|
1152
|
-
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({
|
|
1153
|
-
path,
|
|
1154
|
-
profileName: profile,
|
|
1155
|
-
botIdentity,
|
|
1156
|
-
}),
|
|
1157
|
-
}, operation);
|
|
1158
|
-
},
|
|
1159
|
-
})
|
|
1160
|
-
? []
|
|
1161
|
-
: undefined;
|
|
1142
|
+
return Journal.isTelegramThreadCleanupJournalNamespaceClear(input) ? [] : undefined;
|
|
1143
|
+
});
|
|
1144
|
+
},
|
|
1145
|
+
recoverTemporaryThreadDeadQueueOwners(target, requiredJournalBindingKeys, ownInputs, isCurrent) {
|
|
1146
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1147
|
+
if (!input || !isCurrent())
|
|
1148
|
+
return 0;
|
|
1149
|
+
return Journal.recoverTelegramThreadCleanupDeadQueueOwners({
|
|
1150
|
+
...input, isCurrent, withSourceSerialization: withTelegramJournalSourceSerialization,
|
|
1151
|
+
recoveryOwner: {
|
|
1152
|
+
instanceId: telegramInstanceId, processId: telegramProcessId,
|
|
1153
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
1154
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
1155
|
+
},
|
|
1162
1156
|
});
|
|
1163
1157
|
},
|
|
1164
1158
|
hasWorkspaceRestoreAuthority() {
|
|
@@ -2485,6 +2479,7 @@ export default function (pi) {
|
|
|
2485
2479
|
deleteMessage: deleteTelegramMessage,
|
|
2486
2480
|
sendGuestReply,
|
|
2487
2481
|
editGuestReply,
|
|
2482
|
+
editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
|
|
2488
2483
|
stopGuestPlaceholder: guestPlaceholderRuntime.stop,
|
|
2489
2484
|
finalizeMarkdownPreview,
|
|
2490
2485
|
preparePreviewDelivery,
|
|
@@ -751,6 +751,13 @@ export declare function isTelegramThreadCleanupJournalNamespaceClear(input: Tele
|
|
|
751
751
|
}[];
|
|
752
752
|
};
|
|
753
753
|
}): boolean;
|
|
754
|
+
/** Demand-driven recovery only; caller holds temporary-cleanup admission. Never a deletion grant. */
|
|
755
|
+
export declare function recoverTelegramThreadCleanupDeadQueueOwners(input: Parameters<typeof isTelegramThreadCleanupJournalNamespaceClear>[0] & {
|
|
756
|
+
recoveryOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
757
|
+
isCurrent: () => boolean;
|
|
758
|
+
withSourceSerialization: NonNullable<TelegramUpdateJournalStoreOptions["withSourceSerialization"]>;
|
|
759
|
+
getQueueProcessLiveness?: TelegramUpdateJournalStoreOptions["getQueueProcessLiveness"];
|
|
760
|
+
}): number;
|
|
754
761
|
export declare function publishTelegramUpdateJournalSegment(path: string, segment: TelegramUpdateJournalSegment): TelegramUpdateJournalSegmentPublicationResult;
|
|
755
762
|
export declare function createTelegramUpdateQueueHandoffToken(): string;
|
|
756
763
|
export declare function createTelegramUpdateJournalBotIdentity(input: {
|
|
@@ -1988,6 +1988,86 @@ export function isTelegramThreadCleanupJournalNamespaceClear(input) {
|
|
|
1988
1988
|
return namespace.sources.every((source) => source.evidence.kind === "absent" ||
|
|
1989
1989
|
source.evidence.file.entries.every((entry) => isUnrelatedPlainInput(entry, source.path)));
|
|
1990
1990
|
}
|
|
1991
|
+
/** Demand-driven recovery only; caller holds temporary-cleanup admission. Never a deletion grant. */
|
|
1992
|
+
export function recoverTelegramThreadCleanupDeadQueueOwners(input) {
|
|
1993
|
+
return input.withSourceSerialization(() => {
|
|
1994
|
+
const assertCurrent = () => {
|
|
1995
|
+
if (!input.isCurrent())
|
|
1996
|
+
throw new Error("Temporary cleanup queue recovery lost authority.");
|
|
1997
|
+
};
|
|
1998
|
+
assertCurrent();
|
|
1999
|
+
if (!input.cleanup)
|
|
2000
|
+
return 0;
|
|
2001
|
+
// Validate exact references and complete namespace coverage before considering any mutation.
|
|
2002
|
+
if (isTelegramThreadCleanupJournalNamespaceClear(input))
|
|
2003
|
+
return 0;
|
|
2004
|
+
const namespace = inspectTelegramSessionJournalNamespace(input);
|
|
2005
|
+
if (namespace.retainedInputs?.some(value => value.state !== "committed") ||
|
|
2006
|
+
input.requiredJournalBindingKeys.some(key => !namespace.sources.some(source => source.path === getTelegramUpdateJournalBindingPath(key) && source.evidence.kind === "present")))
|
|
2007
|
+
return 0;
|
|
2008
|
+
const plans = [];
|
|
2009
|
+
for (const source of namespace.sources) {
|
|
2010
|
+
if (source.evidence.kind !== "present")
|
|
2011
|
+
continue;
|
|
2012
|
+
const file = source.evidence.file;
|
|
2013
|
+
if (file.version !== TELEGRAM_UPDATE_JOURNAL_VERSION)
|
|
2014
|
+
continue;
|
|
2015
|
+
const groups = new Map();
|
|
2016
|
+
for (const entry of file.entries) {
|
|
2017
|
+
if (!entry.queueReceiptId)
|
|
2018
|
+
continue;
|
|
2019
|
+
const group = groups.get(entry.queueReceiptId) ?? [];
|
|
2020
|
+
group.push(entry);
|
|
2021
|
+
groups.set(entry.queueReceiptId, group);
|
|
2022
|
+
}
|
|
2023
|
+
for (const group of groups.values()) {
|
|
2024
|
+
const entry = group[0];
|
|
2025
|
+
if (entry.state !== "queued" || !entry.queueReceiptId || !entry.queueOwner ||
|
|
2026
|
+
!entry.queueKind || entry.queueHandoff || entry.inputClaim)
|
|
2027
|
+
continue;
|
|
2028
|
+
if (group.some(value => value.state !== "queued" || value.queueHandoff || value.inputClaim ||
|
|
2029
|
+
value.queueKind !== entry.queueKind || !isDeepStrictEqual(value.queueOwner, entry.queueOwner)))
|
|
2030
|
+
continue;
|
|
2031
|
+
plans.push({ path: source.path, recovery: {
|
|
2032
|
+
queueKind: entry.queueKind, receiptId: entry.queueReceiptId,
|
|
2033
|
+
sourceUpdateIds: group.map(value => value.updateId).sort((a, b) => a - b),
|
|
2034
|
+
deadOwner: cloneJournalQueueOwner(entry.queueOwner), recoveryOwner: { ...input.recoveryOwner },
|
|
2035
|
+
} });
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const liveness = input.getQueueProcessLiveness ?? getTelegramProcessLiveness;
|
|
2039
|
+
const dead = plans.filter(plan => {
|
|
2040
|
+
assertCurrent();
|
|
2041
|
+
const result = liveness(plan.recovery.deadOwner);
|
|
2042
|
+
assertCurrent();
|
|
2043
|
+
return result === "dead";
|
|
2044
|
+
});
|
|
2045
|
+
let removed = 0;
|
|
2046
|
+
for (const plan of dead) {
|
|
2047
|
+
assertCurrent();
|
|
2048
|
+
removed += input.withSourceReference(plan.path, () => {
|
|
2049
|
+
const journal = createTelegramUpdateJournalStore({
|
|
2050
|
+
path: plan.path, profileName: input.profile, botIdentity: input.botIdentity,
|
|
2051
|
+
queueRuntimeIdentity: input.recoveryOwner,
|
|
2052
|
+
// This private synchronous store stays inside the cross-family serializer already held above.
|
|
2053
|
+
withSourceSerialization(operation) { assertCurrent(); return operation(); },
|
|
2054
|
+
sourceAccess: { directory: dirname(plan.path), limits: input.limits },
|
|
2055
|
+
getQueueProcessLiveness(owner) {
|
|
2056
|
+
assertCurrent();
|
|
2057
|
+
const result = liveness(owner);
|
|
2058
|
+
assertCurrent();
|
|
2059
|
+
return result;
|
|
2060
|
+
},
|
|
2061
|
+
withWriterAdmission(operation) { assertCurrent(); return operation(); },
|
|
2062
|
+
});
|
|
2063
|
+
// The journal transaction rechecks whole-group identity, absence of handoff and fresh process-death proof.
|
|
2064
|
+
return journal.recoverDeadQueueOwner(plan.recovery).recoveredUpdateIds.length;
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
assertCurrent();
|
|
2068
|
+
return removed;
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
1991
2071
|
function cloneEntry(entry) {
|
|
1992
2072
|
return {
|
|
1993
2073
|
...entry,
|
|
@@ -73,6 +73,7 @@ export interface TelegramQueuedOutboundAttachmentView {
|
|
|
73
73
|
export interface TelegramOutboundAttachmentQueueTargetView {
|
|
74
74
|
queuedAttachments: TelegramQueuedOutboundAttachmentView[];
|
|
75
75
|
guestQueryId?: string;
|
|
76
|
+
guestInlineMessageId?: string;
|
|
76
77
|
}
|
|
77
78
|
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
78
79
|
chatId: number;
|
|
@@ -161,6 +162,17 @@ export declare function queueTelegramOutboundAttachments(options: {
|
|
|
161
162
|
}): Promise<TelegramOutboundAttachmentToolResult>;
|
|
162
163
|
export declare function deliverTelegramGuestCachedAttachment(options: {
|
|
163
164
|
guestQueryId: string;
|
|
165
|
+
guestInlineMessageId?: string;
|
|
166
|
+
editGuestInlineMessage?: (inlineMessageId: string, content: {
|
|
167
|
+
media: {
|
|
168
|
+
type: "photo" | "document" | "audio";
|
|
169
|
+
media: string;
|
|
170
|
+
caption?: string;
|
|
171
|
+
};
|
|
172
|
+
}, options: {
|
|
173
|
+
assertAuthority: () => void;
|
|
174
|
+
}) => Promise<void>;
|
|
175
|
+
isDeliveryActive?: () => boolean;
|
|
164
176
|
stagingChatId: number;
|
|
165
177
|
stagingTarget?: TelegramTarget;
|
|
166
178
|
attachment: TelegramQueuedOutboundAttachmentView;
|
|
@@ -375,6 +375,9 @@ export async function queueTelegramOutboundAttachments(options) {
|
|
|
375
375
|
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
376
376
|
statPath: options.statPath,
|
|
377
377
|
});
|
|
378
|
+
if (options.activeTurn.guestInlineMessageId && pendingAttachments.some((attachment) => getTelegramGuestAttachmentTransport(attachment.path).fileField === "voice")) {
|
|
379
|
+
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.");
|
|
380
|
+
}
|
|
378
381
|
options.activeTurn.queuedAttachments.push(...pendingAttachments);
|
|
379
382
|
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
380
383
|
return {
|
|
@@ -389,6 +392,16 @@ export async function queueTelegramOutboundAttachments(options) {
|
|
|
389
392
|
}
|
|
390
393
|
export async function deliverTelegramGuestCachedAttachment(options) {
|
|
391
394
|
const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
|
|
395
|
+
const isActive = () => options.isDeliveryActive?.() !== false;
|
|
396
|
+
const assertActive = () => {
|
|
397
|
+
if (!isActive())
|
|
398
|
+
throw new Error("Guest attachment delivery lost authority.");
|
|
399
|
+
};
|
|
400
|
+
if (!isActive())
|
|
401
|
+
return;
|
|
402
|
+
if (options.guestInlineMessageId && (!options.editGuestInlineMessage || transport.fileField === "voice")) {
|
|
403
|
+
throw new Error("Guest inline attachment editing is unavailable for this media.");
|
|
404
|
+
}
|
|
392
405
|
let stagingMessageId;
|
|
393
406
|
let answerAttempted = false;
|
|
394
407
|
try {
|
|
@@ -397,6 +410,8 @@ export async function deliverTelegramGuestCachedAttachment(options) {
|
|
|
397
410
|
...getTelegramMultipartTargetFields(options.stagingTarget),
|
|
398
411
|
}, transport.fileField, options.attachment.path, options.attachment.fileName));
|
|
399
412
|
stagingMessageId = message.message_id;
|
|
413
|
+
if (!isActive())
|
|
414
|
+
return;
|
|
400
415
|
const caption = options.caption
|
|
401
416
|
? Array.from(options.caption).slice(0, 1024).join("")
|
|
402
417
|
: undefined;
|
|
@@ -446,11 +461,26 @@ export async function deliverTelegramGuestCachedAttachment(options) {
|
|
|
446
461
|
...(caption ? { caption } : {}),
|
|
447
462
|
};
|
|
448
463
|
}
|
|
464
|
+
if (!isActive())
|
|
465
|
+
return;
|
|
449
466
|
answerAttempted = true;
|
|
450
|
-
|
|
467
|
+
if (options.guestInlineMessageId && options.editGuestInlineMessage) {
|
|
468
|
+
if (result.type === "voice")
|
|
469
|
+
throw new Error("Voice notes cannot replace inline placeholders.");
|
|
470
|
+
const media = result.type === "photo" ? result.photo_file_id
|
|
471
|
+
: result.type === "audio" ? result.audio_file_id : result.document_file_id;
|
|
472
|
+
await options.editGuestInlineMessage(options.guestInlineMessageId, {
|
|
473
|
+
media: { type: result.type, media, ...(caption ? { caption } : {}) },
|
|
474
|
+
}, { assertAuthority: assertActive });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
await options.answerGuestQuery(options.guestQueryId, result);
|
|
478
|
+
}
|
|
451
479
|
}
|
|
452
480
|
catch (error) {
|
|
453
|
-
if (!
|
|
481
|
+
if (!isActive())
|
|
482
|
+
return;
|
|
483
|
+
if (!options.guestInlineMessageId && !answerAttempted && options.answerGuestText && options.fallbackText) {
|
|
454
484
|
answerAttempted = true;
|
|
455
485
|
await options.answerGuestText(options.guestQueryId, options.fallbackText);
|
|
456
486
|
}
|
|
@@ -459,7 +489,7 @@ export async function deliverTelegramGuestCachedAttachment(options) {
|
|
|
459
489
|
}
|
|
460
490
|
}
|
|
461
491
|
finally {
|
|
462
|
-
if (stagingMessageId !== undefined) {
|
|
492
|
+
if (stagingMessageId !== undefined && isActive()) {
|
|
463
493
|
try {
|
|
464
494
|
await options.deleteMessage(options.stagingChatId, stagingMessageId);
|
|
465
495
|
}
|
|
@@ -506,10 +506,14 @@ export interface TelegramAgentEndRuntimeDeps<TTurn extends PendingTelegramTurn,
|
|
|
506
506
|
}) => Promise<void>;
|
|
507
507
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
508
508
|
/** Replaces the early guest ACK with the final text. */
|
|
509
|
-
editGuestReply?: (inlineMessageId: string, markdown: string
|
|
509
|
+
editGuestReply?: (inlineMessageId: string, markdown: string, options?: {
|
|
510
|
+
assertAuthority?: () => void;
|
|
511
|
+
}) => Promise<void>;
|
|
510
512
|
/** Cancels the animated guest placeholder before the final edit. */
|
|
511
513
|
stopGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
|
|
512
|
-
sendGuestAttachment?: (turn: TTurn, attachment: QueuedAttachment, caption?: string
|
|
514
|
+
sendGuestAttachment?: (turn: TTurn, attachment: QueuedAttachment, caption?: string, options?: {
|
|
515
|
+
isDeliveryActive?: () => boolean;
|
|
516
|
+
}) => Promise<void>;
|
|
513
517
|
sendGuestVoiceReply?: (turn: TTurn, plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>, caption?: string) => Promise<void>;
|
|
514
518
|
planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
515
519
|
sendOutboundReplyArtifacts?: (turn: TTurn, plan: TelegramAgentEndOutboundReplyPlan, options?: {
|
|
@@ -1377,13 +1377,55 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1377
1377
|
});
|
|
1378
1378
|
}
|
|
1379
1379
|
}
|
|
1380
|
-
if (
|
|
1381
|
-
|
|
1380
|
+
if (!isDeliveryActive())
|
|
1381
|
+
return;
|
|
1382
|
+
if (turn.guestInlineMessageId) {
|
|
1383
|
+
let experimentText = assistant.errorMessage
|
|
1382
1384
|
? "Telegram bridge: Pi failed while processing the request."
|
|
1383
1385
|
: finalText;
|
|
1386
|
+
const [attachment] = turn.queuedAttachments;
|
|
1387
|
+
if (!assistant.errorMessage && attachment) {
|
|
1388
|
+
try {
|
|
1389
|
+
if (!deps.sendGuestAttachment)
|
|
1390
|
+
throw new Error("Guest attachment delivery is unavailable.");
|
|
1391
|
+
await deps.sendGuestAttachment(turn, attachment, finalText || undefined, { isDeliveryActive });
|
|
1392
|
+
if (isDeliveryActive() && endPlan.shouldDispatchNext)
|
|
1393
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1398
|
+
phase: "guest-attachment", guestQueryId: turn.guestQueryId,
|
|
1399
|
+
});
|
|
1400
|
+
if (!isDeliveryActive())
|
|
1401
|
+
return;
|
|
1402
|
+
if (isTelegramApiCommitUnknownError(error)) {
|
|
1403
|
+
if (endPlan.shouldDispatchNext)
|
|
1404
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
experimentText = "Telegram bridge could not deliver the requested attachment. Please retry in a normal bot chat.";
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
else if (!assistant.errorMessage && hasOutboundArtifacts) {
|
|
1411
|
+
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");
|
|
1412
|
+
}
|
|
1413
|
+
if (!deps.editGuestReply) {
|
|
1414
|
+
deps.recordRuntimeEvent?.("delivery", new Error("Guest inline reply editing is unavailable."), {
|
|
1415
|
+
phase: "guest-ack-edit", guestQueryId: turn.guestQueryId,
|
|
1416
|
+
});
|
|
1417
|
+
if (endPlan.shouldDispatchNext)
|
|
1418
|
+
deps.dispatchNextQueuedTelegramTurn();
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1384
1421
|
if (experimentText) {
|
|
1385
1422
|
try {
|
|
1386
|
-
await deps.editGuestReply(turn.guestInlineMessageId, experimentText
|
|
1423
|
+
await deps.editGuestReply(turn.guestInlineMessageId, experimentText, {
|
|
1424
|
+
assertAuthority() {
|
|
1425
|
+
if (!isDeliveryActive())
|
|
1426
|
+
throw new Error("Guest reply edit lost authority.");
|
|
1427
|
+
},
|
|
1428
|
+
});
|
|
1387
1429
|
deps.recordRuntimeEvent?.("guest", new Error("Guest ACK edited the guest answer"), { phase: "guest-ack-edited", guestQueryId: turn.guestQueryId });
|
|
1388
1430
|
}
|
|
1389
1431
|
catch (error) {
|
|
@@ -1419,7 +1461,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1419
1461
|
const [guestAttachment] = turn.queuedAttachments;
|
|
1420
1462
|
if (guestAttachment && deps.sendGuestAttachment) {
|
|
1421
1463
|
try {
|
|
1422
|
-
await deps.sendGuestAttachment(turn, guestAttachment, finalText || undefined);
|
|
1464
|
+
await deps.sendGuestAttachment(turn, guestAttachment, finalText || undefined, { isDeliveryActive });
|
|
1423
1465
|
}
|
|
1424
1466
|
catch (error) {
|
|
1425
1467
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
@@ -163,8 +163,12 @@ export declare function createGuestMarkdownReplyEditor(deps: {
|
|
|
163
163
|
editGuestInlineMessage: (inlineMessageId: string, content: {
|
|
164
164
|
richMessage?: TelegramInputRichMessage;
|
|
165
165
|
text?: string;
|
|
166
|
+
}, options?: {
|
|
167
|
+
assertAuthority?: () => void;
|
|
166
168
|
}) => Promise<void>;
|
|
167
|
-
}): (inlineMessageId: string, markdown: string
|
|
169
|
+
}): (inlineMessageId: string, markdown: string, options?: {
|
|
170
|
+
assertAuthority?: () => void;
|
|
171
|
+
}) => Promise<void>;
|
|
168
172
|
/**
|
|
169
173
|
* Guest Mode placeholder rotation: the early ACK is the first placeholder
|
|
170
174
|
* frame and the runtime steps the remaining frames once per interval, moving
|
|
@@ -578,11 +578,11 @@ export function createGuestMarkdownReplySender(deps) {
|
|
|
578
578
|
* chat/message pair.
|
|
579
579
|
*/
|
|
580
580
|
export function createGuestMarkdownReplyEditor(deps) {
|
|
581
|
-
return async (inlineMessageId, markdown) => {
|
|
581
|
+
return async (inlineMessageId, markdown, options) => {
|
|
582
582
|
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
583
583
|
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
584
584
|
richMessage: { markdown: richMarkdown },
|
|
585
|
-
});
|
|
585
|
+
}, options);
|
|
586
586
|
};
|
|
587
587
|
}
|
|
588
588
|
/**
|
|
@@ -211,6 +211,8 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
211
211
|
canUsePreDispatchRebind?: (recipient?: Threads.TelegramWorkspaceRestoreRecipient) => boolean;
|
|
212
212
|
/** Strict complete namespace plus exact current/historical references; only an empty result clears journal protection. */
|
|
213
213
|
inspectTemporaryThreadSources?: (target: Queue.TelegramQueueTarget, requiredJournalBindingKeys: readonly string[], ownInputs?: readonly Threads.TelegramTemporaryThreadInput[]) => readonly number[] | undefined;
|
|
214
|
+
/** Explicit cleanup-only recovery; never called by a read-only protection observer. */
|
|
215
|
+
recoverTemporaryThreadDeadQueueOwners?: (target: Queue.TelegramQueueTarget, requiredJournalBindingKeys: readonly string[], ownInputs: readonly Threads.TelegramTemporaryThreadInput[], isCurrent: () => boolean) => number;
|
|
214
216
|
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
215
217
|
temporaryThreadCleanupDelayMs?: number;
|
|
216
218
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
@@ -4574,6 +4574,100 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4574
4574
|
}
|
|
4575
4575
|
await promptEnqueue(messages, ctx);
|
|
4576
4576
|
};
|
|
4577
|
+
/** Local menu acceptance owns a one-shot effect; source removal and cleanup precede its release, never its replay. */
|
|
4578
|
+
const stageTemporaryRerouteMenu = async (pending, command, messages, ctx, assertRecipientCurrent, completion) => {
|
|
4579
|
+
const original = pending.messages[0];
|
|
4580
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
4581
|
+
const temporary = pending.temporaryThread ?? findCreatedTemporaryThread(pending.sourceTarget);
|
|
4582
|
+
if (!cap?.isCurrent() || !temporary)
|
|
4583
|
+
return undefined;
|
|
4584
|
+
let accept, resume;
|
|
4585
|
+
const accepted = new Promise(resolve => { accept = resolve; });
|
|
4586
|
+
const released = new Promise(resolve => { resume = resolve; });
|
|
4587
|
+
const admission = {
|
|
4588
|
+
assertSourceCurrent() {
|
|
4589
|
+
if (!completion.isCurrent())
|
|
4590
|
+
throw new Error("Rerouted menu source authority changed.");
|
|
4591
|
+
},
|
|
4592
|
+
assertRecipientCurrent,
|
|
4593
|
+
reportCompleted() {
|
|
4594
|
+
const result = completion.reportCompleted();
|
|
4595
|
+
accept(result);
|
|
4596
|
+
return result;
|
|
4597
|
+
},
|
|
4598
|
+
async beforeEffects() {
|
|
4599
|
+
if (!(await released))
|
|
4600
|
+
throw new Error("Rerouted menu execution was not released.");
|
|
4601
|
+
},
|
|
4602
|
+
};
|
|
4603
|
+
const prepare = command.name === "start" ? commandHandler.prepareSelectedStartCommand
|
|
4604
|
+
: command.name === "help" ? commandHandler.prepareSelectedHelpCommand : commandHandler.prepareSelectedMenuCommand;
|
|
4605
|
+
const dispatch = prepare(command, messages, ctx, admission);
|
|
4606
|
+
if (!dispatch)
|
|
4607
|
+
return undefined;
|
|
4608
|
+
const execution = dispatch().catch(error => {
|
|
4609
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-execution" });
|
|
4610
|
+
return false;
|
|
4611
|
+
});
|
|
4612
|
+
if (!(await Promise.race([accepted, execution.then(() => false)]))) {
|
|
4613
|
+
resume(false);
|
|
4614
|
+
await execution;
|
|
4615
|
+
return undefined;
|
|
4616
|
+
}
|
|
4617
|
+
return async () => {
|
|
4618
|
+
let allowEffects = false;
|
|
4619
|
+
try {
|
|
4620
|
+
const confirmed = await new Promise(resolve => {
|
|
4621
|
+
let done = false, unsubscribe;
|
|
4622
|
+
const signal = Updates.getTelegramUpdateExecutionFence(original)?.signal;
|
|
4623
|
+
const finish = (result) => {
|
|
4624
|
+
if (done)
|
|
4625
|
+
return;
|
|
4626
|
+
done = true;
|
|
4627
|
+
clearTimeout(timer);
|
|
4628
|
+
unsubscribe?.();
|
|
4629
|
+
signal?.removeEventListener("abort", abort);
|
|
4630
|
+
resolve(result);
|
|
4631
|
+
};
|
|
4632
|
+
const abort = () => finish(false);
|
|
4633
|
+
const observe = () => {
|
|
4634
|
+
if (!cap.isCurrent() || !completion.isCurrent())
|
|
4635
|
+
finish(false);
|
|
4636
|
+
else if (completion.inspectCompletion())
|
|
4637
|
+
finish(true);
|
|
4638
|
+
};
|
|
4639
|
+
const timer = setTimeout(abort, deps.liveRebindCommandCompletionTimeoutMs ?? 8_000);
|
|
4640
|
+
timer.unref?.();
|
|
4641
|
+
unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(original, observe);
|
|
4642
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
4643
|
+
if (signal?.aborted)
|
|
4644
|
+
abort();
|
|
4645
|
+
else
|
|
4646
|
+
observe();
|
|
4647
|
+
});
|
|
4648
|
+
if (!confirmed)
|
|
4649
|
+
throw new Error("Rerouted menu source removal is unconfirmed; execution remains suppressed.");
|
|
4650
|
+
assertRecipientCurrent();
|
|
4651
|
+
cancelTemporaryThreadCleanup(temporary.token);
|
|
4652
|
+
try {
|
|
4653
|
+
await runTemporaryThreadCleanup(temporary.token, cap, false);
|
|
4654
|
+
}
|
|
4655
|
+
catch (error) {
|
|
4656
|
+
// Cleanup uncertainty retains its canonical grant, not command execution authority or a retry license.
|
|
4657
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-cleanup" });
|
|
4658
|
+
}
|
|
4659
|
+
assertRecipientCurrent();
|
|
4660
|
+
allowEffects = true;
|
|
4661
|
+
}
|
|
4662
|
+
catch (error) {
|
|
4663
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-menu-release" });
|
|
4664
|
+
}
|
|
4665
|
+
finally {
|
|
4666
|
+
resume(allowEffects);
|
|
4667
|
+
await execution;
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4670
|
+
};
|
|
4577
4671
|
const finalizePendingReroute = async (rerouteId, pending, query, successMessage, assertExecutionCurrent = Updates.createTelegramUpdateExecutionFenceGuard(query), retainRecord = false) => {
|
|
4578
4672
|
assertExecutionCurrent();
|
|
4579
4673
|
const dismissed = await dismissRerouteChooserMessage(query, assertExecutionCurrent);
|
|
@@ -4888,9 +4982,17 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4888
4982
|
if (!cancelledCurrent())
|
|
4889
4983
|
return;
|
|
4890
4984
|
entry = cap.adopt(entry);
|
|
4891
|
-
if (!entry ||
|
|
4892
|
-
|
|
4893
|
-
|
|
4985
|
+
if (!entry || !cap.isCurrent())
|
|
4986
|
+
return;
|
|
4987
|
+
if (isRerouteTargetProtected(target, undefined, entry) && deps.recoverTemporaryThreadDeadQueueOwners &&
|
|
4988
|
+
store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary") {
|
|
4989
|
+
const inputs = Threads.getTelegramTemporaryThreadInputs(entry);
|
|
4990
|
+
const removed = deps.recoverTemporaryThreadDeadQueueOwners(target, [cap.journalBindingKey, ...inputs.map(input => input.journalBindingKey)], inputs, cap.isCurrent);
|
|
4991
|
+
if (removed > 0)
|
|
4992
|
+
deps.recordRuntimeEvent?.("routing", "Discarded historical queue receipts from confirmed-dead owners before temporary Thread cleanup.", { phase: "temporary-thread-dead-owner-recovery", removedUpdateCount: removed });
|
|
4993
|
+
}
|
|
4994
|
+
// Recovery is not deletion authority: recapture every ordinary protection after the journal CAS.
|
|
4995
|
+
if (!cap.isCurrent() || isRerouteTargetProtected(target, undefined, entry))
|
|
4894
4996
|
return;
|
|
4895
4997
|
const issued = store.issueTemporaryThreadCleanup(entry, cap.authority);
|
|
4896
4998
|
if (!issued || !cap.isCurrent())
|
|
@@ -5911,7 +6013,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5911
6013
|
await deps.answerCallbackQuery(query.id);
|
|
5912
6014
|
return true;
|
|
5913
6015
|
};
|
|
5914
|
-
const executeUnboundRerouteCallbackOperation = async (query, ctx) => {
|
|
6016
|
+
const executeUnboundRerouteCallbackOperation = async (query, ctx, afterAdmission) => {
|
|
5915
6017
|
const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
|
|
5916
6018
|
if (!parsed)
|
|
5917
6019
|
return false;
|
|
@@ -6432,7 +6534,21 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6432
6534
|
const localMessages = pending.rerouteCopies
|
|
6433
6535
|
? cloneTelegramMessagesForThread(pending.messages, parsed.threadId, pending.rerouteCopies.bySourceUpdateId)
|
|
6434
6536
|
: reroutedMessages;
|
|
6435
|
-
|
|
6537
|
+
const command = pending.dispatchKind === "command"
|
|
6538
|
+
? getLiveRebindMenu(localMessages) ?? getLiveRebindHelp(localMessages) : undefined;
|
|
6539
|
+
const completion = pending.rerouteCopies && command
|
|
6540
|
+
? Updates.prepareTelegramDeferredSourceCompletion(pending.messages[0]) : undefined;
|
|
6541
|
+
if (command && completion) {
|
|
6542
|
+
const release = await stageTemporaryRerouteMenu(pending, command, localMessages, ctx, assertRerouteCurrent, completion);
|
|
6543
|
+
if (!release) {
|
|
6544
|
+
await deps.answerCallbackQuery(query.id, "Command acceptance unconfirmed; not resending");
|
|
6545
|
+
return true;
|
|
6546
|
+
}
|
|
6547
|
+
afterAdmission(release);
|
|
6548
|
+
}
|
|
6549
|
+
else {
|
|
6550
|
+
await dispatchPendingRerouteMessages(pending, localMessages, ctx);
|
|
6551
|
+
}
|
|
6436
6552
|
assertRerouteCurrent();
|
|
6437
6553
|
pending.messages = [];
|
|
6438
6554
|
return completeRoutedReroute();
|
|
@@ -6441,14 +6557,22 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6441
6557
|
const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
|
|
6442
6558
|
if (!parsed)
|
|
6443
6559
|
return false;
|
|
6444
|
-
return withPendingRerouteSelection(parsed.rerouteId, query, () => {
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6560
|
+
return withPendingRerouteSelection(parsed.rerouteId, query, async () => {
|
|
6561
|
+
let release;
|
|
6562
|
+
const operation = () => executeUnboundRerouteCallbackOperation(query, ctx, value => { release = value; });
|
|
6563
|
+
try {
|
|
6564
|
+
if (!deps.runWorkspaceOperation)
|
|
6565
|
+
return await operation();
|
|
6566
|
+
return await deps.runWorkspaceOperation({
|
|
6567
|
+
operationId: `workspace-reroute:${query.id}`,
|
|
6568
|
+
operationKind: "workspace.route-unbound-thread",
|
|
6569
|
+
scopes: [{ kind: "profile" }],
|
|
6570
|
+
}, operation);
|
|
6571
|
+
}
|
|
6572
|
+
finally {
|
|
6573
|
+
// Keep the exact source references, but release the non-reentrant Workspace gate before cleanup.
|
|
6574
|
+
await release?.();
|
|
6575
|
+
}
|
|
6452
6576
|
});
|
|
6453
6577
|
};
|
|
6454
6578
|
const handleUnboundRerouteCallback = async (query, ctx) => {
|
|
@@ -343,6 +343,12 @@ export interface TelegramAnswerGuestQueryOptions {
|
|
|
343
343
|
result?: TelegramGuestCachedMediaResult;
|
|
344
344
|
}
|
|
345
345
|
export interface TelegramEditGuestInlineMessageContent {
|
|
346
|
+
/** Cached media only: inline edits cannot upload a new local file. */
|
|
347
|
+
media?: {
|
|
348
|
+
type: "photo" | "document" | "audio";
|
|
349
|
+
media: string;
|
|
350
|
+
caption?: string;
|
|
351
|
+
};
|
|
346
352
|
text?: string;
|
|
347
353
|
richMessage?: TelegramInputRichMessage;
|
|
348
354
|
parseMode?: "HTML";
|
|
@@ -465,13 +471,12 @@ export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call"
|
|
|
465
471
|
answerCallbackQuery: (callbackQueryId: string, text?: string, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
466
472
|
answerGuestQuery: (guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions) => Promise<void>;
|
|
467
473
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* guest answers, so this exists only to falsify that behavior live.
|
|
474
|
+
* Answers the guest query and retains the sent inline message id for final
|
|
475
|
+
* text or cached-media replacement. Requires direct transport ownership;
|
|
476
|
+
* actual guest-media rendering remains live-client gated.
|
|
472
477
|
*/
|
|
473
478
|
answerGuestQueryForInlineMessage: (guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions) => Promise<string | undefined>;
|
|
474
|
-
editGuestInlineMessage: (inlineMessageId: string, content: TelegramEditGuestInlineMessageContent) => Promise<void>;
|
|
479
|
+
editGuestInlineMessage: (inlineMessageId: string, content: TelegramEditGuestInlineMessageContent, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
475
480
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
476
481
|
prepareTempDir: () => Promise<number>;
|
|
477
482
|
}
|