@llblab/pi-kit 0.36.1 → 0.36.2
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 +5 -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 +17 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -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 +12 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +17 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +42 -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.js +3 -2
- 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.js +119 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +9 -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.d.ts +15 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +22 -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 +9 -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 +16 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -5
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -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 +3 -2
- 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 +137 -24
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +14 -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 +33 -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/package.json +4 -4
|
@@ -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
|
}
|
|
@@ -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) {
|
|
@@ -505,6 +505,17 @@ export declare class TelegramApiStaleTargetError extends Error {
|
|
|
505
505
|
threadId: number;
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
|
+
/** The leader's exact refusal of a follower API call outside that follower's own Thread; carries only the requested target. */
|
|
509
|
+
export declare class TelegramApiFollowerNotAllowedError extends Error {
|
|
510
|
+
readonly requestTarget: {
|
|
511
|
+
chatId: number;
|
|
512
|
+
threadId: number;
|
|
513
|
+
} | undefined;
|
|
514
|
+
constructor(message: string, requestTarget?: {
|
|
515
|
+
chatId: number;
|
|
516
|
+
threadId: number;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
508
519
|
export declare function getTelegramApiErrorRequestTarget(error: unknown): {
|
|
509
520
|
chatId: number;
|
|
510
521
|
threadId: number;
|
|
@@ -338,6 +338,15 @@ export class TelegramApiStaleTargetError extends Error {
|
|
|
338
338
|
this.requestTarget = { ...requestTarget };
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
|
+
/** The leader's exact refusal of a follower API call outside that follower's own Thread; carries only the requested target. */
|
|
342
|
+
export class TelegramApiFollowerNotAllowedError extends Error {
|
|
343
|
+
requestTarget;
|
|
344
|
+
constructor(message, requestTarget) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = "TelegramApiFollowerNotAllowedError";
|
|
347
|
+
this.requestTarget = requestTarget ? { ...requestTarget } : undefined;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
341
350
|
export function getTelegramApiErrorRequestTarget(error) {
|
|
342
351
|
const target = error instanceof TelegramApiHttpError ||
|
|
343
352
|
error instanceof TelegramApiStaleTargetError
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Zones: telegram threads, workspace lifecycle
|
|
4
4
|
* Owns fail-closed cleanup eligibility projection without persistence or Bot API effects
|
|
5
5
|
*/
|
|
6
|
-
import type { TelegramWorkspaceDeletionPermit, TelegramWorkspaceDestructiveFence, TelegramWorkspaceRetirementFence } from "./workspace-admission.ts";
|
|
6
|
+
import type { TelegramWorkspaceDeletionPermit, TelegramWorkspaceDestructiveFence, TelegramWorkspaceRetirementFence, TelegramWorkspaceAdmissionLedger, TelegramWorkspaceAdmissionOwner } from "./workspace-admission.ts";
|
|
7
|
+
import type { TelegramTopicTargetStore, TelegramWorkspaceThreadBinding } from "./threads.ts";
|
|
8
|
+
import type { TelegramWorkspaceOperationRunner } from "./workspace-retirement.ts";
|
|
9
|
+
import type { TelegramWorkspaceThreadDeletionTransport } from "./telegram-api.ts";
|
|
10
|
+
import type { TelegramProcessLiveness } from "./process-identity.ts";
|
|
7
11
|
export interface TelegramThreadCleanupBindingSnapshot {
|
|
8
12
|
cwd: string;
|
|
9
13
|
workspaceKey: string;
|
|
@@ -38,7 +42,8 @@ export interface TelegramThreadCleanupCandidate {
|
|
|
38
42
|
sessionId?: string;
|
|
39
43
|
sessionKey?: string;
|
|
40
44
|
instanceSlot: string;
|
|
41
|
-
|
|
45
|
+
/** Null is explicit absence in reviewed work; it never denotes a reusable letter. */
|
|
46
|
+
slot: string | null;
|
|
42
47
|
target: {
|
|
43
48
|
chatId: number;
|
|
44
49
|
threadId: number;
|
|
@@ -155,6 +160,8 @@ export declare function createTelegramThreadCleanupWorkStore(options: {
|
|
|
155
160
|
runtimeDir?: string;
|
|
156
161
|
profileName: string;
|
|
157
162
|
tokenSha256: string;
|
|
163
|
+
/** Exact captured ledger identity; standalone fixtures may use the plain profile identity. */
|
|
164
|
+
admissionProfileKey?: string;
|
|
158
165
|
maxWorkSets?: number;
|
|
159
166
|
maxBytes?: number;
|
|
160
167
|
getNowMs?: () => number;
|
|
@@ -185,6 +192,7 @@ export declare function executeTelegramInactiveThreadCleanup(input: {
|
|
|
185
192
|
}>;
|
|
186
193
|
export declare function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
187
194
|
ledger: {
|
|
195
|
+
getProfileKey(): string;
|
|
188
196
|
read(): {
|
|
189
197
|
fence?: TelegramWorkspaceDestructiveFence;
|
|
190
198
|
};
|
|
@@ -192,7 +200,7 @@ export declare function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
192
200
|
operationId: string;
|
|
193
201
|
cleanupWorkSetId: string;
|
|
194
202
|
bindingKey: string;
|
|
195
|
-
slot: string;
|
|
203
|
+
slot: string | null;
|
|
196
204
|
target: {
|
|
197
205
|
chatId: number;
|
|
198
206
|
threadId: number;
|
|
@@ -256,6 +264,34 @@ export interface TelegramInactiveThreadCleanupCoordinatorDeps<TBinding> {
|
|
|
256
264
|
deleteWithPermit(permit: TelegramThreadCleanupDeletionPermit, candidate: TelegramThreadCleanupCandidate): Promise<void>;
|
|
257
265
|
commitBinding(candidate: TelegramThreadCleanupCandidate, currentBinding?: TBinding): Promise<boolean>;
|
|
258
266
|
}
|
|
267
|
+
export interface TelegramInactiveThreadCleanupAuthority {
|
|
268
|
+
profileName: string;
|
|
269
|
+
leaderEpoch: number | string;
|
|
270
|
+
ledger: TelegramWorkspaceAdmissionLedger;
|
|
271
|
+
isCurrent(): boolean;
|
|
272
|
+
}
|
|
273
|
+
/** Review owns ordinary admission; confirmed cleanup uses the shared gate and its exclusive manual fence instead. */
|
|
274
|
+
export declare function createTelegramInactiveThreadCleanupRuntime(deps: {
|
|
275
|
+
store: Pick<TelegramTopicTargetStore, "load" | "listWorkspaceBindings" | "listReservations" | "listPendingProvisions" | "listPendingCleanups" | "commitInactiveWorkspaceCleanup">;
|
|
276
|
+
captureAuthority(): TelegramInactiveThreadCleanupAuthority | undefined;
|
|
277
|
+
getWorkStore(authority: TelegramInactiveThreadCleanupAuthority): TelegramThreadCleanupWorkStore;
|
|
278
|
+
getProtection(binding: TelegramWorkspaceThreadBinding): {
|
|
279
|
+
liveOwner: TelegramThreadCleanupProtectionState;
|
|
280
|
+
acceptedWork: TelegramThreadCleanupProtectionState;
|
|
281
|
+
deliveryAuthority: TelegramThreadCleanupProtectionState;
|
|
282
|
+
};
|
|
283
|
+
getProcessLiveness(owner: TelegramWorkspaceAdmissionOwner): TelegramProcessLiveness;
|
|
284
|
+
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
285
|
+
runWorkspaceOperation: TelegramWorkspaceOperationRunner;
|
|
286
|
+
deleteThread: TelegramWorkspaceThreadDeletionTransport;
|
|
287
|
+
onError?: (error: unknown) => void;
|
|
288
|
+
}): {
|
|
289
|
+
review(): Promise<{
|
|
290
|
+
count: number;
|
|
291
|
+
operationId?: string;
|
|
292
|
+
}>;
|
|
293
|
+
clean(operationId: string): Promise<TelegramInactiveThreadCleanupResult>;
|
|
294
|
+
};
|
|
259
295
|
export declare function createTelegramInactiveThreadCleanupSettingsPort<TBinding>(deps: TelegramInactiveThreadCleanupCoordinatorDeps<TBinding>): (operationId: string) => Promise<TelegramInactiveThreadCleanupResult>;
|
|
260
296
|
export interface TelegramInactiveThreadCleanupResult {
|
|
261
297
|
deleted: number;
|