@llblab/pi-telegram 0.39.1 → 0.39.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/AGENTS.md +1 -1
- package/CHANGELOG.md +4 -0
- package/docs/architecture.md +1 -1
- package/index.ts +16 -7
- package/lib/bus-follower.ts +15 -0
- package/lib/bus-leader.ts +17 -4
- package/lib/bus.ts +16 -2
- package/lib/sync.ts +17 -0
- package/lib/telegram-api.ts +91 -45
- package/lib/threads.ts +5 -0
- package/lib/updates.ts +14 -6
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
81
81
|
- A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
|
|
82
82
|
- A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
|
|
83
83
|
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
|
|
84
|
-
- Execution failures persist bounded diagnostics and attempt state as `retry-wait
|
|
84
|
+
- Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
|
|
85
85
|
- An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
|
|
86
86
|
- The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
|
|
87
87
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.39.2: Deleted Thread Receipt Hotfix
|
|
6
|
+
|
|
7
|
+
- `Deleted Thread Receipt`: Terminally settles the currently executing durable update when Telegram returns exact HTTP 400 stale/deleted-thread evidence for its `{chatId, threadId}`, including leader-first shared-store invalidation followed by follower settlement. Transient, ambiguous, unrelated, and stale-looking HTTP 5xx failures retain indefinite retry authority; persisted follower records remain restart hints rather than speculative live registrations.
|
|
8
|
+
|
|
5
9
|
## 0.39.1: Compaction Failure Formatting Hotfix
|
|
6
10
|
|
|
7
11
|
- `Compaction Failure Formatting`: Preserves the HTML parse mode when a confirmed `/compact` callback reports asynchronous failure, preventing bold standalone failure headings from appearing as literal `<b>…</b>` markup in Telegram.
|
package/docs/architecture.md
CHANGED
|
@@ -209,7 +209,7 @@ The profile-scoped journal separates transport progress from semantic progress.
|
|
|
209
209
|
|
|
210
210
|
Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and valid authority files are never automatically deleted, reset, or quarantined. Only a missing-snapshot history that cannot be reconstructed safely uses the explicit evidence-preserving quarantine-and-reset compatibility fallback above.
|
|
211
211
|
|
|
212
|
-
`pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete.
|
|
212
|
+
`pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Execution failures become `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time, except that an exact HTTP 400 stale/deleted Telegram thread error carrying its proven request `{chatId, threadId}` terminally settles the currently executing source after shared binding invalidation; follower settlement remains idempotent when the leader already persisted that stale binding. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
|
|
213
213
|
|
|
214
214
|
Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
|
|
215
215
|
|
package/index.ts
CHANGED
|
@@ -844,6 +844,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
844
844
|
});
|
|
845
845
|
},
|
|
846
846
|
);
|
|
847
|
+
const staleTopicApiErrorRecoveryDeps = {
|
|
848
|
+
topicTargetStore: threadStore,
|
|
849
|
+
getSyncState: telegramSyncStateRuntime.getState,
|
|
850
|
+
setSyncState: telegramSyncStateRuntime.setState,
|
|
851
|
+
recordEvent: recordRuntimeEvent,
|
|
852
|
+
};
|
|
853
|
+
const recoverStaleTelegramTopicApiError =
|
|
854
|
+
Sync.createTelegramStaleTopicApiErrorRecoveryRuntime(
|
|
855
|
+
staleTopicApiErrorRecoveryDeps,
|
|
856
|
+
);
|
|
847
857
|
const {
|
|
848
858
|
owner: updateWorkerOwnerRuntime,
|
|
849
859
|
leader: updateAdmissionLifecycleRuntime,
|
|
@@ -867,6 +877,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
867
877
|
worker: {
|
|
868
878
|
defaultHandle: inboundRouteRuntime.handleUpdate,
|
|
869
879
|
onStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
|
|
880
|
+
settleTerminalExecutionFailure(error) {
|
|
881
|
+
return Sync.settleStaleTelegramTopicExecutionFailure(
|
|
882
|
+
error,
|
|
883
|
+
staleTopicApiErrorRecoveryDeps,
|
|
884
|
+
);
|
|
885
|
+
},
|
|
870
886
|
},
|
|
871
887
|
leader: {
|
|
872
888
|
resolveBinding: resolveTelegramUpdateJournalBinding,
|
|
@@ -1056,13 +1072,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1056
1072
|
onPollingStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
|
|
1057
1073
|
recordRuntimeEvent,
|
|
1058
1074
|
});
|
|
1059
|
-
const recoverStaleTelegramTopicApiError =
|
|
1060
|
-
Sync.createTelegramStaleTopicApiErrorRecoveryRuntime({
|
|
1061
|
-
topicTargetStore: threadStore,
|
|
1062
|
-
getSyncState: telegramSyncStateRuntime.getState,
|
|
1063
|
-
setSyncState: telegramSyncStateRuntime.setState,
|
|
1064
|
-
recordEvent: recordRuntimeEvent,
|
|
1065
|
-
});
|
|
1066
1075
|
const authorizeFollowerApiCall = Bus.createTelegramFollowerApiCallAuthorizer({
|
|
1067
1076
|
isMessageOwned: messageOwnershipRuntime.isOwnedByFollower,
|
|
1068
1077
|
});
|
package/lib/bus-follower.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { TelegramTarget } from "./target.ts";
|
|
|
20
20
|
import {
|
|
21
21
|
isTelegramApiMethodRetrySafe,
|
|
22
22
|
TelegramApiCommitUnknownError,
|
|
23
|
+
TelegramApiStaleTargetError,
|
|
23
24
|
} from "./telegram-api.ts";
|
|
24
25
|
import {
|
|
25
26
|
createTelegramBusFollowerDeliveryIdentity,
|
|
@@ -898,6 +899,20 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
898
899
|
response?.kind === "bus.ack"
|
|
899
900
|
? response.message
|
|
900
901
|
: "Telegram bus API call did not return an acknowledgement.";
|
|
902
|
+
if (
|
|
903
|
+
response?.kind === "bus.ack" &&
|
|
904
|
+
response.error?.code === "stale-target" &&
|
|
905
|
+
response.error.chatId !== undefined &&
|
|
906
|
+
response.error.threadId !== undefined
|
|
907
|
+
) {
|
|
908
|
+
throw new TelegramApiStaleTargetError(
|
|
909
|
+
message ?? "Telegram thread target is stale.",
|
|
910
|
+
{
|
|
911
|
+
chatId: response.error.chatId,
|
|
912
|
+
threadId: response.error.threadId,
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
}
|
|
901
916
|
if (
|
|
902
917
|
response?.kind === "bus.ack" &&
|
|
903
918
|
response.error?.code === "commit-unknown"
|
package/lib/bus-leader.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as Sync from "./sync.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
10
|
import {
|
|
11
|
+
getTelegramApiErrorRequestTarget,
|
|
11
12
|
isTelegramApiCommitUnknownError,
|
|
12
13
|
type TelegramApiCallOptions,
|
|
13
14
|
} from "./telegram-api.ts";
|
|
@@ -1756,6 +1757,10 @@ async function handleFollowerApiCall(
|
|
|
1756
1757
|
result,
|
|
1757
1758
|
};
|
|
1758
1759
|
} catch (error) {
|
|
1760
|
+
const staleTarget =
|
|
1761
|
+
Threads.isTelegramTopicTargetStaleError(error)
|
|
1762
|
+
? getTelegramApiErrorRequestTarget(error)
|
|
1763
|
+
: undefined;
|
|
1759
1764
|
return {
|
|
1760
1765
|
kind: "bus.ack",
|
|
1761
1766
|
requestId: envelope.requestId,
|
|
@@ -1764,14 +1769,22 @@ async function handleFollowerApiCall(
|
|
|
1764
1769
|
error instanceof Error
|
|
1765
1770
|
? error.message
|
|
1766
1771
|
: "Telegram bus API call failed.",
|
|
1767
|
-
...(
|
|
1772
|
+
...(staleTarget
|
|
1768
1773
|
? {
|
|
1769
1774
|
error: {
|
|
1770
|
-
code: "
|
|
1771
|
-
|
|
1775
|
+
code: "stale-target" as const,
|
|
1776
|
+
chatId: staleTarget.chatId,
|
|
1777
|
+
threadId: staleTarget.threadId,
|
|
1772
1778
|
},
|
|
1773
1779
|
}
|
|
1774
|
-
:
|
|
1780
|
+
: isTelegramApiCommitUnknownError(error)
|
|
1781
|
+
? {
|
|
1782
|
+
error: {
|
|
1783
|
+
code: "commit-unknown" as const,
|
|
1784
|
+
method: error.method,
|
|
1785
|
+
},
|
|
1786
|
+
}
|
|
1787
|
+
: {}),
|
|
1775
1788
|
};
|
|
1776
1789
|
}
|
|
1777
1790
|
}
|
package/lib/bus.ts
CHANGED
|
@@ -816,8 +816,11 @@ export type TelegramBusEnvelope = (
|
|
|
816
816
|
| "commit-unknown"
|
|
817
817
|
| "request-id-collision"
|
|
818
818
|
| "ledger-overloaded"
|
|
819
|
-
| "incompatible-protocol"
|
|
819
|
+
| "incompatible-protocol"
|
|
820
|
+
| "stale-target";
|
|
820
821
|
method?: string;
|
|
822
|
+
chatId?: number;
|
|
823
|
+
threadId?: number;
|
|
821
824
|
};
|
|
822
825
|
}
|
|
823
826
|
) & { auth?: string };
|
|
@@ -2527,13 +2530,24 @@ function parseAckEnvelope(
|
|
|
2527
2530
|
code === "commit-unknown" ||
|
|
2528
2531
|
code === "request-id-collision" ||
|
|
2529
2532
|
code === "ledger-overloaded" ||
|
|
2530
|
-
code === "incompatible-protocol"
|
|
2533
|
+
code === "incompatible-protocol" ||
|
|
2534
|
+
code === "stale-target"
|
|
2531
2535
|
) {
|
|
2536
|
+
const chatId = value.error.chatId;
|
|
2537
|
+
const threadId = value.error.threadId;
|
|
2538
|
+
if (
|
|
2539
|
+
code === "stale-target" &&
|
|
2540
|
+
(!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))
|
|
2541
|
+
) {
|
|
2542
|
+
return undefined;
|
|
2543
|
+
}
|
|
2532
2544
|
envelope.error = {
|
|
2533
2545
|
code,
|
|
2534
2546
|
...(typeof value.error.method === "string"
|
|
2535
2547
|
? { method: value.error.method }
|
|
2536
2548
|
: {}),
|
|
2549
|
+
...(typeof chatId === "number" ? { chatId } : {}),
|
|
2550
|
+
...(typeof threadId === "number" ? { threadId } : {}),
|
|
2537
2551
|
};
|
|
2538
2552
|
}
|
|
2539
2553
|
}
|
package/lib/sync.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
8
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
9
10
|
import {
|
|
@@ -423,6 +424,22 @@ export function createTelegramStaleTopicApiErrorRecoveryRuntime<
|
|
|
423
424
|
recoverStaleTelegramTopicApiError(apiBody, error, deps);
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
export async function settleStaleTelegramTopicExecutionFailure<
|
|
428
|
+
TSyncState extends TelegramSyncState,
|
|
429
|
+
>(
|
|
430
|
+
error: unknown,
|
|
431
|
+
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
|
|
432
|
+
): Promise<boolean> {
|
|
433
|
+
const target = getTelegramApiErrorRequestTarget(error);
|
|
434
|
+
if (!target || !isTelegramTopicTargetStaleError(error)) return false;
|
|
435
|
+
await recoverStaleTelegramTopicApiError(
|
|
436
|
+
{ chat_id: target.chatId, message_thread_id: target.threadId },
|
|
437
|
+
error,
|
|
438
|
+
deps,
|
|
439
|
+
);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
426
443
|
export async function recoverStaleTelegramTopicApiError<
|
|
427
444
|
TSyncState extends TelegramSyncState,
|
|
428
445
|
>(
|
package/lib/telegram-api.ts
CHANGED
|
@@ -587,6 +587,7 @@ export function isTelegramApiCommitUnknownError(
|
|
|
587
587
|
class TelegramApiHttpError extends Error {
|
|
588
588
|
readonly status: number | undefined;
|
|
589
589
|
readonly retryAfterSeconds: number | undefined;
|
|
590
|
+
requestTarget?: { chatId: number; threadId: number };
|
|
590
591
|
constructor(
|
|
591
592
|
message: string,
|
|
592
593
|
status: number | undefined,
|
|
@@ -598,6 +599,41 @@ class TelegramApiHttpError extends Error {
|
|
|
598
599
|
}
|
|
599
600
|
}
|
|
600
601
|
|
|
602
|
+
function attachTelegramApiRequestTarget(
|
|
603
|
+
error: unknown,
|
|
604
|
+
body: Record<string, unknown> | Record<string, string>,
|
|
605
|
+
): void {
|
|
606
|
+
if (!(error instanceof TelegramApiHttpError)) return;
|
|
607
|
+
const chatId = Number(body.chat_id);
|
|
608
|
+
const threadId = Number(body.message_thread_id);
|
|
609
|
+
if (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId)) return;
|
|
610
|
+
error.requestTarget = { chatId, threadId };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export class TelegramApiStaleTargetError extends Error {
|
|
614
|
+
readonly requestTarget: { chatId: number; threadId: number };
|
|
615
|
+
|
|
616
|
+
constructor(
|
|
617
|
+
message: string,
|
|
618
|
+
requestTarget: { chatId: number; threadId: number },
|
|
619
|
+
) {
|
|
620
|
+
super(message);
|
|
621
|
+
this.name = "TelegramApiStaleTargetError";
|
|
622
|
+
this.requestTarget = { ...requestTarget };
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function getTelegramApiErrorRequestTarget(
|
|
627
|
+
error: unknown,
|
|
628
|
+
): { chatId: number; threadId: number } | undefined {
|
|
629
|
+
const target =
|
|
630
|
+
error instanceof TelegramApiHttpError ||
|
|
631
|
+
error instanceof TelegramApiStaleTargetError
|
|
632
|
+
? error.requestTarget
|
|
633
|
+
: undefined;
|
|
634
|
+
return target ? { ...target } : undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
601
637
|
export function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
602
638
|
return (
|
|
603
639
|
error instanceof Error && error.message.includes("message is not modified")
|
|
@@ -1156,21 +1192,26 @@ export async function callTelegram<TResponse>(
|
|
|
1156
1192
|
options?: TelegramApiCallOptions,
|
|
1157
1193
|
): Promise<TResponse> {
|
|
1158
1194
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1195
|
+
try {
|
|
1196
|
+
return await callTelegramWithRetry(
|
|
1197
|
+
method,
|
|
1198
|
+
async (family) =>
|
|
1199
|
+
telegramFetch(
|
|
1200
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1201
|
+
{
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
headers: { "content-type": "application/json" },
|
|
1204
|
+
body: JSON.stringify(body),
|
|
1205
|
+
signal: options?.signal,
|
|
1206
|
+
},
|
|
1207
|
+
family,
|
|
1208
|
+
),
|
|
1209
|
+
options,
|
|
1210
|
+
);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
attachTelegramApiRequestTarget(error, body);
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1174
1215
|
}
|
|
1175
1216
|
|
|
1176
1217
|
export type TelegramBotIdentityResponse = Pick<
|
|
@@ -1206,43 +1247,48 @@ export async function callTelegramMultipart<TResponse>(
|
|
|
1206
1247
|
): Promise<TResponse> {
|
|
1207
1248
|
const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
|
|
1208
1249
|
const fileBlob = await openAsBlob(filePath);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1250
|
+
try {
|
|
1251
|
+
return await callTelegramWithRetry(
|
|
1252
|
+
method,
|
|
1253
|
+
async (family) => {
|
|
1254
|
+
if (family) {
|
|
1255
|
+
const multipart = await buildTelegramMultipartBody(
|
|
1256
|
+
fields,
|
|
1257
|
+
fileField,
|
|
1258
|
+
fileBlob,
|
|
1259
|
+
fileName,
|
|
1260
|
+
);
|
|
1261
|
+
return telegramFetch(
|
|
1262
|
+
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1263
|
+
{
|
|
1264
|
+
method: "POST",
|
|
1265
|
+
headers: { "content-type": multipart.contentType },
|
|
1266
|
+
body: multipart.body as unknown as BodyInit,
|
|
1267
|
+
signal: options?.signal,
|
|
1268
|
+
},
|
|
1269
|
+
family,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
const form = new FormData();
|
|
1273
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1274
|
+
form.set(key, value);
|
|
1275
|
+
}
|
|
1276
|
+
form.set(fileField, fileBlob, fileName);
|
|
1219
1277
|
return telegramFetch(
|
|
1220
1278
|
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1221
1279
|
{
|
|
1222
1280
|
method: "POST",
|
|
1223
|
-
|
|
1224
|
-
body: multipart.body as unknown as BodyInit,
|
|
1281
|
+
body: form,
|
|
1225
1282
|
signal: options?.signal,
|
|
1226
1283
|
},
|
|
1227
|
-
family,
|
|
1228
1284
|
);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
|
|
1237
|
-
{
|
|
1238
|
-
method: "POST",
|
|
1239
|
-
body: form,
|
|
1240
|
-
signal: options?.signal,
|
|
1241
|
-
},
|
|
1242
|
-
);
|
|
1243
|
-
},
|
|
1244
|
-
options,
|
|
1245
|
-
);
|
|
1285
|
+
},
|
|
1286
|
+
options,
|
|
1287
|
+
);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
attachTelegramApiRequestTarget(error, fields);
|
|
1290
|
+
throw error;
|
|
1291
|
+
}
|
|
1246
1292
|
}
|
|
1247
1293
|
|
|
1248
1294
|
export async function downloadTelegramFile(
|
package/lib/threads.ts
CHANGED
|
@@ -2882,6 +2882,11 @@ export function getTelegramTargetFromApiBody(
|
|
|
2882
2882
|
|
|
2883
2883
|
export function isTelegramTopicTargetStaleError(error: unknown): boolean {
|
|
2884
2884
|
if (!(error instanceof Error)) return false;
|
|
2885
|
+
const status =
|
|
2886
|
+
"status" in error && typeof error.status === "number"
|
|
2887
|
+
? error.status
|
|
2888
|
+
: undefined;
|
|
2889
|
+
if (status !== undefined && status !== 400) return false;
|
|
2885
2890
|
const message = error.message.toLowerCase();
|
|
2886
2891
|
return (
|
|
2887
2892
|
message.includes("topic_id_invalid") ||
|
package/lib/updates.ts
CHANGED
|
@@ -1782,6 +1782,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
|
1782
1782
|
classifyExecutionFailure?: (
|
|
1783
1783
|
error: unknown,
|
|
1784
1784
|
) => TelegramUpdateExecutionFailureClassification;
|
|
1785
|
+
settleTerminalExecutionFailure?: (error: unknown) => Promise<boolean>;
|
|
1785
1786
|
scheduleRetry?: (callback: () => void, delayMs: number) => unknown;
|
|
1786
1787
|
cancelRetry?: (handle: unknown) => void;
|
|
1787
1788
|
batchSize?: number;
|
|
@@ -4833,13 +4834,20 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
4833
4834
|
});
|
|
4834
4835
|
worker = createTelegramUpdateWorkerRuntime({
|
|
4835
4836
|
...deps,
|
|
4836
|
-
executeUpdate
|
|
4837
|
+
async executeUpdate(update, ctx, signal) {
|
|
4837
4838
|
const typedUpdate = update as TUpdate;
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4839
|
+
try {
|
|
4840
|
+
return await executeUpdate(
|
|
4841
|
+
deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
|
|
4842
|
+
ctx,
|
|
4843
|
+
signal,
|
|
4844
|
+
);
|
|
4845
|
+
} catch (error) {
|
|
4846
|
+
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
4847
|
+
return { kind: "complete" };
|
|
4848
|
+
}
|
|
4849
|
+
throw error;
|
|
4850
|
+
}
|
|
4843
4851
|
},
|
|
4844
4852
|
});
|
|
4845
4853
|
return worker;
|