@llblab/pi-telegram 0.39.0 → 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 +8 -0
- package/docs/architecture.md +1 -1
- package/docs/ui-style.md +2 -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/commands.ts +23 -6
- package/lib/routing.ts +5 -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,14 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.39.1: Compaction Failure Formatting Hotfix
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
5
13
|
## 0.39.0: Session-Owned Queue Lifecycle
|
|
6
14
|
|
|
7
15
|
- `Queue Counters`: Excludes Skip-marked prompts immediately from the shared executable queue count used by the Pi status bar and Telegram main menu while preserving physical queue position, deferred drop semantics, and reversible Keep behavior.
|
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/docs/ui-style.md
CHANGED
|
@@ -53,7 +53,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
53
53
|
| `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
|
|
54
54
|
| `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
|
|
55
55
|
| `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
|
|
56
|
-
| `❌` | No / cancel /
|
|
56
|
+
| `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
|
|
57
57
|
| `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
|
|
58
58
|
|
|
59
59
|
### Informational Feedback
|
|
@@ -63,6 +63,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
63
63
|
| `💤` | Nothing active | No-active-turn notices | Neutral idle result, not an error. |
|
|
64
64
|
| `✅` | Completed successfully | Compaction and other completion notices | Use only after the operation has completed. |
|
|
65
65
|
| `🚫` | Unavailable, denied, or cancelled operation | Missing capability/auth notices, access denial, and compaction cancellation | Callback alerts use the same emoji without bold markup. |
|
|
66
|
+
| `⚠️` | Recoverable operation failure | Compaction failure notice | The attempted operation failed, but the original session state remains usable. |
|
|
66
67
|
|
|
67
68
|
### State Indicators And Button Grammars
|
|
68
69
|
|
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/commands.ts
CHANGED
|
@@ -1133,6 +1133,25 @@ function getTelegramCommandErrorMessage(error: unknown): string {
|
|
|
1133
1133
|
return error instanceof Error ? error.message : String(error);
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
|
+
function formatTelegramCompactionFailure(error: unknown): string {
|
|
1137
|
+
let message = getTelegramCommandErrorMessage(error).trim();
|
|
1138
|
+
const redundantPrefixes = [
|
|
1139
|
+
"Compaction failed: ",
|
|
1140
|
+
"Turn prefix summarization failed: ",
|
|
1141
|
+
];
|
|
1142
|
+
let stripped = true;
|
|
1143
|
+
while (stripped) {
|
|
1144
|
+
stripped = false;
|
|
1145
|
+
for (const prefix of redundantPrefixes) {
|
|
1146
|
+
if (!message.startsWith(prefix)) continue;
|
|
1147
|
+
message = message.slice(prefix.length).trim();
|
|
1148
|
+
stripped = true;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const sentence = /[.!?]$/u.test(message) ? message : `${message}.`;
|
|
1152
|
+
return `Compaction failed! ${sentence}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1136
1155
|
export function parseTelegramCommand(
|
|
1137
1156
|
text: string,
|
|
1138
1157
|
): ParsedTelegramCommand | undefined {
|
|
@@ -1432,11 +1451,10 @@ export async function handleTelegramCompactCommand(
|
|
|
1432
1451
|
deps.updateStatus();
|
|
1433
1452
|
dispatchNextQueuedTelegramTurnAfterCompact(deps);
|
|
1434
1453
|
deps.recordRuntimeEvent?.("compact", error);
|
|
1435
|
-
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
1436
1454
|
void deps.sendTextReply(
|
|
1437
1455
|
formatTelegramInformationHeading(
|
|
1438
|
-
"
|
|
1439
|
-
|
|
1456
|
+
"⚠️",
|
|
1457
|
+
formatTelegramCompactionFailure(error),
|
|
1440
1458
|
),
|
|
1441
1459
|
{ parseMode: "HTML" },
|
|
1442
1460
|
);
|
|
@@ -1447,11 +1465,10 @@ export async function handleTelegramCompactCommand(
|
|
|
1447
1465
|
deps.setCompactionInProgress(false);
|
|
1448
1466
|
deps.updateStatus();
|
|
1449
1467
|
deps.recordRuntimeEvent?.("compact", error);
|
|
1450
|
-
const errorMessage = getTelegramCommandErrorMessage(error);
|
|
1451
1468
|
await deps.sendTextReply(
|
|
1452
1469
|
formatTelegramInformationHeading(
|
|
1453
|
-
"
|
|
1454
|
-
|
|
1470
|
+
"⚠️",
|
|
1471
|
+
formatTelegramCompactionFailure(error),
|
|
1455
1472
|
),
|
|
1456
1473
|
{ parseMode: "HTML" },
|
|
1457
1474
|
);
|
package/lib/routing.ts
CHANGED
|
@@ -1677,9 +1677,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1677
1677
|
})
|
|
1678
1678
|
: undefined,
|
|
1679
1679
|
stopTypingLoop: deps.stopTypingLoop,
|
|
1680
|
-
sendTextReply: (text) =>
|
|
1680
|
+
sendTextReply: (text, options) =>
|
|
1681
1681
|
deps
|
|
1682
|
-
.sendTextReply(chatId, replyToMessageId, text, {
|
|
1682
|
+
.sendTextReply(chatId, replyToMessageId, text, {
|
|
1683
|
+
target,
|
|
1684
|
+
parseMode: options?.parseMode,
|
|
1685
|
+
})
|
|
1683
1686
|
.then(() => {}),
|
|
1684
1687
|
suppressStartNotice: true,
|
|
1685
1688
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
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;
|