@llblab/pi-kit 0.36.2 → 0.37.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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -3
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +27 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +31 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +16 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +58 -39
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +21 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +10 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2 -15
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +16 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +18 -17
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +8 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +87 -51
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +22 -13
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +11 -7
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +2 -26
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
TelegramApiAuthorityError,
|
|
37
37
|
type TelegramApiCallOptions,
|
|
38
38
|
} from "./telegram-api.ts";
|
|
39
|
+
import type { TelegramTarget } from "./target.ts";
|
|
39
40
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
40
41
|
import {
|
|
41
42
|
acquireTelegramUpdateRouting,
|
|
@@ -624,6 +625,7 @@ export interface TelegramBridgeCommandStartPollingResult {
|
|
|
624
625
|
}
|
|
625
626
|
|
|
626
627
|
export interface TelegramBridgeCommandRegistrationDeps {
|
|
628
|
+
handleInternalAction?: TelegramSessionActionRuntime["handleInternalAction"];
|
|
627
629
|
promptForConfig: (
|
|
628
630
|
ctx: ExtensionCommandContext,
|
|
629
631
|
profileName?: string,
|
|
@@ -789,6 +791,7 @@ export function registerTelegramBridgeCommands(
|
|
|
789
791
|
pi: ExtensionAPI,
|
|
790
792
|
deps: TelegramBridgeCommandRegistrationDeps,
|
|
791
793
|
): void {
|
|
794
|
+
const handleInternalAction = deps.handleInternalAction;
|
|
792
795
|
pi.registerCommand("telegram-setup", {
|
|
793
796
|
description: "<profile> — Configure Telegram bot token",
|
|
794
797
|
handler: async (args, ctx) => {
|
|
@@ -804,9 +807,21 @@ export function registerTelegramBridgeCommands(
|
|
|
804
807
|
ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info");
|
|
805
808
|
},
|
|
806
809
|
});
|
|
807
|
-
pi.registerCommand(
|
|
810
|
+
pi.registerCommand(TELEGRAM_CONNECT_COMMAND_NAME, {
|
|
808
811
|
description: "<profile> — Start Telegram bridge",
|
|
809
812
|
handler: async (args, ctx) => {
|
|
813
|
+
const words = args.trim().split(/\s+/);
|
|
814
|
+
// Service dispatch never inherits profile activation or explicit-connect recovery authority.
|
|
815
|
+
if (words.some(word =>
|
|
816
|
+
word.toLowerCase().startsWith(TELEGRAM_INTERNAL_COMMAND_FLAG),
|
|
817
|
+
)) {
|
|
818
|
+
if (words[0] !== TELEGRAM_INTERNAL_COMMAND_FLAG || !handleInternalAction) {
|
|
819
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
await handleInternalAction(words.slice(1).join(" "), ctx);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
810
825
|
const sessionGeneration = deps.getSessionGeneration?.();
|
|
811
826
|
let intentId: string | undefined;
|
|
812
827
|
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
@@ -1194,34 +1209,38 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
|
|
|
1194
1209
|
) => void;
|
|
1195
1210
|
getAdmissionScope?: () => string | undefined;
|
|
1196
1211
|
getAdmissionJournalBinding?: () => string | undefined;
|
|
1212
|
+
/** Ordinary reply destination selected before publication, not resend authority. */
|
|
1213
|
+
getReplyTarget?: (
|
|
1214
|
+
message: TelegramCommandRuntimeMessage,
|
|
1215
|
+
) => TelegramTarget | undefined;
|
|
1197
1216
|
onControlQueued?: (
|
|
1198
1217
|
message: TelegramCommandRuntimeMessage,
|
|
1199
1218
|
receipt: TelegramQueueAdmissionReceipt,
|
|
1200
1219
|
) => void;
|
|
1201
1220
|
showStatus: (
|
|
1202
1221
|
chatId: number,
|
|
1203
|
-
replyToMessageId: number,
|
|
1222
|
+
replyToMessageId: number | undefined,
|
|
1204
1223
|
ctx: TContext,
|
|
1205
1224
|
threadId?: number,
|
|
1206
1225
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
1207
1226
|
) => Promise<void>;
|
|
1208
1227
|
openModelMenu: (
|
|
1209
1228
|
chatId: number,
|
|
1210
|
-
replyToMessageId: number,
|
|
1229
|
+
replyToMessageId: number | undefined,
|
|
1211
1230
|
ctx: TContext,
|
|
1212
1231
|
threadId?: number,
|
|
1213
1232
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
1214
1233
|
) => Promise<void>;
|
|
1215
1234
|
openSettingsMenu?: (
|
|
1216
1235
|
chatId: number,
|
|
1217
|
-
replyToMessageId: number,
|
|
1236
|
+
replyToMessageId: number | undefined,
|
|
1218
1237
|
ctx: TContext,
|
|
1219
1238
|
threadId?: number,
|
|
1220
1239
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
1221
1240
|
) => Promise<void>;
|
|
1222
1241
|
sendTextReply: (
|
|
1223
1242
|
chatId: number,
|
|
1224
|
-
replyToMessageId: number,
|
|
1243
|
+
replyToMessageId: number | undefined,
|
|
1225
1244
|
text: string,
|
|
1226
1245
|
options?: {
|
|
1227
1246
|
parseMode?: "HTML";
|
|
@@ -1336,6 +1355,7 @@ export function createTelegramCommandTargetQueueRuntime<
|
|
|
1336
1355
|
createControlItem,
|
|
1337
1356
|
enqueueControlItem: controlQueueController.enqueue,
|
|
1338
1357
|
}),
|
|
1358
|
+
getReplyTarget: deps.getReplyTarget,
|
|
1339
1359
|
getAdmissionScope: deps.getAdmissionScope,
|
|
1340
1360
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
1341
1361
|
onControlQueued: deps.onControlQueued,
|
|
@@ -1352,6 +1372,25 @@ export function createTelegramCommandTargetRuntime<
|
|
|
1352
1372
|
>(
|
|
1353
1373
|
deps: TelegramCommandTargetRuntimeDeps<TContext>,
|
|
1354
1374
|
): TelegramCommandTargetRuntime<TMessage, TContext> {
|
|
1375
|
+
const getReplyTarget = (
|
|
1376
|
+
message: TMessage,
|
|
1377
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
1378
|
+
) => {
|
|
1379
|
+
const source = getTelegramCommandMessageTarget(message);
|
|
1380
|
+
const recipient = options?.assertAuthority
|
|
1381
|
+
? undefined
|
|
1382
|
+
: deps.getReplyTarget?.(message);
|
|
1383
|
+
if (!recipient) return source;
|
|
1384
|
+
if (recipient.chatId !== source.chatId)
|
|
1385
|
+
throw new Error("Telegram command reply cannot change its recipient chat.");
|
|
1386
|
+
if (source.threadId !== undefined && recipient.threadId === undefined)
|
|
1387
|
+
throw new Error("Telegram command cannot move a Thread reply to All.");
|
|
1388
|
+
return {
|
|
1389
|
+
...recipient,
|
|
1390
|
+
replyToMessageId:
|
|
1391
|
+
recipient.threadId === source.threadId ? source.replyToMessageId : undefined,
|
|
1392
|
+
};
|
|
1393
|
+
};
|
|
1355
1394
|
return {
|
|
1356
1395
|
enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
|
|
1357
1396
|
const sourceUpdateId = message.pi_telegram_source_update_id;
|
|
@@ -1381,7 +1420,7 @@ export function createTelegramCommandTargetRuntime<
|
|
|
1381
1420
|
);
|
|
1382
1421
|
},
|
|
1383
1422
|
showStatus: (message, ctx, options) => {
|
|
1384
|
-
const target =
|
|
1423
|
+
const target = getReplyTarget(message, options);
|
|
1385
1424
|
return deps.showStatus(
|
|
1386
1425
|
target.chatId,
|
|
1387
1426
|
target.replyToMessageId,
|
|
@@ -1391,7 +1430,7 @@ export function createTelegramCommandTargetRuntime<
|
|
|
1391
1430
|
);
|
|
1392
1431
|
},
|
|
1393
1432
|
openModelMenu: (message, ctx, options) => {
|
|
1394
|
-
const target =
|
|
1433
|
+
const target = getReplyTarget(message, options);
|
|
1395
1434
|
return deps.openModelMenu(
|
|
1396
1435
|
target.chatId,
|
|
1397
1436
|
target.replyToMessageId,
|
|
@@ -1401,7 +1440,7 @@ export function createTelegramCommandTargetRuntime<
|
|
|
1401
1440
|
);
|
|
1402
1441
|
},
|
|
1403
1442
|
openSettingsMenu: async (message, ctx, options) => {
|
|
1404
|
-
const target =
|
|
1443
|
+
const target = getReplyTarget(message, options);
|
|
1405
1444
|
if (!deps.openSettingsMenu) {
|
|
1406
1445
|
await deps.sendTextReply(
|
|
1407
1446
|
target.chatId,
|
|
@@ -1423,7 +1462,7 @@ export function createTelegramCommandTargetRuntime<
|
|
|
1423
1462
|
);
|
|
1424
1463
|
},
|
|
1425
1464
|
sendTextReply: async (message, text, options) => {
|
|
1426
|
-
const target =
|
|
1465
|
+
const target = getReplyTarget(message, options);
|
|
1427
1466
|
const assertAuthority = options?.assertAuthority,
|
|
1428
1467
|
send = deps.sendTextReply;
|
|
1429
1468
|
const capturedOptions = { ...options, target };
|
|
@@ -2217,6 +2256,7 @@ export function createTelegramCommandHandlerTargetRuntime<
|
|
|
2217
2256
|
}),
|
|
2218
2257
|
appendControlItem: deps.appendControlItem,
|
|
2219
2258
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
2259
|
+
getReplyTarget: deps.getReplyTarget,
|
|
2220
2260
|
getAdmissionScope: deps.getAdmissionScope,
|
|
2221
2261
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
2222
2262
|
onControlQueued: deps.onControlQueued,
|
|
@@ -4157,18 +4197,16 @@ async function handleTelegramCommandRuntime<
|
|
|
4157
4197
|
);
|
|
4158
4198
|
}
|
|
4159
4199
|
|
|
4160
|
-
export const
|
|
4161
|
-
export const
|
|
4162
|
-
"Internal Telegram command cannot be run manually";
|
|
4200
|
+
export const TELEGRAM_CONNECT_COMMAND_NAME = "telegram-connect";
|
|
4201
|
+
export const TELEGRAM_INTERNAL_COMMAND_FLAG = "--internal";
|
|
4163
4202
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
|
|
4164
|
-
"This internal Telegram
|
|
4203
|
+
"This internal Telegram action cannot be run manually.";
|
|
4165
4204
|
|
|
4166
4205
|
function delayTelegramSessionAction(delayMs: number): Promise<void> {
|
|
4167
4206
|
return new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
4168
4207
|
}
|
|
4169
4208
|
|
|
4170
4209
|
export interface TelegramSessionActionRuntimeDeps {
|
|
4171
|
-
registerCommand: Pi.ExtensionAPI["registerCommand"];
|
|
4172
4210
|
sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
|
|
4173
4211
|
notifyResult: (
|
|
4174
4212
|
target: { chatId: number; threadId?: number; messageId: number },
|
|
@@ -4251,7 +4289,6 @@ function createTelegramSessionReplacementSettlementRuntime<TContext>(deps: {
|
|
|
4251
4289
|
}
|
|
4252
4290
|
|
|
4253
4291
|
export interface TelegramSessionActionAssemblyDeps {
|
|
4254
|
-
registerCommand: Pi.ExtensionAPI["registerCommand"];
|
|
4255
4292
|
sendUserMessage: Pi.ExtensionAPI["sendUserMessage"];
|
|
4256
4293
|
store: {
|
|
4257
4294
|
load: () => Promise<void>;
|
|
@@ -4333,7 +4370,6 @@ export function createTelegramSessionActionAssembly(
|
|
|
4333
4370
|
report(new Error("Telegram new-session result delivery failed."));
|
|
4334
4371
|
};
|
|
4335
4372
|
const action = createTelegramSessionActionRuntime({
|
|
4336
|
-
registerCommand: deps.registerCommand,
|
|
4337
4373
|
sendUserMessage: deps.sendUserMessage,
|
|
4338
4374
|
notifyResult(target, result) {
|
|
4339
4375
|
return sendTerminalResult(target, result);
|
|
@@ -4465,7 +4501,10 @@ type TelegramPendingInternalAction = {
|
|
|
4465
4501
|
};
|
|
4466
4502
|
|
|
4467
4503
|
export interface TelegramSessionActionRuntime {
|
|
4468
|
-
|
|
4504
|
+
handleInternalAction: (
|
|
4505
|
+
args: string,
|
|
4506
|
+
ctx: Pi.ExtensionCommandContext,
|
|
4507
|
+
) => Promise<void>;
|
|
4469
4508
|
scheduleAfterUpdate: (
|
|
4470
4509
|
updateId: number,
|
|
4471
4510
|
target: { chatId: number; threadId?: number; messageId: number },
|
|
@@ -4486,7 +4525,6 @@ export function createTelegramSessionActionRuntime(
|
|
|
4486
4525
|
}
|
|
4487
4526
|
| undefined;
|
|
4488
4527
|
let pendingAction: TelegramPendingInternalAction | undefined;
|
|
4489
|
-
let registered = false;
|
|
4490
4528
|
|
|
4491
4529
|
const reportFailure = (error: unknown): void => {
|
|
4492
4530
|
try {
|
|
@@ -4497,37 +4535,34 @@ export function createTelegramSessionActionRuntime(
|
|
|
4497
4535
|
};
|
|
4498
4536
|
|
|
4499
4537
|
return {
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
await deps.notifyResult(action.target, "failure");
|
|
4526
|
-
}
|
|
4527
|
-
return;
|
|
4538
|
+
async handleInternalAction(args, ctx) {
|
|
4539
|
+
const action = pendingAction;
|
|
4540
|
+
const [operation, token, ...extra] = args.trim().split(/\s+/);
|
|
4541
|
+
if (
|
|
4542
|
+
!action || operation !== "new" ||
|
|
4543
|
+
token !== action.token || extra.length !== 0
|
|
4544
|
+
) {
|
|
4545
|
+
ctx.ui.notify(TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE, "warning");
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
pendingAction = undefined;
|
|
4549
|
+
switch (action.kind) {
|
|
4550
|
+
case "replace-session":
|
|
4551
|
+
try {
|
|
4552
|
+
await deps.prepareReplacement?.(
|
|
4553
|
+
ctx,
|
|
4554
|
+
action.updateId,
|
|
4555
|
+
action.target,
|
|
4556
|
+
);
|
|
4557
|
+
const result = await ctx.newSession();
|
|
4558
|
+
if (result.cancelled)
|
|
4559
|
+
await deps.notifyResult(action.target, "cancelled");
|
|
4560
|
+
} catch (error) {
|
|
4561
|
+
reportFailure(error);
|
|
4562
|
+
await deps.notifyResult(action.target, "failure");
|
|
4528
4563
|
}
|
|
4529
|
-
|
|
4530
|
-
}
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4531
4566
|
},
|
|
4532
4567
|
scheduleAfterUpdate(updateId, target) {
|
|
4533
4568
|
if (pendingUpdateId !== undefined || pendingAction !== undefined)
|
|
@@ -4546,9 +4581,10 @@ export function createTelegramSessionActionRuntime(
|
|
|
4546
4581
|
pendingAction = { kind: "replace-session", token, updateId, target };
|
|
4547
4582
|
void Promise.resolve()
|
|
4548
4583
|
.then(() =>
|
|
4549
|
-
deps.sendUserMessage(
|
|
4550
|
-
|
|
4551
|
-
|
|
4584
|
+
deps.sendUserMessage(
|
|
4585
|
+
`/${TELEGRAM_CONNECT_COMMAND_NAME} ${TELEGRAM_INTERNAL_COMMAND_FLAG} new ${token}`,
|
|
4586
|
+
{ expandPromptTemplates: true },
|
|
4587
|
+
),
|
|
4552
4588
|
)
|
|
4553
4589
|
.catch((error) => {
|
|
4554
4590
|
pendingAction = undefined;
|
|
@@ -92,7 +92,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
92
92
|
getCommands,
|
|
93
93
|
getThinkingLevel,
|
|
94
94
|
sendUserMessage,
|
|
95
|
-
registerCommand,
|
|
96
95
|
setActiveTools,
|
|
97
96
|
setModel,
|
|
98
97
|
setThinkingLevel,
|
|
@@ -310,7 +309,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
310
309
|
captureAuthority: captureTelegramStateAuthority,
|
|
311
310
|
});
|
|
312
311
|
const sessionActionAssembly = Commands.createTelegramSessionActionAssembly({
|
|
313
|
-
registerCommand,
|
|
314
312
|
sendUserMessage,
|
|
315
313
|
store: threadStore,
|
|
316
314
|
getProfileName: configStore.getActiveProfileName,
|
|
@@ -1364,6 +1362,27 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1364
1362
|
},
|
|
1365
1363
|
getAdmissionScope: getTelegramUpdateAdmissionScope,
|
|
1366
1364
|
getAdmissionJournalBinding: getTelegramQueueJournalBinding,
|
|
1365
|
+
getCommandReplyTarget(message) {
|
|
1366
|
+
if (
|
|
1367
|
+
message.chat.type !== "private" ||
|
|
1368
|
+
"business_connection_id" in message ||
|
|
1369
|
+
!Updates.getTelegramUpdateExecutionFence(message) ||
|
|
1370
|
+
lockRuntime.owns()
|
|
1371
|
+
) return undefined;
|
|
1372
|
+
Updates.assertTelegramUpdateExecutionCurrent(message);
|
|
1373
|
+
const binding = resolveTelegramFollowerJournalBinding();
|
|
1374
|
+
const target = telegramBusFollowerRegistrationState.getTarget();
|
|
1375
|
+
if (
|
|
1376
|
+
!telegramBusFollowerRegistrationState.isRegistered() ||
|
|
1377
|
+
!binding ||
|
|
1378
|
+
binding.recoveryKey !==
|
|
1379
|
+
followerAdmissionLifecycleRuntime.getJournalBindingKey() ||
|
|
1380
|
+
!target ||
|
|
1381
|
+
(threadStore.getBotState().threadMode === "enabled" &&
|
|
1382
|
+
target.threadId === undefined)
|
|
1383
|
+
) throw new Error("Telegram command reply lost its registered recipient.");
|
|
1384
|
+
return { ...target };
|
|
1385
|
+
},
|
|
1367
1386
|
beginCommandEffectWork: publicationRuntime.beginWork,
|
|
1368
1387
|
getMessageOwnership: messageOwnershipRuntime.getForwardOwnership,
|
|
1369
1388
|
recordMessageOwnership: messageOwnershipRuntime.recordRouted,
|
|
@@ -1763,16 +1782,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1763
1782
|
},
|
|
1764
1783
|
);
|
|
1765
1784
|
},
|
|
1766
|
-
recoverRejectedRecipientTarget(error) {
|
|
1767
|
-
const rejected =
|
|
1768
|
-
error instanceof TelegramApi.TelegramApiFollowerNotAllowedError
|
|
1769
|
-
? error.requestTarget
|
|
1770
|
-
: undefined;
|
|
1771
|
-
const current = telegramBusFollowerRegistrationState.getTarget();
|
|
1772
|
-
return rejected && current?.threadId !== undefined
|
|
1773
|
-
? { rejected, current: { chatId: current.chatId, threadId: current.threadId } }
|
|
1774
|
-
: undefined;
|
|
1775
|
-
},
|
|
1776
1785
|
prepareUpdateForExecution(update) {
|
|
1777
1786
|
return BusFollower.prepareTelegramBusFollowerJournaledUpdateForExecution(
|
|
1778
1787
|
update,
|
|
@@ -2822,9 +2831,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
2822
2831
|
},
|
|
2823
2832
|
}.reset,
|
|
2824
2833
|
);
|
|
2825
|
-
sessionActionsRuntime.register();
|
|
2826
2834
|
Bindings.registerTelegramCommandsAndTools({
|
|
2827
2835
|
pi,
|
|
2836
|
+
handleInternalAction: sessionActionsRuntime.handleInternalAction,
|
|
2828
2837
|
agentDir: Paths.resolveAgentDir(),
|
|
2829
2838
|
configStore,
|
|
2830
2839
|
persistConfig: persistTelegramConfigWithSync,
|
|
@@ -90,7 +90,7 @@ export interface TelegramSettingsMenuCallbackDeps extends TelegramSettingsMutati
|
|
|
90
90
|
export interface TelegramSettingsMenuRuntime<TContext> {
|
|
91
91
|
openSettingsMenu: (
|
|
92
92
|
chatId: number,
|
|
93
|
-
replyToMessageId: number,
|
|
93
|
+
replyToMessageId: number | undefined,
|
|
94
94
|
ctx: TContext,
|
|
95
95
|
threadId?: number,
|
|
96
96
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
@@ -264,7 +264,7 @@ export interface TelegramMenuActionRuntimeDeps<
|
|
|
264
264
|
canOfferInFlightModelSwitch: (ctx: TContext) => boolean;
|
|
265
265
|
sendTextReply: (
|
|
266
266
|
chatId: number,
|
|
267
|
-
replyToMessageId: number,
|
|
267
|
+
replyToMessageId: number | undefined,
|
|
268
268
|
text: string,
|
|
269
269
|
options?: {
|
|
270
270
|
target?: { chatId: number; threadId?: number };
|
|
@@ -294,21 +294,21 @@ export interface TelegramMenuActionRuntime<
|
|
|
294
294
|
) => Promise<void>;
|
|
295
295
|
sendStatusMessage: (
|
|
296
296
|
chatId: number,
|
|
297
|
-
replyToMessageId: number,
|
|
297
|
+
replyToMessageId: number | undefined,
|
|
298
298
|
ctx: TContext,
|
|
299
299
|
threadId?: number,
|
|
300
300
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
301
301
|
) => Promise<void>;
|
|
302
302
|
openModelMenu: (
|
|
303
303
|
chatId: number,
|
|
304
|
-
replyToMessageId: number,
|
|
304
|
+
replyToMessageId: number | undefined,
|
|
305
305
|
ctx: TContext,
|
|
306
306
|
threadId?: number,
|
|
307
307
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
308
308
|
) => Promise<void>;
|
|
309
309
|
openThinkingMenu: (
|
|
310
310
|
chatId: number,
|
|
311
|
-
replyToMessageId: number,
|
|
311
|
+
replyToMessageId: number | undefined,
|
|
312
312
|
ctx: TContext,
|
|
313
313
|
threadId?: number,
|
|
314
314
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
@@ -1499,6 +1499,8 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1499
1499
|
getCurrentInstanceId?: () => string | undefined;
|
|
1500
1500
|
getAdmissionScope?: () => string | undefined;
|
|
1501
1501
|
getAdmissionJournalBinding?: () => string | undefined;
|
|
1502
|
+
getCommandReplyTarget?:
|
|
1503
|
+
Commands.TelegramCommandTargetRuntimeDeps<TContext>["getReplyTarget"];
|
|
1502
1504
|
getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
|
|
1503
1505
|
getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
|
|
1504
1506
|
recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
|
|
@@ -1640,7 +1642,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1640
1642
|
) => Promise<void>;
|
|
1641
1643
|
openSettingsMenu?: (
|
|
1642
1644
|
chatId: number,
|
|
1643
|
-
replyToMessageId: number,
|
|
1645
|
+
replyToMessageId: number | undefined,
|
|
1644
1646
|
ctx: TContext,
|
|
1645
1647
|
threadId?: number,
|
|
1646
1648
|
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
@@ -1728,7 +1730,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1728
1730
|
startGuestPlaceholder?: (inlineMessageId: string) => void;
|
|
1729
1731
|
sendTextReply: (
|
|
1730
1732
|
chatId: number,
|
|
1731
|
-
replyToMessageId: number,
|
|
1733
|
+
replyToMessageId: number | undefined,
|
|
1732
1734
|
text: string,
|
|
1733
1735
|
options?: {
|
|
1734
1736
|
parseMode?: "HTML";
|
|
@@ -10181,6 +10183,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10181
10183
|
appendControlItem: deps.queueMutationRuntime.append,
|
|
10182
10184
|
getAdmissionScope: deps.getAdmissionScope,
|
|
10183
10185
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
10186
|
+
getReplyTarget: deps.getCommandReplyTarget,
|
|
10184
10187
|
onControlQueued: (message, receipt) =>
|
|
10185
10188
|
reportQueueAdmission([message], [receipt]),
|
|
10186
10189
|
showStatus: deps.menuActions.sendStatusMessage,
|
|
@@ -1049,17 +1049,21 @@ export class TelegramApiStaleTargetError extends Error {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
|
-
/**
|
|
1052
|
+
/** A follower API refusal; legacy text alone does not prove the request was rejected before execution. */
|
|
1053
1053
|
export class TelegramApiFollowerNotAllowedError extends Error {
|
|
1054
1054
|
readonly requestTarget: { chatId: number; threadId: number } | undefined;
|
|
1055
|
+
/** Present only after a structured pre-execution refusal matches the requested API method. */
|
|
1056
|
+
readonly rejectedRequestMethod: string | undefined;
|
|
1055
1057
|
|
|
1056
1058
|
constructor(
|
|
1057
1059
|
message: string,
|
|
1058
1060
|
requestTarget?: { chatId: number; threadId: number },
|
|
1061
|
+
rejectedRequestMethod?: string,
|
|
1059
1062
|
) {
|
|
1060
1063
|
super(message);
|
|
1061
1064
|
this.name = "TelegramApiFollowerNotAllowedError";
|
|
1062
1065
|
this.requestTarget = requestTarget ? { ...requestTarget } : undefined;
|
|
1066
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
1063
1067
|
}
|
|
1064
1068
|
}
|
|
1065
1069
|
|
|
@@ -2086,7 +2090,8 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2086
2090
|
notBeforeMs: number;
|
|
2087
2091
|
};
|
|
2088
2092
|
const chatActionGates = new Map<string, ChatActionGate>();
|
|
2089
|
-
|
|
2093
|
+
// Pin shared cooldowns until every concurrent Thread settles; this state never owns their sends.
|
|
2094
|
+
const chatActionChatGates = new Map<string, { pendingRequests: number; notBeforeMs: number }>();
|
|
2090
2095
|
const getChatActionKeys = (
|
|
2091
2096
|
method: string,
|
|
2092
2097
|
body: Record<string, unknown>,
|
|
@@ -2126,7 +2131,7 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2126
2131
|
}
|
|
2127
2132
|
}
|
|
2128
2133
|
for (const [key, candidate] of chatActionChatGates) {
|
|
2129
|
-
if (
|
|
2134
|
+
if (candidate.pendingRequests === 0 && nowMs >= candidate.notBeforeMs) {
|
|
2130
2135
|
chatActionChatGates.delete(key);
|
|
2131
2136
|
}
|
|
2132
2137
|
}
|
|
@@ -2142,11 +2147,10 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2142
2147
|
if (chatActionChatGates.size >= chatActionMaxGates) {
|
|
2143
2148
|
return true as TResponse;
|
|
2144
2149
|
}
|
|
2145
|
-
chatGate = { notBeforeMs: 0 };
|
|
2150
|
+
chatGate = { pendingRequests: 0, notBeforeMs: 0 };
|
|
2146
2151
|
chatActionChatGates.set(chatActionKeys.chat, chatGate);
|
|
2147
2152
|
}
|
|
2148
2153
|
if (gate.inFlight) return (await gate.inFlight) as TResponse;
|
|
2149
|
-
if (chatGate.inFlight) return true as TResponse;
|
|
2150
2154
|
if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
|
|
2151
2155
|
return true as TResponse;
|
|
2152
2156
|
}
|
|
@@ -2194,10 +2198,10 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2194
2198
|
})
|
|
2195
2199
|
.finally(() => {
|
|
2196
2200
|
if (gate.inFlight === request) gate.inFlight = undefined;
|
|
2197
|
-
|
|
2201
|
+
chatGate.pendingRequests--;
|
|
2198
2202
|
});
|
|
2199
2203
|
gate.inFlight = request;
|
|
2200
|
-
chatGate.
|
|
2204
|
+
chatGate.pendingRequests++;
|
|
2201
2205
|
return request;
|
|
2202
2206
|
}
|
|
2203
2207
|
try {
|
|
@@ -10495,7 +10495,6 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
10495
10495
|
| "getRecipientBindingKey"
|
|
10496
10496
|
| "hasAuthority"
|
|
10497
10497
|
| "prepareUpdateForExecution"
|
|
10498
|
-
| "recoverRejectedRecipientTarget"
|
|
10499
10498
|
>;
|
|
10500
10499
|
leader: {
|
|
10501
10500
|
resolveBinding: () =>
|
|
@@ -10508,9 +10507,6 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
10508
10507
|
isRegistered: () => boolean;
|
|
10509
10508
|
getGeneration: () => string | undefined;
|
|
10510
10509
|
prepareUpdateForExecution: (update: TUpdate) => TUpdate;
|
|
10511
|
-
recoverRejectedRecipientTarget?: NonNullable<
|
|
10512
|
-
TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>["recoverRejectedRecipientTarget"]
|
|
10513
|
-
>;
|
|
10514
10510
|
/** Session succession before worker creation, under the exact generation-fenced binding. */
|
|
10515
10511
|
prepareBinding?: (
|
|
10516
10512
|
binding: TelegramUpdateAdmissionLifecycleJournalBinding & {
|
|
@@ -10535,13 +10531,6 @@ export type TelegramUpdateAdmissionWorkerRuntimeDeps<
|
|
|
10535
10531
|
execution?: TelegramUpdateExecutionFence,
|
|
10536
10532
|
) => Promise<void>;
|
|
10537
10533
|
prepareUpdateForExecution?: (update: TUpdate) => TUpdate;
|
|
10538
|
-
/**
|
|
10539
|
-
* Recipient-only recovery for a journaled message whose own Thread changed (for example after its Thread was re-created):
|
|
10540
|
-
* classifies a leader refusal of a reply to the original Thread and returns that Thread plus the owner's current one.
|
|
10541
|
-
*/
|
|
10542
|
-
recoverRejectedRecipientTarget?: (error: unknown) =>
|
|
10543
|
-
| { rejected: { chatId: number; threadId: number }; current: TelegramTarget & { threadId: number } }
|
|
10544
|
-
| undefined;
|
|
10545
10534
|
registry?: TelegramUpdateHandlerRegistry;
|
|
10546
10535
|
};
|
|
10547
10536
|
|
|
@@ -10668,7 +10657,6 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10668
10657
|
const typedUpdate = update as TUpdate;
|
|
10669
10658
|
const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
|
|
10670
10659
|
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
10671
|
-
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
10672
10660
|
const projectTo = (destination: TelegramTarget & { threadId: number }): TUpdate => ({ ...prepared,
|
|
10673
10661
|
...(prepared.message ? { message: prepared.message.message_thread_id === destination.threadId ? { ...prepared.message }
|
|
10674
10662
|
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: destination.threadId, reply_to_message: undefined }) } : {}),
|
|
@@ -10686,17 +10674,8 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
10686
10674
|
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
10687
10675
|
return { kind: "complete" };
|
|
10688
10676
|
}
|
|
10689
|
-
//
|
|
10690
|
-
//
|
|
10691
|
-
const recovery = !target ? deps.recoverRejectedRecipientTarget?.(error) : undefined;
|
|
10692
|
-
const message = prepared.message ?? prepared.edited_message;
|
|
10693
|
-
if (recovery && message && message.message_thread_id === recovery.rejected.threadId &&
|
|
10694
|
-
message.chat?.id === recovery.rejected.chatId && recovery.current.chatId === recovery.rejected.chatId &&
|
|
10695
|
-
recovery.current.threadId !== recovery.rejected.threadId) {
|
|
10696
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "recipient-target-recovery", updateId: update.update_id,
|
|
10697
|
-
chatId: recovery.current.chatId, staleThreadId: recovery.rejected.threadId, threadId: recovery.current.threadId });
|
|
10698
|
-
return await executeUpdate(projectTo(recovery.current), ctx, signal);
|
|
10699
|
-
}
|
|
10677
|
+
// A refused reply proves only that send was not issued, not that the handler made no prior effects.
|
|
10678
|
+
// Leave failure settlement to the worker's retry policy; never re-enter the handler to recover delivery here.
|
|
10700
10679
|
throw error;
|
|
10701
10680
|
}
|
|
10702
10681
|
},
|
|
@@ -10777,9 +10756,6 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
|
|
|
10777
10756
|
spendHistoricalInput: undefined,
|
|
10778
10757
|
onHeldSourcesPrepared: undefined,
|
|
10779
10758
|
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
10780
|
-
...(deps.follower.recoverRejectedRecipientTarget
|
|
10781
|
-
? { recoverRejectedRecipientTarget: deps.follower.recoverRejectedRecipientTarget }
|
|
10782
|
-
: {}),
|
|
10783
10759
|
});
|
|
10784
10760
|
},
|
|
10785
10761
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic macOS CI test shards
|
|
3
|
+
* Zones: CI, test execution
|
|
4
|
+
* Keeps real-process tests isolated and partitions the remaining files without changing test deadlines.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { readdirSync } from "node:fs";
|
|
8
|
+
import { delimiter, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const [shard, phase, mode, ...extra] = process.argv.slice(2);
|
|
12
|
+
if (!["0", "1"].includes(shard) || !["process", "remaining"].includes(phase) ||
|
|
13
|
+
(mode !== undefined && mode !== "--list") || extra.length) {
|
|
14
|
+
throw new Error("Usage: run-macos-tests.mjs <0|1> <process|remaining> [--list]");
|
|
15
|
+
}
|
|
16
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
17
|
+
const entries = readdirSync(join(root, "tests"), { withFileTypes: true })
|
|
18
|
+
.filter((entry) => entry.name.endsWith(".test.ts"));
|
|
19
|
+
if (entries.some((entry) => !entry.isFile()))
|
|
20
|
+
throw new Error("The CI test inventory must contain only regular test files.");
|
|
21
|
+
const names = entries.map((entry) => entry.name).sort();
|
|
22
|
+
const processTest = "process-shutdown.test.ts";
|
|
23
|
+
if (!names.includes(processTest)) throw new Error("The real-process test suite is missing.");
|
|
24
|
+
const files = (phase === "process"
|
|
25
|
+
? (shard === "0" ? [processTest] : [])
|
|
26
|
+
: names.filter((name) => name !== processTest).filter((_, index) => index % 2 === Number(shard)))
|
|
27
|
+
.map((name) => `tests/${name}`);
|
|
28
|
+
if (mode === "--list") {
|
|
29
|
+
console.log(JSON.stringify(files));
|
|
30
|
+
} else if (files.length) {
|
|
31
|
+
console.log(`macOS shard ${Number(shard) + 1}/2 ${phase}: ${files.length} test file(s)`);
|
|
32
|
+
const result = spawnSync(process.execPath, [
|
|
33
|
+
"--experimental-strip-types", "--import", "./tests/fixtures/canonical-tmpdir.ts",
|
|
34
|
+
"--test", "--test-concurrency=1", `--test-reporter=${phase === "process" ? "spec" : "dot"}`,
|
|
35
|
+
...files,
|
|
36
|
+
], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
env: { ...process.env, PATH: [join(root, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(delimiter) },
|
|
40
|
+
});
|
|
41
|
+
if (result.error) throw result.error;
|
|
42
|
+
process.exitCode = result.status ?? 1;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@llblab/pi-codex-usage": "0.13.2",
|
|
51
51
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
52
52
|
"@llblab/pi-state-flow": "0.26.5",
|
|
53
|
-
"@llblab/pi-telegram": "0.
|
|
53
|
+
"@llblab/pi-telegram": "0.58.1",
|
|
54
54
|
"@llblab/skills": "1.15.0"
|
|
55
55
|
},
|
|
56
56
|
"bundledDependencies": [
|