@llblab/pi-kit 0.36.1 → 0.36.3
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 +3 -3
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +43 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +45 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +23 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +24 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +7 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +22 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +52 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +120 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +9 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +23 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +30 -13
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +25 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +34 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +54 -11
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +4 -3
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +142 -26
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +18 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +9 -6
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
- package/node_modules/@llblab/pi-telegram/package.json +2 -1
- package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
- package/package.json +4 -4
|
@@ -629,6 +629,7 @@ export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
|
629
629
|
createControlItem,
|
|
630
630
|
enqueueControlItem: controlQueueController.enqueue,
|
|
631
631
|
}),
|
|
632
|
+
getReplyTarget: deps.getReplyTarget,
|
|
632
633
|
getAdmissionScope: deps.getAdmissionScope,
|
|
633
634
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
634
635
|
onControlQueued: deps.onControlQueued,
|
|
@@ -639,6 +640,22 @@ export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
|
639
640
|
});
|
|
640
641
|
}
|
|
641
642
|
export function createTelegramCommandTargetRuntime(deps) {
|
|
643
|
+
const getReplyTarget = (message, options) => {
|
|
644
|
+
const source = getTelegramCommandMessageTarget(message);
|
|
645
|
+
const recipient = options?.assertAuthority
|
|
646
|
+
? undefined
|
|
647
|
+
: deps.getReplyTarget?.(message);
|
|
648
|
+
if (!recipient)
|
|
649
|
+
return source;
|
|
650
|
+
if (recipient.chatId !== source.chatId)
|
|
651
|
+
throw new Error("Telegram command reply cannot change its recipient chat.");
|
|
652
|
+
if (source.threadId !== undefined && recipient.threadId === undefined)
|
|
653
|
+
throw new Error("Telegram command cannot move a Thread reply to All.");
|
|
654
|
+
return {
|
|
655
|
+
...recipient,
|
|
656
|
+
replyToMessageId: recipient.threadId === source.threadId ? source.replyToMessageId : undefined,
|
|
657
|
+
};
|
|
658
|
+
};
|
|
642
659
|
return {
|
|
643
660
|
enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
|
|
644
661
|
const sourceUpdateId = message.pi_telegram_source_update_id;
|
|
@@ -659,15 +676,15 @@ export function createTelegramCommandTargetRuntime(deps) {
|
|
|
659
676
|
deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined);
|
|
660
677
|
},
|
|
661
678
|
showStatus: (message, ctx, options) => {
|
|
662
|
-
const target =
|
|
679
|
+
const target = getReplyTarget(message, options);
|
|
663
680
|
return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
664
681
|
},
|
|
665
682
|
openModelMenu: (message, ctx, options) => {
|
|
666
|
-
const target =
|
|
683
|
+
const target = getReplyTarget(message, options);
|
|
667
684
|
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
668
685
|
},
|
|
669
686
|
openSettingsMenu: async (message, ctx, options) => {
|
|
670
|
-
const target =
|
|
687
|
+
const target = getReplyTarget(message, options);
|
|
671
688
|
if (!deps.openSettingsMenu) {
|
|
672
689
|
await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
|
|
673
690
|
return;
|
|
@@ -675,7 +692,7 @@ export function createTelegramCommandTargetRuntime(deps) {
|
|
|
675
692
|
await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
676
693
|
},
|
|
677
694
|
sendTextReply: async (message, text, options) => {
|
|
678
|
-
const target =
|
|
695
|
+
const target = getReplyTarget(message, options);
|
|
679
696
|
const assertAuthority = options?.assertAuthority, send = deps.sendTextReply;
|
|
680
697
|
const capturedOptions = { ...options, target };
|
|
681
698
|
assertAuthority?.();
|
|
@@ -1061,6 +1078,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
1061
1078
|
}),
|
|
1062
1079
|
appendControlItem: deps.appendControlItem,
|
|
1063
1080
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1081
|
+
getReplyTarget: deps.getReplyTarget,
|
|
1064
1082
|
getAdmissionScope: deps.getAdmissionScope,
|
|
1065
1083
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
1066
1084
|
onControlQueued: deps.onControlQueued,
|
|
@@ -454,17 +454,32 @@ export default function (pi) {
|
|
|
454
454
|
return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(threadStore, binding);
|
|
455
455
|
},
|
|
456
456
|
});
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
457
|
+
const inactiveThreadCleanupRuntime = ThreadCleanupManager.createTelegramInactiveThreadCleanupRuntime({
|
|
458
|
+
store: threadStore,
|
|
459
|
+
captureAuthority() {
|
|
460
|
+
const ledger = workspaceAdmissionRuntime.resolve();
|
|
461
|
+
const leaderEpoch = getCurrentLeaderEpoch();
|
|
462
|
+
const operator = configStore.getAllowedUserId();
|
|
463
|
+
if (!ledger || leaderEpoch === undefined || operator === undefined)
|
|
464
|
+
return undefined;
|
|
465
|
+
const stamp = telegramTransportStampRuntime.getStamp();
|
|
466
|
+
const generation = telegramSessionContextStore.getGeneration();
|
|
467
|
+
return {
|
|
468
|
+
profileName: configStore.getActiveProfileName() ?? "default",
|
|
469
|
+
leaderEpoch, ledger,
|
|
470
|
+
isCurrent() {
|
|
471
|
+
return getCurrentLeaderEpoch() === leaderEpoch &&
|
|
472
|
+
telegramTransportStampRuntime.isActive(stamp) &&
|
|
473
|
+
configStore.getAllowedUserId() === operator &&
|
|
474
|
+
telegramSessionContextStore.getGeneration() === generation;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
460
477
|
},
|
|
461
|
-
listBindings: threadStore.listWorkspaceBindings,
|
|
462
478
|
getProtection: captureWorkspaceExternalProtection,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const profileName = configStore.getActiveProfileName() ?? "default";
|
|
479
|
+
getProcessLiveness: ProcessIdentity.getTelegramProcessLiveness,
|
|
480
|
+
runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
|
|
481
|
+
getWorkStore(authority) {
|
|
482
|
+
const profileName = authority.profileName;
|
|
468
483
|
const botToken = configStore.getBotToken();
|
|
469
484
|
if (!botToken)
|
|
470
485
|
throw new Error("Telegram Thread cleanup review requires an active bot token.");
|
|
@@ -474,9 +489,16 @@ export default function (pi) {
|
|
|
474
489
|
tokenSha256: Journal.createTelegramUpdateJournalBotIdentity({
|
|
475
490
|
botToken,
|
|
476
491
|
}).tokenSha256,
|
|
492
|
+
admissionProfileKey: authority.ledger.getProfileKey(),
|
|
477
493
|
});
|
|
478
494
|
},
|
|
479
495
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
496
|
+
deleteThread(authorize) {
|
|
497
|
+
return directTelegramApiRuntime.deleteWorkspaceThread(authorize);
|
|
498
|
+
},
|
|
499
|
+
onError(error) {
|
|
500
|
+
recordRuntimeEvent("telegram", error, { phase: "manual-inactive-thread-cleanup" });
|
|
501
|
+
},
|
|
480
502
|
});
|
|
481
503
|
const updateAdmissionRuntimeBinding = Updates.createTelegramUpdateAdmissionRuntimeBinding({
|
|
482
504
|
isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
|
|
@@ -940,7 +962,8 @@ export default function (pi) {
|
|
|
940
962
|
sendInteractiveMessage,
|
|
941
963
|
answerCallbackQuery,
|
|
942
964
|
...configControls,
|
|
943
|
-
reviewInactiveThreads:
|
|
965
|
+
reviewInactiveThreads: inactiveThreadCleanupRuntime.review,
|
|
966
|
+
cleanInactiveThreads: inactiveThreadCleanupRuntime.clean,
|
|
944
967
|
getThreadDisplayMode() {
|
|
945
968
|
return threadStore.getBotState().threadMode === "enabled"
|
|
946
969
|
? Config.resolveTelegramThreadDisplayMode(configStore.get())
|
|
@@ -1070,6 +1093,25 @@ export default function (pi) {
|
|
|
1070
1093
|
},
|
|
1071
1094
|
getAdmissionScope: getTelegramUpdateAdmissionScope,
|
|
1072
1095
|
getAdmissionJournalBinding: getTelegramQueueJournalBinding,
|
|
1096
|
+
getCommandReplyTarget(message) {
|
|
1097
|
+
if (message.chat.type !== "private" ||
|
|
1098
|
+
"business_connection_id" in message ||
|
|
1099
|
+
!Updates.getTelegramUpdateExecutionFence(message) ||
|
|
1100
|
+
lockRuntime.owns())
|
|
1101
|
+
return undefined;
|
|
1102
|
+
Updates.assertTelegramUpdateExecutionCurrent(message);
|
|
1103
|
+
const binding = resolveTelegramFollowerJournalBinding();
|
|
1104
|
+
const target = telegramBusFollowerRegistrationState.getTarget();
|
|
1105
|
+
if (!telegramBusFollowerRegistrationState.isRegistered() ||
|
|
1106
|
+
!binding ||
|
|
1107
|
+
binding.recoveryKey !==
|
|
1108
|
+
followerAdmissionLifecycleRuntime.getJournalBindingKey() ||
|
|
1109
|
+
!target ||
|
|
1110
|
+
(threadStore.getBotState().threadMode === "enabled" &&
|
|
1111
|
+
target.threadId === undefined))
|
|
1112
|
+
throw new Error("Telegram command reply lost its registered recipient.");
|
|
1113
|
+
return { ...target };
|
|
1114
|
+
},
|
|
1073
1115
|
beginCommandEffectWork: publicationRuntime.beginWork,
|
|
1074
1116
|
getMessageOwnership: messageOwnershipRuntime.getForwardOwnership,
|
|
1075
1117
|
recordMessageOwnership: messageOwnershipRuntime.recordRouted,
|
|
@@ -159,6 +159,7 @@ export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOp
|
|
|
159
159
|
recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
160
160
|
}
|
|
161
161
|
export declare function resolveGenerativeAppDir(agentDir: string, app: string): string;
|
|
162
|
+
export declare function isGenerativeAppInstalled(agentDir: string, app: string): Promise<boolean>;
|
|
162
163
|
export declare function resolveGenerativeAppModulePath(agentDir: string, app: string): string;
|
|
163
164
|
export declare function invokeGenerativeApp(options: GenerativeAppRuntimeOptions & {
|
|
164
165
|
argument?: unknown;
|
|
@@ -86,6 +86,15 @@ export function resolveGenerativeAppDir(agentDir, app) {
|
|
|
86
86
|
assertAppName(app);
|
|
87
87
|
return join(getAppsRoot(agentDir), app);
|
|
88
88
|
}
|
|
89
|
+
export async function isGenerativeAppInstalled(agentDir, app) {
|
|
90
|
+
try {
|
|
91
|
+
await stat(resolveGenerativeAppModulePath(agentDir, app));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
89
98
|
export function resolveGenerativeAppModulePath(agentDir, app) {
|
|
90
99
|
return join(resolveGenerativeAppDir(agentDir, app), `${app}.mjs`);
|
|
91
100
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Transport authority, durable bindings, and queue custody remain with their owners.
|
|
6
6
|
*/
|
|
7
7
|
import * as BusFollower from "./bus-follower.ts";
|
|
8
|
-
import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
|
|
8
|
+
import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
|
|
9
9
|
import * as Machine from "./machine.ts";
|
|
10
10
|
import * as Queue from "./queue.ts";
|
|
11
11
|
export declare function createAgentStartDedupHook(inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>, resetReplyDedup: () => void, schedulePublication?: (task: () => Promise<void>) => void): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>;
|
|
@@ -14,6 +14,7 @@ type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt">
|
|
|
14
14
|
};
|
|
15
15
|
export interface TelegramBeforeAgentStartResult {
|
|
16
16
|
systemPrompt?: string | string[];
|
|
17
|
+
message?: BeforeAgentStartEventResult["message"];
|
|
17
18
|
}
|
|
18
19
|
type TelegramBeforeAgentStartReturn = Promise<TelegramBeforeAgentStartResult | undefined> | TelegramBeforeAgentStartResult | undefined;
|
|
19
20
|
type TelegramLifecycleModel = ExtensionContext["model"];
|
|
@@ -52,7 +52,7 @@ export interface TelegramSettingsMenuCallbackDeps extends TelegramSettingsMutati
|
|
|
52
52
|
sectionRegistry?: TelegramSectionRegistry;
|
|
53
53
|
}
|
|
54
54
|
export interface TelegramSettingsMenuRuntime<TContext> {
|
|
55
|
-
openSettingsMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
55
|
+
openSettingsMenu: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
56
56
|
handleCallbackQuery: (query: {
|
|
57
57
|
id: string;
|
|
58
58
|
data?: string;
|
|
@@ -49,7 +49,7 @@ export function buildAutomaticThreadCleanupSettingsText(enabled) {
|
|
|
49
49
|
"<code>-</code> <code>on</code> (default): delete the bound thread and release Telegram authority on graceful quit.",
|
|
50
50
|
"<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual <code>/telegram-disconnect</code> still confirms and deletes it.",
|
|
51
51
|
"",
|
|
52
|
-
"Review inactive tabs checks current owner and work evidence. Review never deletes tabs.",
|
|
52
|
+
"On the leader instance, Review inactive tabs checks current owner and work evidence, including tabs without a letter. Review never deletes tabs.",
|
|
53
53
|
].join("\n");
|
|
54
54
|
}
|
|
55
55
|
function buildInactiveThreadReviewText(count) {
|
|
@@ -58,7 +58,8 @@ function buildInactiveThreadReviewText(count) {
|
|
|
58
58
|
"",
|
|
59
59
|
`${count} proven inactive tab${count === 1 ? "" : "s"}.`,
|
|
60
60
|
"No tabs were deleted.",
|
|
61
|
-
"
|
|
61
|
+
"Clean inactive tabs deletes these Telegram tabs and all their messages. Pi session history and project files are not removed.",
|
|
62
|
+
"Clean rechecks owner and work protection; a changed or protected tab is skipped. Choose Back to leave without deleting anything.",
|
|
62
63
|
].join("\n");
|
|
63
64
|
}
|
|
64
65
|
export function buildInactiveThreadReviewReplyMarkup(operationId, canCleanInactiveThreads = false) {
|
|
@@ -87,7 +87,7 @@ export interface TelegramMenuActionRuntimeDeps<TContext, TModel extends MenuMode
|
|
|
87
87
|
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
88
88
|
isIdle: (ctx: TContext) => boolean;
|
|
89
89
|
canOfferInFlightModelSwitch: (ctx: TContext) => boolean;
|
|
90
|
-
sendTextReply: (chatId: number, replyToMessageId: number, text: string, options?: {
|
|
90
|
+
sendTextReply: (chatId: number, replyToMessageId: number | undefined, text: string, options?: {
|
|
91
91
|
target?: {
|
|
92
92
|
chatId: number;
|
|
93
93
|
threadId?: number;
|
|
@@ -102,9 +102,9 @@ export interface TelegramMenuActionRuntime<TContext, TModel extends MenuModel =
|
|
|
102
102
|
updateModelMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
|
|
103
103
|
updateThinkingMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
|
|
104
104
|
updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
|
|
105
|
-
sendStatusMessage: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
106
|
-
openModelMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
107
|
-
openThinkingMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
105
|
+
sendStatusMessage: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
106
|
+
openModelMenu: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
107
|
+
openThinkingMenu: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
108
108
|
}
|
|
109
109
|
export declare function handleTelegramMenuCallbackEntry(callbackQueryId: string, data: string | undefined, state: TelegramModelMenuState | undefined, deps: TelegramMenuCallbackEntryDeps): Promise<void>;
|
|
110
110
|
export declare function handleStoredTelegramMenuCallback<TModel extends MenuModel = MenuModel>(query: MenuCallbackQuery, deps: StoredTelegramMenuCallbackDeps<TModel>): Promise<void>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
|
|
5
5
|
*/
|
|
6
6
|
import type { AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
7
|
-
import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type TurnEndEvent, type TurnEndEventResult, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent, };
|
|
7
|
+
import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type BeforeAgentStartEventResult, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type MessageEndEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, type TurnEndEvent, type TurnEndEventResult, type UIPromptEndEvent, type UIPromptStartEvent } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, TurnEndEvent, TurnEndEventResult, UIPromptEndEvent, UIPromptStartEvent, };
|
|
9
9
|
export interface SessionCompactFailedEvent {
|
|
10
10
|
type: "session_compact_failed";
|
|
11
11
|
reason: "manual" | "threshold" | "overflow";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram prompt injection helpers
|
|
3
3
|
* Zones: pi agent prompts, telegram guidance
|
|
4
|
-
* Owns Telegram
|
|
4
|
+
* Owns stable Telegram guidance, turn-scoped context, and compatibility fallback hooks
|
|
5
5
|
*/
|
|
6
|
-
import type { BeforeAgentStartEvent } from "./pi.ts";
|
|
6
|
+
import type { BeforeAgentStartEvent, BeforeAgentStartEventResult } from "./pi.ts";
|
|
7
7
|
export declare const TELEGRAM_CONNECTED_CONTEXT_MESSAGE = "Telegram session connected. Use Telegram features for Telegram-originated turns or explicit Telegram requests; connectivity alone is not user intent.";
|
|
8
8
|
export declare const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE = "Telegram session disconnected. Do not use Telegram delivery, actions, or Telegram-specific reply features unless the user reconnects it.";
|
|
9
9
|
export declare const TELEGRAM_ATTACH_PROMPT_SNIPPET = "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
|
|
@@ -37,6 +37,9 @@ type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt"
|
|
|
37
37
|
type TelegramBeforeAgentStartResult = {
|
|
38
38
|
systemPrompt: TelegramSystemPrompt;
|
|
39
39
|
};
|
|
40
|
+
type TelegramProactiveBeforeAgentStartResult = Omit<BeforeAgentStartEventResult, "systemPrompt"> & {
|
|
41
|
+
systemPrompt?: TelegramSystemPrompt;
|
|
42
|
+
};
|
|
40
43
|
type TelegramBeforeAgentStartHook = (event: TelegramBeforeAgentStartEvent) => TelegramBeforeAgentStartResult;
|
|
41
44
|
export declare function buildTelegramBridgeSystemPrompt(options: {
|
|
42
45
|
prompt?: string;
|
|
@@ -55,5 +58,5 @@ export interface TelegramProactivePromptHookDeps<TContext> {
|
|
|
55
58
|
reconcileAvailability?: () => void;
|
|
56
59
|
isAvailable: (ctx: TContext) => boolean;
|
|
57
60
|
}
|
|
58
|
-
export declare function createTelegramProactiveBeforeAgentStartHook<TContext>(deps: TelegramProactivePromptHookDeps<TContext>): (event: TelegramBeforeAgentStartEvent, ctx: TContext) => Promise<
|
|
61
|
+
export declare function createTelegramProactiveBeforeAgentStartHook<TContext>(deps: TelegramProactivePromptHookDeps<TContext>): (event: TelegramBeforeAgentStartEvent, ctx: TContext) => Promise<TelegramProactiveBeforeAgentStartResult | undefined>;
|
|
59
62
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram prompt injection helpers
|
|
3
3
|
* Zones: pi agent prompts, telegram guidance
|
|
4
|
-
* Owns Telegram
|
|
4
|
+
* Owns stable Telegram guidance, turn-scoped context, and compatibility fallback hooks
|
|
5
5
|
*/
|
|
6
6
|
import { TELEGRAM_PREFIX } from "./turns.js";
|
|
7
7
|
export const TELEGRAM_CONNECTED_CONTEXT_MESSAGE = "Telegram session connected. Use Telegram features for Telegram-originated turns or explicit Telegram requests; connectivity alone is not user intent.";
|
|
@@ -107,8 +107,17 @@ export function createTelegramModelContextAvailabilityRuntime(deps) {
|
|
|
107
107
|
},
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// Returning any systemPrompt makes Pi replace its prompt, which defeats prompt
|
|
111
|
+
// caching; return an override only when the computed prompt actually differs.
|
|
112
|
+
function overrideSystemPromptIfChanged(incoming, computed) {
|
|
113
|
+
const base = incoming ?? "";
|
|
114
|
+
const unchanged = Array.isArray(base) && Array.isArray(computed)
|
|
115
|
+
? base.length === computed.length &&
|
|
116
|
+
base.every((block, index) => block === computed[index])
|
|
117
|
+
: base === computed;
|
|
118
|
+
return unchanged ? undefined : { systemPrompt: computed };
|
|
119
|
+
}
|
|
120
|
+
function buildTelegramTurnSuffix(options) {
|
|
112
121
|
const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
|
|
113
122
|
const telegramHead = telegramPrefix.endsWith("]")
|
|
114
123
|
? telegramPrefix.slice(0, -1)
|
|
@@ -116,9 +125,13 @@ export function buildTelegramBridgeSystemPrompt(options) {
|
|
|
116
125
|
const trimmedPrompt = options.prompt?.trimStart() ?? "";
|
|
117
126
|
const telegramTurn = trimmedPrompt.startsWith(`${telegramHead}]`) ||
|
|
118
127
|
trimmedPrompt.startsWith(`${telegramHead}|`);
|
|
119
|
-
|
|
128
|
+
return telegramTurn
|
|
120
129
|
? `${options.telegramTurnSystemPromptSuffix}\n- The current user message came from Telegram.`
|
|
121
130
|
: "";
|
|
131
|
+
}
|
|
132
|
+
export function buildTelegramBridgeSystemPrompt(options) {
|
|
133
|
+
const basePrompt = options.systemPrompt ?? "";
|
|
134
|
+
const telegramSuffix = buildTelegramTurnSuffix(options);
|
|
122
135
|
return {
|
|
123
136
|
systemPrompt: Array.isArray(basePrompt)
|
|
124
137
|
? [...basePrompt, options.localSystemPromptSuffix + telegramSuffix]
|
|
@@ -148,15 +161,43 @@ function stripTelegramToolMetadataFromSystemPrompt(systemPrompt) {
|
|
|
148
161
|
? systemPrompt.map(stripTelegramToolMetadataFromString)
|
|
149
162
|
: stripTelegramToolMetadataFromString(systemPrompt);
|
|
150
163
|
}
|
|
164
|
+
const TELEGRAM_TOOL_NAMES = ["telegram_attach", "telegram_message"];
|
|
165
|
+
// Sections cannot modify an opaque prompt owned by an earlier handler.
|
|
166
|
+
// Preserve append/strip compatibility there instead of silently losing guidance.
|
|
167
|
+
function applyTelegramPromptSections(event, available) {
|
|
168
|
+
const options = event.systemPromptOptions;
|
|
169
|
+
const sections = options?.sections;
|
|
170
|
+
if (typeof sections !== "object" || sections === null || options?.forceSystemPrompt !== undefined)
|
|
171
|
+
return false;
|
|
172
|
+
if (available) {
|
|
173
|
+
sections.telegram = LOCAL_SYSTEM_PROMPT_SUFFIX.trim();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
delete sections.telegram;
|
|
177
|
+
for (const name of TELEGRAM_TOOL_NAMES) {
|
|
178
|
+
if (options?.toolSnippets)
|
|
179
|
+
delete options.toolSnippets[name];
|
|
180
|
+
if (options?.toolGuidelines)
|
|
181
|
+
delete options.toolGuidelines[name];
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
151
185
|
export function createTelegramProactiveBeforeAgentStartHook(deps) {
|
|
152
186
|
const baseHook = deps.baseHook ?? createTelegramBeforeAgentStartHook();
|
|
153
187
|
return async (event, ctx) => {
|
|
154
188
|
deps.reconcileAvailability?.();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
const available = deps.isAvailable(ctx);
|
|
190
|
+
// A caller-supplied baseHook owns its own prompt text, so it stays on the forced-prompt path.
|
|
191
|
+
if (!deps.baseHook && applyTelegramPromptSections(event, available)) {
|
|
192
|
+
const turnSuffix = available ? buildTelegramTurnSuffix({
|
|
193
|
+
prompt: event.messageType === "custom" ? undefined : event.prompt,
|
|
194
|
+
telegramTurnSystemPromptSuffix: TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX,
|
|
195
|
+
}).trim() : "";
|
|
196
|
+
return turnSuffix ? { message: { customType: "telegram-turn", content: turnSuffix, display: false } } : undefined;
|
|
197
|
+
}
|
|
198
|
+
if (!available) {
|
|
199
|
+
return overrideSystemPromptIfChanged(event.systemPrompt, stripTelegramToolMetadataFromSystemPrompt(event.systemPrompt));
|
|
159
200
|
}
|
|
160
|
-
return baseHook(event);
|
|
201
|
+
return overrideSystemPromptIfChanged(event.systemPrompt, baseHook(event).systemPrompt);
|
|
161
202
|
};
|
|
162
203
|
}
|
|
@@ -174,6 +174,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
174
174
|
getCurrentInstanceId?: () => string | undefined;
|
|
175
175
|
getAdmissionScope?: () => string | undefined;
|
|
176
176
|
getAdmissionJournalBinding?: () => string | undefined;
|
|
177
|
+
getCommandReplyTarget?: Commands.TelegramCommandTargetRuntimeDeps<TContext>["getReplyTarget"];
|
|
177
178
|
getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
|
|
178
179
|
getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
|
|
179
180
|
recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
|
|
@@ -256,7 +257,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
256
257
|
isVoiceReplyActive?: () => boolean;
|
|
257
258
|
updateSettingsMenuMessage?: (state: Menu.TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
|
|
258
259
|
openQueueMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
259
|
-
openSettingsMenu?: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
260
|
+
openSettingsMenu?: (chatId: number, replyToMessageId: number | undefined, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
|
|
260
261
|
settingsMenuCallbackHandler?: (query: TCallbackQuery, ctx: TContext) => Promise<boolean>;
|
|
261
262
|
queueMenuCallbackHandler: (query: TCallbackQuery, ctx: TContext) => Promise<boolean>;
|
|
262
263
|
buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
|
|
@@ -303,7 +304,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
303
304
|
}) => Promise<string | undefined>;
|
|
304
305
|
/** Starts the animated placeholder on an answered guest inline message. */
|
|
305
306
|
startGuestPlaceholder?: (inlineMessageId: string) => void;
|
|
306
|
-
sendTextReply: (chatId: number, replyToMessageId: number, text: string, options?: {
|
|
307
|
+
sendTextReply: (chatId: number, replyToMessageId: number | undefined, text: string, options?: {
|
|
307
308
|
parseMode?: "HTML";
|
|
308
309
|
target?: Queue.TelegramQueueTarget;
|
|
309
310
|
assertAuthority?: TelegramApiCallOptions["assertAuthority"];
|
|
@@ -1052,6 +1052,8 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1052
1052
|
/** A frozen destination pauses All-command expiry until routing finishes or releases it. */
|
|
1053
1053
|
const isPendingRerouteDestinationHeld = (pending) => pending.phase.kind !== "waiting" && pending.phase.kind !== "released";
|
|
1054
1054
|
const pendingUnboundReroutes = new Map();
|
|
1055
|
+
// Body-free, bounded warm deduplication: album members can expire through the same chooser.
|
|
1056
|
+
const expiredRoutingViews = new Set();
|
|
1055
1057
|
const implicitThreadCreations = new Map();
|
|
1056
1058
|
const guidedUnboundTopicKeys = new Set();
|
|
1057
1059
|
let nextUnboundRerouteId = 0;
|
|
@@ -1409,6 +1411,20 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1409
1411
|
return result;
|
|
1410
1412
|
}
|
|
1411
1413
|
catch (error) {
|
|
1414
|
+
const missingTarget = TelegramApi.getTelegramApiErrorRequestTarget(error);
|
|
1415
|
+
if (method === "deleteForumTopic" && missingTarget && error instanceof Error &&
|
|
1416
|
+
TelegramApi.isTelegramApiRequestRejected(error, method) &&
|
|
1417
|
+
TelegramApi.isTelegramStaleTargetHttpError(error) &&
|
|
1418
|
+
ThreadReconciler.isTelegramTopicDeletedErrorMessage(error.message) &&
|
|
1419
|
+
missingTarget?.chatId === temporaryCleanup.target?.chatId &&
|
|
1420
|
+
missingTarget?.threadId === temporaryCleanup.target?.threadId &&
|
|
1421
|
+
missingTarget?.chatId === body.chat_id && missingTarget?.threadId === body.message_thread_id) {
|
|
1422
|
+
// Only the client's parsed exact-target HTTP 400 absence closes the one-shot grant.
|
|
1423
|
+
// Closed/denied/foreign/forged responses remain uncertain and never license another request.
|
|
1424
|
+
assertExecutionCurrent?.();
|
|
1425
|
+
temporaryDeleteConfirmed = true;
|
|
1426
|
+
return true;
|
|
1427
|
+
}
|
|
1412
1428
|
temporaryTransportUncertain = true;
|
|
1413
1429
|
throw error;
|
|
1414
1430
|
}
|
|
@@ -5524,7 +5540,8 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5524
5540
|
return formatTelegramRoutingInputPreview(message);
|
|
5525
5541
|
};
|
|
5526
5542
|
const expireRoutingInput = async (source, ctx, signal) => {
|
|
5527
|
-
const operator = deps.configStore.getAllowedUserId(), epoch = deps.getCurrentLeaderEpoch?.();
|
|
5543
|
+
const operator = deps.configStore.getAllowedUserId(), epoch = deps.getCurrentLeaderEpoch?.(), generation = deps.getSessionGeneration?.(), instanceId = deps.getCurrentInstanceId?.(), scope = deps.getAdmissionScope?.();
|
|
5544
|
+
const sendTextReply = deps.sendTextReply, editInteractiveMessage = deps.editInteractiveMessage;
|
|
5528
5545
|
const message = source.original.update.message;
|
|
5529
5546
|
const runtimeCurrent = () => !signal.aborted &&
|
|
5530
5547
|
operator !== undefined &&
|
|
@@ -5532,6 +5549,9 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5532
5549
|
deps.isContextActive?.(ctx) === true &&
|
|
5533
5550
|
deps.configStore.getAllowedUserId() === operator &&
|
|
5534
5551
|
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
5552
|
+
deps.getSessionGeneration?.() === generation &&
|
|
5553
|
+
deps.getCurrentInstanceId?.() === instanceId &&
|
|
5554
|
+
deps.getAdmissionScope?.() === scope &&
|
|
5535
5555
|
deps.getAdmissionJournalBinding?.() === source.journalBindingKey &&
|
|
5536
5556
|
source.original.routingInput?.operatorUserId === operator &&
|
|
5537
5557
|
message?.from?.id === operator &&
|
|
@@ -5559,27 +5579,73 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5559
5579
|
return;
|
|
5560
5580
|
// Expiry revokes the donor carrier; old controls and late ACKs cannot issue another delivery. No body is archived.
|
|
5561
5581
|
let movedIntoTab = false;
|
|
5582
|
+
let unissued = source.original.routingInput?.phase === "waiting" &&
|
|
5583
|
+
(!temporary || temporary.forwardProtocol === "one-shot-v1") &&
|
|
5584
|
+
![...(temporary?.forwardedInputs ?? []), ...(temporary?.completedInputs ?? [])].some(input => input.journalBindingKey === source.journalBindingKey && input.updateIds.includes(source.original.updateId));
|
|
5585
|
+
const views = new Map();
|
|
5586
|
+
const chooser = source.original.routingInput?.chooser;
|
|
5587
|
+
if (chooser)
|
|
5588
|
+
views.set(`${chooser.chatId}:${chooser.messageId}`, { ...chooser });
|
|
5562
5589
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
5563
5590
|
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId))
|
|
5564
5591
|
continue;
|
|
5565
5592
|
movedIntoTab ||= pending.allTabCopiesForwarded !== undefined;
|
|
5593
|
+
unissued &&= isPendingRerouteUntouched(pending) || pending.phase.kind === "released";
|
|
5594
|
+
if (pending.chooserMessageId !== undefined)
|
|
5595
|
+
views.set(`${pending.sourceTarget.chatId}:${pending.chooserMessageId}`, {
|
|
5596
|
+
...pending.sourceTarget, messageId: pending.chooserMessageId,
|
|
5597
|
+
});
|
|
5566
5598
|
removePendingReroute(id);
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
if (
|
|
5574
|
-
|
|
5599
|
+
}
|
|
5600
|
+
const notice = unissued
|
|
5601
|
+
? "<b>⌛ Routing expired; input was not delivered.</b>"
|
|
5602
|
+
: "<b>⌛ Routing expired; delivery is unconfirmed.</b>\nPreviously accepted work may continue.";
|
|
5603
|
+
const noticeKey = `${source.journalBindingKey}:${source.original.routingInput?.expiresAtMs}:${chooser ? `${chooser.chatId}:${chooser.messageId}` : source.original.updateId}`;
|
|
5604
|
+
if (runtimeCurrent() && !expiredRoutingViews.has(noticeKey)) {
|
|
5605
|
+
if (expiredRoutingViews.size >= 100)
|
|
5606
|
+
expiredRoutingViews.delete(expiredRoutingViews.values().next().value);
|
|
5607
|
+
expiredRoutingViews.add(noticeKey);
|
|
5608
|
+
let visible = false;
|
|
5609
|
+
const assertNoticeAuthority = () => {
|
|
5610
|
+
if (!runtimeCurrent())
|
|
5611
|
+
throw new Error("Routing expiry notice authority changed.");
|
|
5612
|
+
};
|
|
5613
|
+
for (const view of views.values()) {
|
|
5614
|
+
if (!runtimeCurrent() || !editInteractiveMessage || deps.editInteractiveMessage !== editInteractiveMessage)
|
|
5615
|
+
break;
|
|
5616
|
+
if (view.chatId !== message.chat.id || !Number.isSafeInteger(view.threadId) || view.threadId <= 0)
|
|
5617
|
+
continue;
|
|
5575
5618
|
try {
|
|
5576
|
-
await
|
|
5619
|
+
await editInteractiveMessage(view.chatId, view.messageId, notice, "html", { inline_keyboard: [] }, {
|
|
5620
|
+
target: { chatId: view.chatId, threadId: view.threadId }, assertAuthority: assertNoticeAuthority,
|
|
5621
|
+
});
|
|
5622
|
+
visible = true;
|
|
5577
5623
|
}
|
|
5578
5624
|
catch (error) {
|
|
5579
|
-
deps.recordRuntimeEvent?.("routing", error, {
|
|
5580
|
-
|
|
5581
|
-
|
|
5625
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "routing-input-expiry-view" });
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
// A temporary tab is about to disappear; a cold/missing chooser also needs an independent owned destination.
|
|
5629
|
+
const leader = instanceId ? deps.threadStore?.getActiveByInstanceId(instanceId) : undefined;
|
|
5630
|
+
if ((!visible || temporary?.target) && runtimeCurrent() && leader &&
|
|
5631
|
+
leader.target.chatId === message.chat.id && Number.isSafeInteger(leader.target.threadId) && leader.target.threadId > 0) {
|
|
5632
|
+
const target = { chatId: leader.target.chatId, threadId: leader.target.threadId };
|
|
5633
|
+
try {
|
|
5634
|
+
const assertFallback = () => {
|
|
5635
|
+
assertNoticeAuthority();
|
|
5636
|
+
const active = deps.threadStore?.getActiveByInstanceId(instanceId);
|
|
5637
|
+
if (deps.sendTextReply !== sendTextReply || active?.target.chatId !== target.chatId || active.target.threadId !== target.threadId)
|
|
5638
|
+
throw new Error("Routing expiry notice destination changed.");
|
|
5639
|
+
};
|
|
5640
|
+
assertFallback();
|
|
5641
|
+
await sendTextReply(target.chatId, 0, notice, { parseMode: "HTML", target, assertAuthority: assertFallback });
|
|
5582
5642
|
}
|
|
5643
|
+
catch (error) {
|
|
5644
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "routing-input-expiry-notice" });
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
else if (!visible && runtimeCurrent()) {
|
|
5648
|
+
deps.recordRuntimeEvent?.("routing", "Routing expiry notice has no current owned Thread.", { phase: "routing-input-expiry-notice-unavailable" });
|
|
5583
5649
|
}
|
|
5584
5650
|
}
|
|
5585
5651
|
// The scheduled stage captures only context/epoch and a tab token. It reconstructs disposition from body-free proof,
|
|
@@ -5780,7 +5846,8 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5780
5846
|
if (!recordRecoveredRerouteSource(pending, source.original.updateId, source.result))
|
|
5781
5847
|
continue;
|
|
5782
5848
|
try {
|
|
5783
|
-
await retireCancellationChooser(id, pending, isCurrent)
|
|
5849
|
+
if (await retireCancellationChooser(id, pending, isCurrent))
|
|
5850
|
+
await deleteCancelledOrphanTab(pending, pending.abandonment, isCurrent);
|
|
5784
5851
|
}
|
|
5785
5852
|
catch (error) {
|
|
5786
5853
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
@@ -5809,6 +5876,43 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5809
5876
|
}
|
|
5810
5877
|
return true;
|
|
5811
5878
|
};
|
|
5879
|
+
/**
|
|
5880
|
+
* A cancelled input in an unbound (non-temporary) tab leaves nothing to route: once no chooser, queue, live owner,
|
|
5881
|
+
* Workspace/Restore record or retained journal arrival still names the tab, it is deleted like a routing tab. Any
|
|
5882
|
+
* missing proof or failure keeps the tab; this never retries and never blocks the Cancel outcome.
|
|
5883
|
+
*/
|
|
5884
|
+
const deleteCancelledOrphanTab = async (pending, cancellation, isAuthorityCurrent) => {
|
|
5885
|
+
const { chatId, threadId } = pending.sourceTarget;
|
|
5886
|
+
if (threadId === undefined ||
|
|
5887
|
+
pending.temporaryThread ||
|
|
5888
|
+
isTemporaryTabTarget({ chatId, threadId }) ||
|
|
5889
|
+
hasPendingRerouteForTarget({ chatId, threadId }) ||
|
|
5890
|
+
!deps.inspectTemporaryThreadSources)
|
|
5891
|
+
return;
|
|
5892
|
+
const target = { chatId, threadId };
|
|
5893
|
+
try {
|
|
5894
|
+
const own = {
|
|
5895
|
+
journalBindingKey: cancellation.journalBindingKey,
|
|
5896
|
+
updateIds: Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages),
|
|
5897
|
+
};
|
|
5898
|
+
const assertCurrent = () => {
|
|
5899
|
+
if (!isAuthorityCurrent())
|
|
5900
|
+
throw new Error("Unbound tab cleanup authority changed.");
|
|
5901
|
+
};
|
|
5902
|
+
assertCurrent();
|
|
5903
|
+
if (deps.inspectTemporaryThreadSources(target, [cancellation.journalBindingKey], [own])?.length !== 0)
|
|
5904
|
+
return;
|
|
5905
|
+
assertCurrent();
|
|
5906
|
+
await closeReroutedUnboundTopic(target, pending.chooserMessageId, assertCurrent);
|
|
5907
|
+
}
|
|
5908
|
+
catch (error) {
|
|
5909
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
5910
|
+
phase: "unbound-cancel-tab-cleanup",
|
|
5911
|
+
chatId: target.chatId,
|
|
5912
|
+
threadId: target.threadId,
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
};
|
|
5812
5916
|
const handleUnboundRerouteCancelCallback = async (query, ctx) => {
|
|
5813
5917
|
if (!query.data?.startsWith(TELEGRAM_UNBOUND_REROUTE_CANCEL_CALLBACK_PREFIX))
|
|
5814
5918
|
return false;
|
|
@@ -5890,6 +5994,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5890
5994
|
await Promise.all([
|
|
5891
5995
|
deps.answerCallbackQuery(query.id, "Routing cancelled"),
|
|
5892
5996
|
deleteAllTabCopies(pending),
|
|
5997
|
+
deleteCancelledOrphanTab(pending, cancellation, isAuthorityCurrent),
|
|
5893
5998
|
]);
|
|
5894
5999
|
}
|
|
5895
6000
|
catch (error) {
|
|
@@ -7034,6 +7139,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
7034
7139
|
appendControlItem: deps.queueMutationRuntime.append,
|
|
7035
7140
|
getAdmissionScope: deps.getAdmissionScope,
|
|
7036
7141
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
7142
|
+
getReplyTarget: deps.getCommandReplyTarget,
|
|
7037
7143
|
onControlQueued: (message, receipt) => reportQueueAdmission([message], [receipt]),
|
|
7038
7144
|
showStatus: deps.menuActions.sendStatusMessage,
|
|
7039
7145
|
openModelMenu: deps.menuActions.openModelMenu,
|