@llblab/pi-kit 0.3.1 → 0.4.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/AGENTS.md +3 -3
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +19 -10
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
- package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-clean-room/README.md +61 -0
- package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
- package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
- package/node_modules/@llblab/pi-clean-room/package.json +53 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
- package/node_modules/@llblab/pi-state-flow/README.md +197 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/package.json +55 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +2 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +16 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
- package/node_modules/@llblab/pi-telegram/index.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +34 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
- package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
- package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
- package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
- package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
- package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
- package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
- package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
- package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
- package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
- package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
- package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
- package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
- package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
- package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
- package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
- package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
- package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
- package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/package.json +46 -0
- package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
- package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
- package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
- package/package.json +20 -8
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
|
@@ -135,6 +135,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
135
135
|
export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
136
136
|
topicTargetStore: Pick<
|
|
137
137
|
Threads.TelegramTopicTargetStore,
|
|
138
|
+
| "list"
|
|
138
139
|
| "markStaleByTarget"
|
|
139
140
|
| "persist"
|
|
140
141
|
| "upsertPendingCleanup"
|
|
@@ -726,6 +727,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
726
727
|
],
|
|
727
728
|
}),
|
|
728
729
|
{
|
|
730
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
729
731
|
callApi: deps.callApi,
|
|
730
732
|
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
731
733
|
return deps.topicTargetStore.markStaleByTarget(
|
|
@@ -805,16 +807,21 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
805
807
|
target: { chatId: target.chatId, threadId: target.threadId },
|
|
806
808
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
807
809
|
};
|
|
810
|
+
const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
|
|
811
|
+
record.target.chatId === target.chatId && record.target.threadId === target.threadId);
|
|
812
|
+
const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
808
813
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
809
814
|
await deps.topicTargetStore.persist();
|
|
815
|
+
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
816
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
817
|
+
currentLeaderEpoch: leaderEpoch,
|
|
818
|
+
records: [],
|
|
819
|
+
pendingCleanups: [intent],
|
|
820
|
+
});
|
|
810
821
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
811
|
-
|
|
812
|
-
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
813
|
-
currentLeaderEpoch: leaderEpoch,
|
|
814
|
-
records: [],
|
|
815
|
-
pendingCleanups: [intent],
|
|
816
|
-
}),
|
|
822
|
+
cleanupPlan,
|
|
817
823
|
{
|
|
824
|
+
isCleanupTargetProtected,
|
|
818
825
|
callApi: deps.callApi,
|
|
819
826
|
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
820
827
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
@@ -823,6 +830,7 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
823
830
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
824
831
|
},
|
|
825
832
|
);
|
|
833
|
+
if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) return;
|
|
826
834
|
if (cleanup.incompleteActions?.length) {
|
|
827
835
|
throw new Error(
|
|
828
836
|
"Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.",
|
|
@@ -916,6 +924,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
916
924
|
});
|
|
917
925
|
deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
|
|
918
926
|
const cleanupPorts = {
|
|
927
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
919
928
|
callApi: deps.callApi,
|
|
920
929
|
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
921
930
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
@@ -1450,18 +1450,25 @@ export function createTelegramBusFollowerThreadRestoreHandler(
|
|
|
1450
1450
|
return async ({ record, target, oldTarget }) => {
|
|
1451
1451
|
if (!record.instanceId) return false;
|
|
1452
1452
|
const follower = deps.followerRegistry.get(record.instanceId);
|
|
1453
|
-
if (!follower
|
|
1453
|
+
if (!follower?.registrationGeneration || !oldTarget ||
|
|
1454
|
+
follower.target?.chatId !== oldTarget.chatId ||
|
|
1455
|
+
follower.target.threadId !== oldTarget.threadId ||
|
|
1456
|
+
target.chatId !== oldTarget.chatId || target.threadId === oldTarget.threadId) return false;
|
|
1454
1457
|
const replaced = await deps.followerTargetController.replaceTarget({
|
|
1455
1458
|
follower,
|
|
1456
1459
|
target,
|
|
1457
1460
|
oldTarget,
|
|
1458
1461
|
reason: "thread-restore",
|
|
1459
1462
|
});
|
|
1460
|
-
|
|
1463
|
+
const current = deps.followerRegistry.get(record.instanceId);
|
|
1464
|
+
if (!replaced || !current ||
|
|
1465
|
+
current.registrationGeneration !== follower.registrationGeneration ||
|
|
1466
|
+
current.target?.chatId !== oldTarget.chatId ||
|
|
1467
|
+
current.target.threadId !== oldTarget.threadId) return false;
|
|
1461
1468
|
deps.followerRegistry.register({
|
|
1462
|
-
...
|
|
1469
|
+
...current,
|
|
1463
1470
|
target,
|
|
1464
|
-
connectedAtMs:
|
|
1471
|
+
connectedAtMs: current.connectedAtMs,
|
|
1465
1472
|
});
|
|
1466
1473
|
deps.onRestored?.();
|
|
1467
1474
|
return true;
|
|
@@ -556,8 +556,40 @@ export function normalizeMarkdownAfterVoiceExtraction(
|
|
|
556
556
|
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
function isTelegramCommentOnlyLinePrefix(value: string): boolean {
|
|
560
|
+
return /^[ \t]*(?:(?:>[ \t]*)+)?(?:(?:[-+*]|\d+[.)])[ \t]+)?$/u.test(
|
|
561
|
+
value,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function stripTelegramHtmlCommentBlocks(markdown: string): string {
|
|
566
|
+
let result = "";
|
|
567
|
+
let offset = 0;
|
|
568
|
+
while (offset < markdown.length) {
|
|
569
|
+
const start = markdown.indexOf("<!--", offset);
|
|
570
|
+
if (start === -1) return result + markdown.slice(offset);
|
|
571
|
+
const close = markdown.indexOf("-->", start + 4);
|
|
572
|
+
const lineStart = markdown.lastIndexOf("\n", start - 1) + 1;
|
|
573
|
+
const afterComment = close === -1 ? markdown.length : close + 3;
|
|
574
|
+
const newlineAfterComment = markdown.indexOf("\n", afterComment);
|
|
575
|
+
const lineEnd =
|
|
576
|
+
newlineAfterComment === -1 ? markdown.length : newlineAfterComment;
|
|
577
|
+
const commentOwnsLine =
|
|
578
|
+
isTelegramCommentOnlyLinePrefix(markdown.slice(lineStart, start)) &&
|
|
579
|
+
markdown.slice(afterComment, lineEnd).trim().length === 0;
|
|
580
|
+
result += markdown.slice(offset, commentOwnsLine ? lineStart : start);
|
|
581
|
+
if (close === -1) return result;
|
|
582
|
+
offset = commentOwnsLine
|
|
583
|
+
? newlineAfterComment === -1
|
|
584
|
+
? markdown.length
|
|
585
|
+
: newlineAfterComment + 1
|
|
586
|
+
: afterComment;
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
559
591
|
export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
560
|
-
const withoutClosedBlocks =
|
|
592
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
561
593
|
const openBlockIndex =
|
|
562
594
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
563
595
|
const previewMarkdown =
|
|
@@ -570,7 +602,7 @@ export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
|
570
602
|
export function stripTelegramCommentMarkupForDelivery(
|
|
571
603
|
markdown: string,
|
|
572
604
|
): string {
|
|
573
|
-
const withoutClosedBlocks =
|
|
605
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
574
606
|
const openBlockIndex =
|
|
575
607
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
576
608
|
const deliveryMarkdown =
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "./outbound-buttons.ts";
|
|
27
27
|
import {
|
|
28
28
|
planTelegramVoiceReply,
|
|
29
|
+
stripTelegramCommentMarkupForDelivery,
|
|
29
30
|
type TelegramVoiceReplyItem,
|
|
30
31
|
} from "./outbound-markup.ts";
|
|
31
32
|
import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.ts";
|
|
@@ -698,7 +699,9 @@ export function createTelegramOutboundTextReplyRuntime<TReplyMarkup = unknown>(
|
|
|
698
699
|
return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
|
|
699
700
|
},
|
|
700
701
|
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
701
|
-
const
|
|
702
|
+
const deliveryMarkdown = stripTelegramCommentMarkupForDelivery(markdown);
|
|
703
|
+
if (!deliveryMarkdown) return undefined;
|
|
704
|
+
const transformed = await transformTelegramOutboundTextReply(deliveryMarkdown, {
|
|
702
705
|
handlers: deps.getHandlers?.(),
|
|
703
706
|
cwd: deps.cwd,
|
|
704
707
|
execCommand: deps.execCommand,
|
|
@@ -281,19 +281,17 @@ function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
|
281
281
|
"",
|
|
282
282
|
`You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.`,
|
|
283
283
|
"Select the Pi thread that should handle it:",
|
|
284
|
+
"To restore into a new thread, send a plain message in that destination thread first.",
|
|
284
285
|
].join("\n");
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
function buildTelegramUnboundRerouteChooserMarkup(
|
|
288
289
|
rerouteId: string,
|
|
289
290
|
records: readonly Threads.TelegramTopicTargetRecord[],
|
|
290
|
-
|
|
291
|
-
currentLeaderProfileKey?: string;
|
|
292
|
-
currentInstanceId?: string;
|
|
293
|
-
} = {},
|
|
291
|
+
options: { canRestore: boolean },
|
|
294
292
|
): Menu.TelegramReplyMarkup {
|
|
295
293
|
const activeRecords = records.filter((record) => record.status === "active");
|
|
296
|
-
const canRestoreAnyLiveThread = activeRecords.length > 0;
|
|
294
|
+
const canRestoreAnyLiveThread = options.canRestore && activeRecords.length > 0;
|
|
297
295
|
const rows = activeRecords.map((record) => [
|
|
298
296
|
{
|
|
299
297
|
text: getTelegramRouteThreadButtonLabel(record),
|
|
@@ -433,6 +431,7 @@ async function deleteReservedTelegramTopicThroughReconciler(
|
|
|
433
431
|
});
|
|
434
432
|
deps.recordThreadReconciliationPlan?.(plan);
|
|
435
433
|
await ThreadReconciler.applyThreadReconciliationPlan(plan, {
|
|
434
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.threadStore),
|
|
436
435
|
callApi: deps.callApi,
|
|
437
436
|
markStaleByTarget: (staleTarget, syncStatus, lastSyncError) =>
|
|
438
437
|
deps.threadStore?.markStaleByTarget(
|
|
@@ -734,6 +733,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
734
733
|
instanceId?: string;
|
|
735
734
|
};
|
|
736
735
|
type PendingUnboundReroute = {
|
|
736
|
+
sourceTarget: Queue.TelegramQueueTarget;
|
|
737
|
+
chooserMessageId?: number;
|
|
737
738
|
messages: TMessage[];
|
|
738
739
|
createdAtMs: number;
|
|
739
740
|
dispatchKind: "prompt" | "command";
|
|
@@ -842,12 +843,33 @@ export function createTelegramInboundRouteRuntime<
|
|
|
842
843
|
nextUnboundRerouteId += 1;
|
|
843
844
|
const id = nextUnboundRerouteId.toString(36);
|
|
844
845
|
pendingUnboundReroutes.set(id, {
|
|
846
|
+
sourceTarget: {
|
|
847
|
+
chatId: messages[0]!.chat.id,
|
|
848
|
+
...(typeof messages[0]!.message_thread_id === "number"
|
|
849
|
+
? { threadId: messages[0]!.message_thread_id }
|
|
850
|
+
: {}),
|
|
851
|
+
},
|
|
845
852
|
messages,
|
|
846
853
|
createdAtMs: Date.now(),
|
|
847
854
|
dispatchKind,
|
|
848
855
|
});
|
|
849
856
|
return id;
|
|
850
857
|
};
|
|
858
|
+
const rememberRerouteChooser = (id: string, messageId: number | undefined): void => {
|
|
859
|
+
const pending = pendingUnboundReroutes.get(id);
|
|
860
|
+
if (pending) pending.chooserMessageId = messageId;
|
|
861
|
+
};
|
|
862
|
+
const matchesRerouteChooser = (
|
|
863
|
+
pending: PendingUnboundReroute,
|
|
864
|
+
query: TCallbackQuery,
|
|
865
|
+
): boolean => {
|
|
866
|
+
const message = query.message;
|
|
867
|
+
return !!message && pending.chooserMessageId !== undefined &&
|
|
868
|
+
message.message_id === pending.chooserMessageId &&
|
|
869
|
+
message.chat.id === pending.sourceTarget.chatId &&
|
|
870
|
+
(message.message_thread_id === undefined ||
|
|
871
|
+
message.message_thread_id === pending.sourceTarget.threadId);
|
|
872
|
+
};
|
|
851
873
|
const pendingUnboundRerouteMediaGroups = new Map<
|
|
852
874
|
string,
|
|
853
875
|
{
|
|
@@ -951,6 +973,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
951
973
|
assertExecutionCurrent?.();
|
|
952
974
|
deps.recordThreadReconciliationPlan?.(plan);
|
|
953
975
|
const result = await ThreadReconciler.applyThreadReconciliationPlan(plan, {
|
|
976
|
+
isCleanupTargetProtected(target) {
|
|
977
|
+
assertExecutionCurrent?.();
|
|
978
|
+
return isRerouteTargetProtected(target);
|
|
979
|
+
},
|
|
954
980
|
callApi: deps.callApi,
|
|
955
981
|
markStaleByTarget: (staleTarget, syncStatus, lastSyncError) =>
|
|
956
982
|
deps.threadStore?.markStaleByTarget(
|
|
@@ -967,6 +993,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
967
993
|
assertExecutionCurrent?.();
|
|
968
994
|
return (result.incompleteActions?.length ?? 0) === 0;
|
|
969
995
|
};
|
|
996
|
+
const isRerouteTargetProtected = (target: Queue.TelegramQueueTarget): boolean => {
|
|
997
|
+
const matches = (candidate: Queue.TelegramQueueTarget): boolean =>
|
|
998
|
+
candidate.chatId === target.chatId && candidate.threadId === target.threadId;
|
|
999
|
+
return (deps.getLiveThreadTargets?.() ?? []).some(matches) ||
|
|
1000
|
+
(deps.threadStore?.list() ?? []).some((record) => matches(record.target)) ||
|
|
1001
|
+
(deps.threadStore?.listReservations() ?? []).some((record) => matches(record.target)) ||
|
|
1002
|
+
(deps.threadStore?.listPendingProvisions() ?? []).some((record) => !!record.target && matches(record.target));
|
|
1003
|
+
};
|
|
970
1004
|
const dismissRerouteChooserMessage = async (
|
|
971
1005
|
query: TCallbackQuery,
|
|
972
1006
|
assertExecutionCurrent?: () => void,
|
|
@@ -986,6 +1020,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
986
1020
|
assertExecutionCurrent?.();
|
|
987
1021
|
return true;
|
|
988
1022
|
} catch (error) {
|
|
1023
|
+
assertExecutionCurrent?.();
|
|
989
1024
|
deps.recordRuntimeEvent?.("telegram", error, {
|
|
990
1025
|
phase: "reroute-chooser-delete",
|
|
991
1026
|
chatId,
|
|
@@ -1103,12 +1138,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1103
1138
|
pending: PendingUnboundReroute,
|
|
1104
1139
|
query: TCallbackQuery,
|
|
1105
1140
|
successMessage: string,
|
|
1106
|
-
assertExecutionCurrent
|
|
1141
|
+
assertExecutionCurrent = Updates.createTelegramUpdateExecutionFenceGuard(query),
|
|
1107
1142
|
): Promise<void> => {
|
|
1143
|
+
assertExecutionCurrent();
|
|
1108
1144
|
const dismissed = await dismissRerouteChooserMessage(
|
|
1109
1145
|
query,
|
|
1110
1146
|
assertExecutionCurrent,
|
|
1111
1147
|
);
|
|
1148
|
+
assertExecutionCurrent();
|
|
1112
1149
|
if (dismissed) {
|
|
1113
1150
|
pendingUnboundReroutes.delete(rerouteId);
|
|
1114
1151
|
await deps.answerCallbackQuery(query.id, successMessage);
|
|
@@ -1169,6 +1206,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1169
1206
|
query.data,
|
|
1170
1207
|
);
|
|
1171
1208
|
if (!parsed) return false;
|
|
1209
|
+
const assertExecutionCurrent = Updates.createTelegramUpdateExecutionFenceGuard(query);
|
|
1210
|
+
assertExecutionCurrent();
|
|
1172
1211
|
const chatId = query.message?.chat?.id;
|
|
1173
1212
|
const messageId = query.message?.message_id;
|
|
1174
1213
|
const pending = pendingUnboundReroutes.get(parsed.rerouteId);
|
|
@@ -1176,12 +1215,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1176
1215
|
typeof chatId !== "number" ||
|
|
1177
1216
|
typeof messageId !== "number" ||
|
|
1178
1217
|
!deps.threadStore ||
|
|
1179
|
-
!pending
|
|
1218
|
+
!pending ||
|
|
1219
|
+
!matchesRerouteChooser(pending, query)
|
|
1180
1220
|
) {
|
|
1181
1221
|
await deps.answerCallbackQuery(query.id, "Message route expired.");
|
|
1182
1222
|
return true;
|
|
1183
1223
|
}
|
|
1224
|
+
if (pending.sourceTarget.threadId === undefined) {
|
|
1225
|
+
await deps.answerCallbackQuery(query.id, "Restore needs a destination thread. Send a plain message in a new Telegram thread first.");
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1184
1228
|
await deps.threadStore.load();
|
|
1229
|
+
assertExecutionCurrent();
|
|
1185
1230
|
const activeRecords = getTelegramRoutableThreadRecords(
|
|
1186
1231
|
deps.threadStore.list(),
|
|
1187
1232
|
deps.getLiveThreadTargets?.(),
|
|
@@ -1199,19 +1244,17 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1199
1244
|
replyMarkup,
|
|
1200
1245
|
);
|
|
1201
1246
|
} else if (deps.sendInteractiveMessage) {
|
|
1202
|
-
await deps.sendInteractiveMessage(
|
|
1247
|
+
const chooserId = await deps.sendInteractiveMessage(
|
|
1203
1248
|
chatId,
|
|
1204
1249
|
formatTelegramUnboundRerouteRestoreChooserText(),
|
|
1205
1250
|
"html",
|
|
1206
1251
|
replyMarkup,
|
|
1207
|
-
|
|
1208
|
-
? {
|
|
1209
|
-
target: { chatId, threadId: query.message.message_thread_id },
|
|
1210
|
-
replyToMessageId: messageId,
|
|
1211
|
-
}
|
|
1212
|
-
: undefined,
|
|
1252
|
+
{ target: pending.sourceTarget, replyToMessageId: messageId },
|
|
1213
1253
|
);
|
|
1254
|
+
assertExecutionCurrent();
|
|
1255
|
+
rememberRerouteChooser(parsed.rerouteId, chooserId);
|
|
1214
1256
|
}
|
|
1257
|
+
assertExecutionCurrent();
|
|
1215
1258
|
await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
|
|
1216
1259
|
return true;
|
|
1217
1260
|
};
|
|
@@ -1226,7 +1269,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1226
1269
|
assertExecutionCurrent();
|
|
1227
1270
|
const chatId = query.message?.chat?.id;
|
|
1228
1271
|
const pending = pendingUnboundReroutes.get(parsed.rerouteId);
|
|
1229
|
-
if (typeof chatId !== "number" || !deps.threadStore || !pending
|
|
1272
|
+
if (typeof chatId !== "number" || !deps.threadStore || !pending ||
|
|
1273
|
+
!matchesRerouteChooser(pending, query)) {
|
|
1230
1274
|
await deps.answerCallbackQuery(query.id, "Message route expired.");
|
|
1231
1275
|
return true;
|
|
1232
1276
|
}
|
|
@@ -1300,10 +1344,23 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1300
1344
|
parsed.threadId,
|
|
1301
1345
|
);
|
|
1302
1346
|
const sourceTarget =
|
|
1303
|
-
typeof
|
|
1304
|
-
? { chatId, threadId:
|
|
1347
|
+
typeof pending.sourceTarget.threadId === "number"
|
|
1348
|
+
? { chatId, threadId: pending.sourceTarget.threadId }
|
|
1305
1349
|
: undefined;
|
|
1306
|
-
const sourceMessageId =
|
|
1350
|
+
const sourceMessageId = pending.chooserMessageId;
|
|
1351
|
+
if (parsed.useNewSlot && !sourceTarget) {
|
|
1352
|
+
await deps.answerCallbackQuery(query.id, "Restore needs a destination thread. Send a plain message in a new Telegram thread first.");
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
if (parsed.useNewSlot && sourceTarget && record.target.chatId === sourceTarget.chatId &&
|
|
1356
|
+
record.target.threadId === sourceTarget.threadId) {
|
|
1357
|
+
await deps.answerCallbackQuery(query.id, "🚫 Selected thread is already the destination.");
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
if (parsed.useNewSlot && sourceTarget && isRerouteTargetProtected(sourceTarget)) {
|
|
1361
|
+
await deps.answerCallbackQuery(query.id, "🚫 Thread restore source is already owned.");
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1307
1364
|
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1308
1365
|
const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
|
|
1309
1366
|
const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
|
|
@@ -1507,6 +1564,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1507
1564
|
rerouteConfirmedAtMs: nowMs,
|
|
1508
1565
|
});
|
|
1509
1566
|
await deps.threadStore.persist();
|
|
1567
|
+
assertExecutionCurrent();
|
|
1510
1568
|
deps.setCurrentLeaderIdentity?.({
|
|
1511
1569
|
target: sourceTarget,
|
|
1512
1570
|
slot,
|
|
@@ -1944,7 +2002,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1944
2002
|
};
|
|
1945
2003
|
const sendUnboundRerouteChooserNow = async (
|
|
1946
2004
|
messages: TMessage[],
|
|
1947
|
-
|
|
2005
|
+
_ctx: TContext,
|
|
1948
2006
|
reportDeferred = true,
|
|
1949
2007
|
): Promise<void> => {
|
|
1950
2008
|
const message = messages[0];
|
|
@@ -1988,20 +2046,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1988
2046
|
const text = formatTelegramUnboundRerouteChooserText(activeRecords, {
|
|
1989
2047
|
includeGuidance,
|
|
1990
2048
|
});
|
|
1991
|
-
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1992
2049
|
const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
|
|
1993
2050
|
rerouteId,
|
|
1994
2051
|
activeRecords,
|
|
1995
|
-
{
|
|
1996
|
-
currentLeaderProfileKey: getLeaderTopicProfileKey(
|
|
1997
|
-
ctx,
|
|
1998
|
-
currentInstanceId,
|
|
1999
|
-
),
|
|
2000
|
-
currentInstanceId,
|
|
2001
|
-
},
|
|
2052
|
+
{ canRestore: sourceTarget !== undefined },
|
|
2002
2053
|
);
|
|
2003
2054
|
if (deps.sendInteractiveMessage) {
|
|
2004
|
-
await deps.sendInteractiveMessage(
|
|
2055
|
+
const chooserId = await deps.sendInteractiveMessage(
|
|
2005
2056
|
message.chat.id,
|
|
2006
2057
|
text,
|
|
2007
2058
|
"html",
|
|
@@ -2010,12 +2061,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2010
2061
|
? { target: sourceTarget, replyToMessageId: message.message_id }
|
|
2011
2062
|
: { replyToMessageId: message.message_id },
|
|
2012
2063
|
);
|
|
2064
|
+
rememberRerouteChooser(rerouteId, chooserId);
|
|
2013
2065
|
return;
|
|
2014
2066
|
}
|
|
2015
|
-
await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2067
|
+
const chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2016
2068
|
parseMode: "HTML",
|
|
2017
2069
|
target: sourceTarget,
|
|
2018
2070
|
});
|
|
2071
|
+
rememberRerouteChooser(rerouteId, chooserId);
|
|
2019
2072
|
};
|
|
2020
2073
|
const sendUnboundRerouteChooser = async (
|
|
2021
2074
|
message: TMessage,
|
|
@@ -2080,9 +2133,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2080
2133
|
const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
|
|
2081
2134
|
rerouteId,
|
|
2082
2135
|
activeRecords,
|
|
2136
|
+
{ canRestore: typeof message.message_thread_id === "number" },
|
|
2083
2137
|
);
|
|
2084
2138
|
if (deps.sendInteractiveMessage) {
|
|
2085
|
-
await deps.sendInteractiveMessage(
|
|
2139
|
+
const chooserId = await deps.sendInteractiveMessage(
|
|
2086
2140
|
message.chat.id,
|
|
2087
2141
|
text,
|
|
2088
2142
|
"html",
|
|
@@ -2096,10 +2150,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2096
2150
|
}
|
|
2097
2151
|
: undefined,
|
|
2098
2152
|
);
|
|
2153
|
+
rememberRerouteChooser(rerouteId, chooserId);
|
|
2099
2154
|
return true;
|
|
2100
2155
|
}
|
|
2101
2156
|
if (deps.callApi) {
|
|
2102
|
-
await deps.callApi("sendMessage", {
|
|
2157
|
+
const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
|
|
2103
2158
|
chat_id: message.chat.id,
|
|
2104
2159
|
text,
|
|
2105
2160
|
parse_mode: "HTML",
|
|
@@ -2116,12 +2171,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2116
2171
|
}
|
|
2117
2172
|
: {}),
|
|
2118
2173
|
});
|
|
2174
|
+
rememberRerouteChooser(rerouteId, chooser?.message_id);
|
|
2119
2175
|
return true;
|
|
2120
2176
|
}
|
|
2121
|
-
await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2177
|
+
const chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2122
2178
|
parseMode: "HTML",
|
|
2123
2179
|
target: options.target,
|
|
2124
2180
|
});
|
|
2181
|
+
rememberRerouteChooser(rerouteId, chooserId);
|
|
2125
2182
|
return true;
|
|
2126
2183
|
};
|
|
2127
2184
|
const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { getTelegramApiErrorRequestTarget } from "./telegram-api.ts";
|
|
7
|
+
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.ts";
|
|
8
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
10
|
import {
|
|
11
|
+
createTelegramCleanupTargetProtection,
|
|
11
12
|
getTelegramTargetFromApiBody,
|
|
12
13
|
isTelegramTopicTargetStaleError,
|
|
13
14
|
provisionOwnBusTopic,
|
|
@@ -121,6 +122,7 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
121
122
|
| undefined;
|
|
122
123
|
topicTargetStore: Pick<
|
|
123
124
|
TelegramTopicTargetStore,
|
|
125
|
+
| "list"
|
|
124
126
|
| "markStaleByTarget"
|
|
125
127
|
| "persist"
|
|
126
128
|
| "upsertPendingCleanup"
|
|
@@ -247,16 +249,21 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
247
249
|
target,
|
|
248
250
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
249
251
|
};
|
|
252
|
+
const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === currentRecord.instanceId &&
|
|
253
|
+
record.target.chatId === target.chatId && record.target.threadId === target.threadId);
|
|
254
|
+
const isCleanupTargetProtected = createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
250
255
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
251
256
|
await deps.topicTargetStore.persist();
|
|
257
|
+
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
258
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
259
|
+
currentLeaderEpoch: leaderEpoch,
|
|
260
|
+
records: [],
|
|
261
|
+
pendingCleanups: [intent],
|
|
262
|
+
});
|
|
252
263
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
253
|
-
|
|
254
|
-
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
255
|
-
currentLeaderEpoch: leaderEpoch,
|
|
256
|
-
records: [],
|
|
257
|
-
pendingCleanups: [intent],
|
|
258
|
-
}),
|
|
264
|
+
cleanupPlan,
|
|
259
265
|
{
|
|
266
|
+
isCleanupTargetProtected,
|
|
260
267
|
callApi(method, body) {
|
|
261
268
|
return deps.callApi(method, body);
|
|
262
269
|
},
|
|
@@ -277,6 +284,9 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
277
284
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
278
285
|
},
|
|
279
286
|
);
|
|
287
|
+
if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) {
|
|
288
|
+
return "Thread disconnect superseded by a new binding.";
|
|
289
|
+
}
|
|
280
290
|
cleanupPending = Boolean(cleanup.incompleteActions?.length);
|
|
281
291
|
}
|
|
282
292
|
const leaderTarget = deps.getLeaderTarget();
|
|
@@ -404,7 +414,7 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
404
414
|
topicTargetStore: Pick<
|
|
405
415
|
TelegramTopicTargetStore,
|
|
406
416
|
"load" | "markStaleByTarget" | "persist"
|
|
407
|
-
|
|
417
|
+
> & Partial<Pick<TelegramTopicTargetStore, "invalidateTarget">>;
|
|
408
418
|
getSyncState: () => TSyncState;
|
|
409
419
|
setSyncState: (state: TSyncState) => void;
|
|
410
420
|
recordEvent: (
|
|
@@ -413,6 +423,46 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
413
423
|
details?: Record<string, unknown>,
|
|
414
424
|
) => void;
|
|
415
425
|
getNowMs?: () => number;
|
|
426
|
+
isCurrent?: () => boolean;
|
|
427
|
+
isAuthorityCurrent?: () => boolean;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function captureTelegramStaleTargetRequestRecovery<TSyncState extends TelegramSyncState>(
|
|
431
|
+
body: Record<string, unknown>,
|
|
432
|
+
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> & {
|
|
433
|
+
topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "markStaleByTarget" | "persist" | "invalidateTarget">;
|
|
434
|
+
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
435
|
+
getSessionGeneration: () => number;
|
|
436
|
+
getProfileName: () => string | undefined;
|
|
437
|
+
onRecovered: () => void;
|
|
438
|
+
},
|
|
439
|
+
): ((error: unknown) => Promise<void>) | undefined {
|
|
440
|
+
const target = getTelegramTargetFromApiBody(body);
|
|
441
|
+
const epoch = deps.getCurrentLeaderEpoch();
|
|
442
|
+
if (!target || epoch === undefined) return undefined;
|
|
443
|
+
const key = getTelegramTargetKey(target);
|
|
444
|
+
const record = deps.topicTargetStore.list().find((candidate) => getTelegramTargetKey(candidate.target) === key);
|
|
445
|
+
if (!record) return undefined;
|
|
446
|
+
const generation = deps.getSessionGeneration();
|
|
447
|
+
const profile = deps.getProfileName();
|
|
448
|
+
const isAuthorityCurrent = (): boolean => deps.getCurrentLeaderEpoch() === epoch &&
|
|
449
|
+
deps.getSessionGeneration() === generation && deps.getProfileName() === profile;
|
|
450
|
+
const isCurrent = (): boolean => {
|
|
451
|
+
const current = deps.topicTargetStore.list().find((candidate) => getTelegramTargetKey(candidate.target) === key);
|
|
452
|
+
return isAuthorityCurrent() &&
|
|
453
|
+
current?.instanceId === record.instanceId && current?.profileKey === record.profileKey &&
|
|
454
|
+
current?.updatedAtMs === record.updatedAtMs && current?.createdAtMs === record.createdAtMs;
|
|
455
|
+
};
|
|
456
|
+
return async (error) => {
|
|
457
|
+
const requestTarget = getTelegramApiErrorRequestTarget(error);
|
|
458
|
+
if (!isTelegramStaleTargetHttpError(error) || !requestTarget ||
|
|
459
|
+
getTelegramTargetKey(requestTarget) !== key || !isCurrent()) return;
|
|
460
|
+
if (await recoverStaleTelegramTopicApiError(
|
|
461
|
+
{ chat_id: target.chatId, message_thread_id: target.threadId }, error, { ...deps, isCurrent, isAuthorityCurrent },
|
|
462
|
+
)) {
|
|
463
|
+
deps.onRecovered();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
416
466
|
}
|
|
417
467
|
|
|
418
468
|
export function createTelegramStaleTopicApiErrorRecoveryRuntime<
|
|
@@ -448,12 +498,16 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
448
498
|
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
|
|
449
499
|
): Promise<boolean> {
|
|
450
500
|
const target = getTelegramTargetFromApiBody(apiBody);
|
|
451
|
-
if (!target || !isTelegramTopicTargetStaleError(error)) return false;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return false;
|
|
501
|
+
if (!target || !isTelegramTopicTargetStaleError(error) || deps.isCurrent?.() === false) return false;
|
|
502
|
+
if (deps.isCurrent) {
|
|
503
|
+
if (!deps.topicTargetStore.invalidateTarget || !await deps.topicTargetStore.invalidateTarget(
|
|
504
|
+
target, deps.isCurrent, String(error),
|
|
505
|
+
)) return false;
|
|
506
|
+
if (deps.isAuthorityCurrent?.() === false) return false;
|
|
507
|
+
} else {
|
|
508
|
+
await deps.topicTargetStore.load();
|
|
509
|
+
if (!deps.topicTargetStore.markStaleByTarget(target, "deleted", String(error))) return false;
|
|
510
|
+
await deps.topicTargetStore.persist();
|
|
457
511
|
}
|
|
458
512
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
459
513
|
let state = markTelegramSyncSliceSuspect(deps.getSyncState(), "topic-state", {
|
|
@@ -466,8 +520,12 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
466
520
|
reason: "stale-api-error",
|
|
467
521
|
action: "topic-target-stale",
|
|
468
522
|
}) as TSyncState;
|
|
523
|
+
state = markTelegramSyncSliceSuspect(state, "target-bindings", {
|
|
524
|
+
nowMs,
|
|
525
|
+
reason: "stale-api-error",
|
|
526
|
+
action: "topic-target-stale",
|
|
527
|
+
}) as TSyncState;
|
|
469
528
|
deps.setSyncState(state);
|
|
470
|
-
await deps.topicTargetStore.persist();
|
|
471
529
|
deps.recordEvent("bus", error, {
|
|
472
530
|
phase: "topic-target-stale",
|
|
473
531
|
chatId: target.chatId,
|
|
@@ -606,6 +664,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
606
664
|
});
|
|
607
665
|
deps.recordThreadReconciliationPlan?.(replacementPlan);
|
|
608
666
|
await ThreadReconciler.applyThreadReconciliationPlan(replacementPlan, {
|
|
667
|
+
isCleanupTargetProtected: createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
609
668
|
callApi: deps.callApi,
|
|
610
669
|
markStaleByTarget: (target, syncStatus, lastSyncError) =>
|
|
611
670
|
deps.topicTargetStore.markStaleByTarget(
|