@llblab/pi-telegram 0.52.0 → 0.52.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 +6 -5
- package/BACKLOG.md +18 -1
- package/CHANGELOG.md +12 -0
- package/README.md +5 -2
- package/dist/lib/bindings.js +1 -0
- package/dist/lib/bus-api.js +4 -3
- package/dist/lib/extension.js +4 -0
- package/dist/lib/journal.d.ts +7 -1
- package/dist/lib/journal.js +72 -11
- package/dist/lib/locks.d.ts +5 -0
- package/dist/lib/locks.js +29 -11
- package/dist/lib/polling.d.ts +2 -0
- package/dist/lib/polling.js +12 -2
- package/dist/lib/queue.d.ts +2 -0
- package/dist/lib/queue.js +25 -3
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +118 -44
- package/dist/lib/sync.d.ts +5 -0
- package/dist/lib/sync.js +48 -5
- package/dist/lib/telegram-api.d.ts +2 -0
- package/dist/lib/telegram-api.js +22 -1
- package/dist/lib/threads.d.ts +6 -4
- package/dist/lib/threads.js +45 -3
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +34 -18
- package/dist/package.json +1 -1
- package/docs/architecture.md +14 -8
- package/docs/ui-style.md +1 -1
- package/lib/bindings.ts +1 -0
- package/lib/bus-api.ts +4 -2
- package/lib/extension.ts +5 -0
- package/lib/journal.ts +80 -11
- package/lib/locks.ts +28 -11
- package/lib/polling.ts +21 -2
- package/lib/queue.ts +26 -3
- package/lib/routing.ts +91 -36
- package/lib/sync.ts +47 -5
- package/lib/telegram-api.ts +26 -1
- package/lib/threads.ts +46 -7
- package/lib/updates.ts +43 -21
- package/package.json +1 -1
package/dist/lib/routing.js
CHANGED
|
@@ -189,6 +189,7 @@ function formatTelegramTemporaryThreadChooserText(command) {
|
|
|
189
189
|
"<b>🧵 Choose target thread:</b>",
|
|
190
190
|
"",
|
|
191
191
|
command ? `You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.` : "Choose where to send your message.",
|
|
192
|
+
"Unaccepted routing expires after 60 minutes; disposable tabs are removed when clear. Accepted work and restored Threads are kept.",
|
|
192
193
|
"Select the Pi thread that should handle it, or restore a Pi into this tab:",
|
|
193
194
|
].join("\n");
|
|
194
195
|
}
|
|
@@ -1902,9 +1903,9 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1902
1903
|
* absence of chooser and journal custody, and the ordinary protection checks must all hold again. The attempt is
|
|
1903
1904
|
* durably issued once; a skipped or unknown removal keeps the entry protecting the tab across restart.
|
|
1904
1905
|
*/
|
|
1905
|
-
const runTemporaryThreadCleanup = async (token, cap) => {
|
|
1906
|
-
const store = cap.store
|
|
1907
|
-
if (!
|
|
1906
|
+
const runTemporaryThreadCleanup = async (token, cap, reconcileExpiry) => {
|
|
1907
|
+
const store = cap.store;
|
|
1908
|
+
if ((!deps.inspectRestoreSourceAbandonment && !deps.inspectRoutingInputGroupExpiry) || !deps.runWorkspaceOperation || !deps.callApi || !cap.isCurrent())
|
|
1908
1909
|
return;
|
|
1909
1910
|
await deps.runWorkspaceOperation({ operationId: `temporary-thread-cleanup-${randomBytes(16).toString("hex")}`,
|
|
1910
1911
|
operationKind: "workspace.temporary-thread", scopes: [{ kind: "profile" }] }, async () => {
|
|
@@ -1912,15 +1913,48 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1912
1913
|
return;
|
|
1913
1914
|
let entry = store.listTemporaryThreads().find(value => value.token === token && value.phase === "created" && !!value.target);
|
|
1914
1915
|
const target = entry?.target;
|
|
1915
|
-
if (!entry || !target || entry.cleanupIssued ||
|
|
1916
|
-
entry.
|
|
1916
|
+
if (!entry || !target || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId ||
|
|
1917
|
+
entry.source.journalBindingKey !== cap.journalBindingKey)
|
|
1918
|
+
return;
|
|
1919
|
+
const owner = `telegram-owner:${cap.operatorUserId}`;
|
|
1920
|
+
if (reconcileExpiry && deps.inspectRoutingInputGroupExpiry) {
|
|
1921
|
+
for (const group of Threads.getTelegramTemporaryThreadInputs(entry)) {
|
|
1922
|
+
if ([...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? [])].some(value => isDeepStrictEqual(value, group)))
|
|
1923
|
+
continue;
|
|
1924
|
+
const inspect = (id) => deps.inspectRoutingInputGroupExpiry(group)?.find(value => value.updateId === id);
|
|
1925
|
+
if (!group.updateIds.every(id => inspect(id)?.operatorAuthorityId === owner))
|
|
1926
|
+
continue;
|
|
1927
|
+
entry = cap.adopt(entry);
|
|
1928
|
+
if (!entry || !cap.isCurrent())
|
|
1929
|
+
return;
|
|
1930
|
+
let recorded, failure;
|
|
1931
|
+
try {
|
|
1932
|
+
recorded = store.recordTemporaryThreadInputExpiry(entry, group, cap.authority, inspect);
|
|
1933
|
+
}
|
|
1934
|
+
catch (error) {
|
|
1935
|
+
failure = error;
|
|
1936
|
+
}
|
|
1937
|
+
if (!cap.isCurrent())
|
|
1938
|
+
return;
|
|
1939
|
+
const retained = store.listTemporaryThreads().find(value => value.token === token);
|
|
1940
|
+
// A lost publication ACK may already have recorded the group or retired a bound tab's temporary frame.
|
|
1941
|
+
if (!retained)
|
|
1942
|
+
return;
|
|
1943
|
+
if (!recorded && !retained.cancelledInputs?.some(value => isDeepStrictEqual(value, group)))
|
|
1944
|
+
throw failure ?? new Error("Chooser expiry metadata was not confirmed.");
|
|
1945
|
+
entry = retained;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (!Threads.isTelegramTemporaryThreadFullyResolved(entry) || hasPendingRerouteForTarget(target))
|
|
1917
1949
|
return;
|
|
1918
|
-
const
|
|
1919
|
-
const cancelledCurrent = () => cancelled.every(input =>
|
|
1920
|
-
const
|
|
1921
|
-
return
|
|
1922
|
-
evidence.
|
|
1923
|
-
|
|
1950
|
+
const cancelled = entry.cancelledInputs ?? [];
|
|
1951
|
+
const cancelledCurrent = () => cancelled.every(input => {
|
|
1952
|
+
const expiry = deps.inspectRoutingInputGroupExpiry?.(input);
|
|
1953
|
+
return input.updateIds.every(updateId => {
|
|
1954
|
+
const evidence = deps.inspectRestoreSourceAbandonment?.(updateId, input.journalBindingKey) ?? expiry?.find(value => value.updateId === updateId);
|
|
1955
|
+
return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId && evidence.operatorAuthorityId === owner;
|
|
1956
|
+
});
|
|
1957
|
+
});
|
|
1924
1958
|
if (!cancelledCurrent())
|
|
1925
1959
|
return;
|
|
1926
1960
|
entry = cap.adopt(entry);
|
|
@@ -1940,8 +1974,40 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1940
1974
|
store.retireTemporaryThread(entry, cap.authority);
|
|
1941
1975
|
});
|
|
1942
1976
|
};
|
|
1943
|
-
|
|
1944
|
-
|
|
1977
|
+
// Reuse the one timer per tab for body-free metadata retries. Future retry timers do not join settlement waits;
|
|
1978
|
+
// only their running attempt does. No retry is licensed once a destructive grant may have been issued.
|
|
1979
|
+
const queueTemporaryThreadCleanup = (token, cap, delay, reconcileExpiry, retry = false) => {
|
|
1980
|
+
cancelTemporaryThreadCleanup(token);
|
|
1981
|
+
let settle = () => undefined;
|
|
1982
|
+
const task = new Promise(resolve => { settle = resolve; });
|
|
1983
|
+
const timer = setTimeout(() => {
|
|
1984
|
+
if (temporaryCleanupTimers.get(token)?.timer !== timer)
|
|
1985
|
+
return;
|
|
1986
|
+
temporaryCleanupTimers.delete(token);
|
|
1987
|
+
if (retry)
|
|
1988
|
+
restoreSettlementTasks.add(task);
|
|
1989
|
+
runTemporaryThreadCleanup(token, cap, reconcileExpiry)
|
|
1990
|
+
.catch(error => {
|
|
1991
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup" });
|
|
1992
|
+
if (!reconcileExpiry || !cap.isCurrent())
|
|
1993
|
+
return;
|
|
1994
|
+
try {
|
|
1995
|
+
const retained = cap.store.listTemporaryThreads().find(value => value.token === token);
|
|
1996
|
+
if (retained && !retained.cleanupIssued && cap.isCurrent())
|
|
1997
|
+
queueTemporaryThreadCleanup(token, cap, 60_000, true, true);
|
|
1998
|
+
}
|
|
1999
|
+
catch { /* Unknown grant state never licenses a retry. */ }
|
|
2000
|
+
})
|
|
2001
|
+
.finally(settle);
|
|
2002
|
+
}, delay);
|
|
2003
|
+
timer.unref?.();
|
|
2004
|
+
temporaryCleanupTimers.set(token, { timer, settle });
|
|
2005
|
+
if (!retry)
|
|
2006
|
+
restoreSettlementTasks.add(task);
|
|
2007
|
+
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
2008
|
+
};
|
|
2009
|
+
/** Starts the quiet period for resolved groups or body-free expiry reconciliation; fresh input/authority revokes it. */
|
|
2010
|
+
const scheduleTemporaryThreadCleanup = (target, ctx, reconcileExpiry = false) => {
|
|
1945
2011
|
const cap = captureTemporaryThreadAuthority(ctx);
|
|
1946
2012
|
if (!cap || !deps.runWorkspaceOperation || target.threadId === undefined || hasPendingRerouteForTarget(target))
|
|
1947
2013
|
return;
|
|
@@ -1953,27 +2019,22 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1953
2019
|
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-schedule" });
|
|
1954
2020
|
return;
|
|
1955
2021
|
}
|
|
1956
|
-
if (!entry || entry.cleanupIssued ||
|
|
1957
|
-
|
|
2022
|
+
if (!entry || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId || entry.source.journalBindingKey !== cap.journalBindingKey)
|
|
2023
|
+
return;
|
|
2024
|
+
if (!reconcileExpiry && !Threads.isTelegramTemporaryThreadFullyResolved(entry)) {
|
|
2025
|
+
try {
|
|
2026
|
+
reconcileExpiry = Threads.getTelegramTemporaryThreadInputs(entry).some(group => !!deps.inspectRoutingInputGroupExpiry?.(group));
|
|
2027
|
+
}
|
|
2028
|
+
catch (error) {
|
|
2029
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-schedule" });
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (!reconcileExpiry && !Threads.isTelegramTemporaryThreadFullyResolved(entry))
|
|
1958
2034
|
return;
|
|
1959
|
-
const token = entry.token;
|
|
1960
|
-
cancelTemporaryThreadCleanup(token);
|
|
1961
2035
|
const requested = deps.temporaryThreadCleanupDelayMs;
|
|
1962
2036
|
const delay = typeof requested === "number" && Number.isFinite(requested) ? Math.max(0, requested) : 1000;
|
|
1963
|
-
|
|
1964
|
-
const task = new Promise(resolve => { settle = resolve; });
|
|
1965
|
-
const timer = setTimeout(() => {
|
|
1966
|
-
if (temporaryCleanupTimers.get(token)?.timer !== timer)
|
|
1967
|
-
return;
|
|
1968
|
-
temporaryCleanupTimers.delete(token);
|
|
1969
|
-
runTemporaryThreadCleanup(token, cap)
|
|
1970
|
-
.catch(error => { deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup" }); })
|
|
1971
|
-
.finally(settle);
|
|
1972
|
-
}, delay);
|
|
1973
|
-
timer.unref?.();
|
|
1974
|
-
temporaryCleanupTimers.set(token, { timer, settle });
|
|
1975
|
-
restoreSettlementTasks.add(task);
|
|
1976
|
-
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
2037
|
+
queueTemporaryThreadCleanup(entry.token, cap, delay, reconcileExpiry);
|
|
1977
2038
|
};
|
|
1978
2039
|
const recordCancelledTemporaryThreadInput = (pending, ctx, isCurrent) => {
|
|
1979
2040
|
const store = deps.getWorkspaceRestoreStore?.(), target = pending.sourceTarget;
|
|
@@ -2056,10 +2117,15 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
2056
2117
|
if (!cap.isCurrent())
|
|
2057
2118
|
return;
|
|
2058
2119
|
const self = cap.authority.executor.instanceId;
|
|
2059
|
-
const
|
|
2120
|
+
const entries = cap.store.listTemporaryThreads();
|
|
2121
|
+
const preserved = entries.filter(entry => entry.operatorUserId === cap.operatorUserId && entry.phase === "created" &&
|
|
2122
|
+
cap.store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary" &&
|
|
2123
|
+
Threads.getTelegramTemporaryThreadInputs(entry).some(source => source.journalBindingKey === journalBindingKey &&
|
|
2124
|
+
source.updateIds.some(id => input.routingSourceIds?.includes(id)))).map(entry => entry.token);
|
|
2125
|
+
const disposable = entries.flatMap(entry => entry.operatorUserId === cap.operatorUserId && !preserved.includes(entry.token) &&
|
|
2060
2126
|
entry.executor.instanceId !== self && entry.phase === "created" && entry.target &&
|
|
2061
2127
|
cap.store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary" ? [entry] : []);
|
|
2062
|
-
const forgotten = cap.store.forgetPreviousWorld(cap.authority);
|
|
2128
|
+
const forgotten = cap.store.forgetPreviousWorld(cap.authority, preserved);
|
|
2063
2129
|
if (!forgotten)
|
|
2064
2130
|
return;
|
|
2065
2131
|
result.forgotten = forgotten.operations.length + forgotten.temporaryThreads.length;
|
|
@@ -2087,6 +2153,17 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
2087
2153
|
deps.recordRuntimeEvent?.("routing", error, { phase: "new-world-tab-delete" });
|
|
2088
2154
|
}
|
|
2089
2155
|
}
|
|
2156
|
+
// Same-instance session replacement may have lost a metadata-only timer after the source was already spent.
|
|
2157
|
+
// Reconstruct from retained tab/source proof under fresh authority, never from a prompt body or an issued delete.
|
|
2158
|
+
for (const entry of cap.store.listTemporaryThreads()) {
|
|
2159
|
+
if (!cap.isCurrent())
|
|
2160
|
+
break;
|
|
2161
|
+
if (entry.phase !== "created" || !entry.target || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId ||
|
|
2162
|
+
entry.source.journalBindingKey !== journalBindingKey)
|
|
2163
|
+
continue;
|
|
2164
|
+
if (Threads.getTelegramTemporaryThreadInputs(entry).some(group => !!deps.inspectRoutingInputGroupExpiry?.(group)))
|
|
2165
|
+
scheduleTemporaryThreadCleanup(entry.target, ctx, true);
|
|
2166
|
+
}
|
|
2090
2167
|
});
|
|
2091
2168
|
}
|
|
2092
2169
|
catch (error) {
|
|
@@ -2172,10 +2249,11 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
2172
2249
|
const expireRoutingInput = async (source, ctx, signal) => {
|
|
2173
2250
|
const operator = deps.configStore.getAllowedUserId(), epoch = deps.getCurrentLeaderEpoch?.();
|
|
2174
2251
|
const message = source.original.update.message;
|
|
2175
|
-
const
|
|
2252
|
+
const runtimeCurrent = () => !signal.aborted && operator !== undefined && epoch !== undefined &&
|
|
2176
2253
|
deps.isContextActive?.(ctx) === true && deps.configStore.getAllowedUserId() === operator &&
|
|
2177
2254
|
deps.getCurrentLeaderEpoch?.() === epoch && deps.getAdmissionJournalBinding?.() === source.journalBindingKey &&
|
|
2178
2255
|
source.original.routingInput?.operatorUserId === operator && message?.from?.id === operator && message.from.is_bot !== true && message.chat?.type === "private";
|
|
2256
|
+
const current = () => runtimeCurrent() && source.isCurrent();
|
|
2179
2257
|
if (!current() || !deps.runWorkspaceOperation)
|
|
2180
2258
|
return;
|
|
2181
2259
|
await deps.runWorkspaceOperation({ operationId: `routing-expiry-${randomBytes(16).toString("hex")}`,
|
|
@@ -2185,19 +2263,11 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
2185
2263
|
const restores = deps.getWorkspaceRestoreStore?.();
|
|
2186
2264
|
if (deps.getWorkspaceRestoreStore && !restores)
|
|
2187
2265
|
return;
|
|
2188
|
-
|
|
2189
|
-
intent.request.source.updateIds.includes(source.original.updateId)))
|
|
2190
|
-
return;
|
|
2191
|
-
for (const pending of pendingUnboundReroutes.values()) {
|
|
2192
|
-
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId))
|
|
2193
|
-
continue;
|
|
2194
|
-
if (pending.selectionAttempted || pending.destinationSelected || pending.dispatching || pending.foreignForwardIssued || pending.workspaceRestore || pending.foreignRetry || pending.cleanup || pending.abandonment?.running)
|
|
2195
|
-
return;
|
|
2196
|
-
}
|
|
2266
|
+
const temporary = restores?.listTemporaryThreads().find(entry => Threads.getTelegramTemporaryThreadInputs(entry).some(input => input.journalBindingKey === source.journalBindingKey && input.updateIds.includes(source.original.updateId)));
|
|
2197
2267
|
const result = source.expire();
|
|
2198
2268
|
if (!result)
|
|
2199
2269
|
return;
|
|
2200
|
-
//
|
|
2270
|
+
// Expiry revokes the donor carrier; old controls and late ACKs cannot issue another delivery. No body is archived.
|
|
2201
2271
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
2202
2272
|
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId))
|
|
2203
2273
|
continue;
|
|
@@ -2214,6 +2284,10 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
2214
2284
|
}
|
|
2215
2285
|
}
|
|
2216
2286
|
}
|
|
2287
|
+
// The scheduled stage captures only context/epoch and a tab token. It reconstructs disposition from body-free proof,
|
|
2288
|
+
// never closes over this source or asks the worker to retain a terminal prompt while metadata publication fails.
|
|
2289
|
+
if (temporary?.target && runtimeCurrent())
|
|
2290
|
+
scheduleTemporaryThreadCleanup(temporary.target, ctx, true);
|
|
2217
2291
|
});
|
|
2218
2292
|
};
|
|
2219
2293
|
const renderCancellationReview = async (view, isCurrent) => {
|
package/dist/lib/sync.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
92
92
|
recordRuntimeEvent: (category: string, error: unknown, details?: Record<string, unknown>) => void;
|
|
93
93
|
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
94
94
|
workspaceOperationKind?: string;
|
|
95
|
+
isDisconnectCurrent?: () => boolean;
|
|
95
96
|
getNowMs?: () => number;
|
|
96
97
|
}
|
|
97
98
|
export declare function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
@@ -112,6 +113,10 @@ export interface TelegramThreadDisconnectAssembly {
|
|
|
112
113
|
}
|
|
113
114
|
export declare function createTelegramThreadDisconnectAssembly<TSyncState extends TelegramSyncState>(deps: Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> & {
|
|
114
115
|
stopPolling: () => Promise<string>;
|
|
116
|
+
captureStopPolling?: () => {
|
|
117
|
+
isCurrent: () => boolean;
|
|
118
|
+
stop: () => Promise<string>;
|
|
119
|
+
};
|
|
115
120
|
suspendPolling: () => Promise<void>;
|
|
116
121
|
}): TelegramThreadDisconnectAssembly;
|
|
117
122
|
export declare function createTelegramManualThreadDisconnectHandler<TSyncState extends TelegramSyncState>(deps: TelegramManualThreadDisconnectDeps<TSyncState>): () => Promise<string>;
|
package/dist/lib/sync.js
CHANGED
|
@@ -74,11 +74,41 @@ function createTelegramSessionRestartThreadCleanupHandler(deps) {
|
|
|
74
74
|
}
|
|
75
75
|
export function createTelegramThreadDisconnectAssembly(deps) {
|
|
76
76
|
return {
|
|
77
|
-
disconnect
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
async disconnect() {
|
|
78
|
+
const captured = deps.captureStopPolling?.();
|
|
79
|
+
const stop = captured?.stop ?? deps.stopPolling;
|
|
80
|
+
let stopAttempted = false;
|
|
81
|
+
const disconnect = createTelegramManualThreadDisconnectHandler({
|
|
82
|
+
...deps,
|
|
83
|
+
workspaceOperationKind: "workspace.disconnect-thread",
|
|
84
|
+
isDisconnectCurrent: captured?.isCurrent ?? deps.isDisconnectCurrent,
|
|
85
|
+
stopPolling() {
|
|
86
|
+
stopAttempted = true;
|
|
87
|
+
return stop();
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
return await disconnect();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Never retry a stop whose teardown/release outcome may already have committed.
|
|
95
|
+
if (stopAttempted) {
|
|
96
|
+
deps.recordRuntimeEvent("connection", error, { phase: "disconnect-stop" });
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
deps.recordRuntimeEvent("connection", error, { phase: "disconnect-cleanup" });
|
|
100
|
+
let stopped;
|
|
101
|
+
try {
|
|
102
|
+
stopped = await stop();
|
|
103
|
+
}
|
|
104
|
+
catch (stopError) {
|
|
105
|
+
deps.recordRuntimeEvent("connection", stopError, { phase: "disconnect-stop" });
|
|
106
|
+
throw stopError;
|
|
107
|
+
}
|
|
108
|
+
const separator = /[.!?]$/u.test(stopped) ? " " : ". ";
|
|
109
|
+
return `${stopped}${separator}Thread cleanup was not confirmed. Check /telegram-status --debug.`;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
82
112
|
cleanupForSessionRestart: createTelegramSessionRestartThreadCleanupHandler({
|
|
83
113
|
...deps,
|
|
84
114
|
suspendPolling: deps.suspendPolling,
|
|
@@ -86,7 +116,12 @@ export function createTelegramThreadDisconnectAssembly(deps) {
|
|
|
86
116
|
};
|
|
87
117
|
}
|
|
88
118
|
export function createTelegramManualThreadDisconnectHandler(deps) {
|
|
119
|
+
const assertCurrent = () => {
|
|
120
|
+
if (deps.isDisconnectCurrent?.() === false)
|
|
121
|
+
throw new Error("Telegram disconnect was superseded by a new connection.");
|
|
122
|
+
};
|
|
89
123
|
const operation = async () => {
|
|
124
|
+
assertCurrent();
|
|
90
125
|
const currentRecord = deps.getCurrentThreadRecord();
|
|
91
126
|
let cleanupPending = false;
|
|
92
127
|
if (currentRecord?.target.threadId) {
|
|
@@ -98,6 +133,7 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
|
|
|
98
133
|
if (isManualFollower && !ownsLeader) {
|
|
99
134
|
if (deps.disconnectFollowerThread) {
|
|
100
135
|
const disconnected = await deps.disconnectFollowerThread();
|
|
136
|
+
assertCurrent();
|
|
101
137
|
if (!disconnected) {
|
|
102
138
|
throw new Error("Telegram follower thread deletion requires a live leader registration.");
|
|
103
139
|
}
|
|
@@ -122,6 +158,7 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
|
|
|
122
158
|
const isCleanupTargetProtected = createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
123
159
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
124
160
|
await deps.topicTargetStore.persist();
|
|
161
|
+
assertCurrent();
|
|
125
162
|
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
126
163
|
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
127
164
|
currentLeaderEpoch: leaderEpoch,
|
|
@@ -131,20 +168,25 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
|
|
|
131
168
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(cleanupPlan, {
|
|
132
169
|
isCleanupTargetProtected,
|
|
133
170
|
callApi(method, body) {
|
|
171
|
+
assertCurrent();
|
|
134
172
|
return deps.callApi(method, body);
|
|
135
173
|
},
|
|
136
174
|
markStaleByTarget(targetToMark, syncStatus, lastSyncError) {
|
|
175
|
+
assertCurrent();
|
|
137
176
|
return deps.topicTargetStore.markStaleByTarget(targetToMark, syncStatus, lastSyncError);
|
|
138
177
|
},
|
|
139
178
|
removeCleanupIntentById(id) {
|
|
179
|
+
assertCurrent();
|
|
140
180
|
return deps.topicTargetStore.removePendingCleanup(id);
|
|
141
181
|
},
|
|
142
182
|
persist() {
|
|
183
|
+
assertCurrent();
|
|
143
184
|
return deps.topicTargetStore.persist();
|
|
144
185
|
},
|
|
145
186
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
146
187
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
147
188
|
});
|
|
189
|
+
assertCurrent();
|
|
148
190
|
if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) {
|
|
149
191
|
return "Thread disconnect superseded by a new binding.";
|
|
150
192
|
}
|
|
@@ -160,6 +202,7 @@ export function createTelegramManualThreadDisconnectHandler(deps) {
|
|
|
160
202
|
action: "manual-disconnect",
|
|
161
203
|
}));
|
|
162
204
|
}
|
|
205
|
+
assertCurrent();
|
|
163
206
|
const stopped = await deps.stopPolling();
|
|
164
207
|
return cleanupPending
|
|
165
208
|
? `${stopped} Telegram thread cleanup remains pending for the next leader.`
|
|
@@ -514,6 +514,8 @@ export declare function fetchTelegramBotIdentity(botToken: string, fetchImpl?: t
|
|
|
514
514
|
*/
|
|
515
515
|
export declare function callTelegramMultipart<TResponse>(botToken: string | undefined, method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions): Promise<TResponse>;
|
|
516
516
|
export declare function downloadTelegramFile(botToken: string | undefined, fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions): Promise<string>;
|
|
517
|
+
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
518
|
+
export declare function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined;
|
|
517
519
|
export declare function answerTelegramCallbackQuery(botToken: string | undefined, callbackQueryId: string, text?: string, options?: TelegramAnswerCallbackQueryOptions): Promise<void>;
|
|
518
520
|
export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?: {
|
|
519
521
|
message_thread_id?: number;
|
package/dist/lib/telegram-api.js
CHANGED
|
@@ -30,6 +30,7 @@ function getTelegramApiTempDir() {
|
|
|
30
30
|
return resolveTelegramAttachmentsDir();
|
|
31
31
|
}
|
|
32
32
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
33
|
+
const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
|
|
33
34
|
const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
|
|
34
35
|
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
|
|
35
36
|
const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
|
|
@@ -391,6 +392,22 @@ async function writeTelegramDownloadResponse(response, targetPath, maxFileSizeBy
|
|
|
391
392
|
}
|
|
392
393
|
await pipeline(Readable.from(response.body, { objectMode: false }), createTelegramDownloadLimitTransform(maxFileSizeBytes), createWriteStream(targetPath, { mode: 0o600 }));
|
|
393
394
|
}
|
|
395
|
+
async function publishTelegramDownload(partPath, targetPath, signal) {
|
|
396
|
+
for (let attempt = 0;; attempt++) {
|
|
397
|
+
throwIfTelegramApiCallAborted(signal);
|
|
398
|
+
try {
|
|
399
|
+
await rename(partPath, targetPath);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const code = error?.code;
|
|
404
|
+
// Windows may briefly deny replacement while another publisher/scanner holds the file.
|
|
405
|
+
if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES") || attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS)
|
|
406
|
+
throw error;
|
|
407
|
+
await sleepTelegramRetry(50 * 2 ** attempt, signal);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
394
411
|
async function removeTelegramPartialDownload(path) {
|
|
395
412
|
try {
|
|
396
413
|
await unlink(path);
|
|
@@ -787,7 +804,7 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
|
|
|
787
804
|
assertTelegramFileSizeWithinLimit(contentLength ? Number.parseInt(contentLength, 10) : undefined, options?.maxFileSizeBytes);
|
|
788
805
|
try {
|
|
789
806
|
await writeTelegramDownloadResponse(response, partPath, options?.maxFileSizeBytes);
|
|
790
|
-
await
|
|
807
|
+
await publishTelegramDownload(partPath, targetPath, options?.signal);
|
|
791
808
|
}
|
|
792
809
|
catch (error) {
|
|
793
810
|
await removeTelegramPartialDownload(partPath);
|
|
@@ -795,6 +812,10 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
|
|
|
795
812
|
}
|
|
796
813
|
return targetPath;
|
|
797
814
|
}
|
|
815
|
+
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
816
|
+
export function formatTelegramCallbackAnswerText(text) {
|
|
817
|
+
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
818
|
+
}
|
|
798
819
|
export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
|
|
799
820
|
try {
|
|
800
821
|
await callTelegram(botToken, "answerCallbackQuery", text
|
package/dist/lib/threads.d.ts
CHANGED
|
@@ -252,7 +252,7 @@ export interface TelegramTemporaryThreadInput {
|
|
|
252
252
|
journalBindingKey: string;
|
|
253
253
|
updateIds: number[];
|
|
254
254
|
}
|
|
255
|
-
/**
|
|
255
|
+
/** Journal-owned donor discard evidence: retained manual cancellation or body-free chooser expiry, never recipient cancellation. */
|
|
256
256
|
export interface TelegramTemporaryThreadCancellationEvidence {
|
|
257
257
|
journalBindingKey: string;
|
|
258
258
|
updateId: number;
|
|
@@ -266,7 +266,7 @@ export interface TelegramTemporaryThreadEntry {
|
|
|
266
266
|
};
|
|
267
267
|
/** Append-only known source groups. Missing legacy metadata never proves that the creation source was alone. */
|
|
268
268
|
inputs?: TelegramTemporaryThreadInput[];
|
|
269
|
-
/** Whole known groups with positively observed cancellation; never
|
|
269
|
+
/** Whole known groups with positively observed donor cancellation or expiry; never recipient cancellation or deletion authority. */
|
|
270
270
|
cancelledInputs?: TelegramTemporaryThreadInput[];
|
|
271
271
|
/** Whole known groups whose Forward was positively completed by the journal owner; never deletion authority alone. */
|
|
272
272
|
completedInputs?: TelegramTemporaryThreadInput[];
|
|
@@ -361,6 +361,8 @@ export interface TelegramWorkspaceRestore {
|
|
|
361
361
|
recordTemporaryThreadForwardIssued(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput, authority: TelegramWorkspaceRestoreAuthority): TelegramTemporaryThreadEntry | undefined;
|
|
362
362
|
recordTemporaryThreadInputCompletion(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput, authority: TelegramWorkspaceRestoreAuthority): TelegramTemporaryThreadEntry | undefined;
|
|
363
363
|
recordTemporaryThreadInputCancellation(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput, authority: TelegramWorkspaceRestoreAuthority, inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
364
|
+
/** Body-free chooser expiry may terminate an uncertain donor Forward/Restore, never accepted recipient work or a bound target. */
|
|
365
|
+
recordTemporaryThreadInputExpiry(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput, authority: TelegramWorkspaceRestoreAuthority, inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
364
366
|
/** Caller holds profile admission and proves fresh source/protection clearance; publication grants one attempt, never retry. */
|
|
365
367
|
issueTemporaryThreadCleanup(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority): {
|
|
366
368
|
issued: true;
|
|
@@ -370,8 +372,8 @@ export interface TelegramWorkspaceRestore {
|
|
|
370
372
|
/** `completed` names one newly completed group; every other known group must already be cancelled or completed. */
|
|
371
373
|
retireTemporaryThread(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority, completed?: TelegramTemporaryThreadInput): TelegramTemporaryThreadEntry | undefined;
|
|
372
374
|
/** New-world restart: atomically forgets this operator's Restore intents and temporary entries from previous runtime instances.
|
|
373
|
-
* Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
374
|
-
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority): {
|
|
375
|
+
* Caller may preserve exact unbound temporary tokens for clock-bearing sources. Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
376
|
+
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority, preserveTemporaryTokens?: readonly string[]): {
|
|
375
377
|
operations: TelegramWorkspaceRestoreIntent[];
|
|
376
378
|
temporaryThreads: TelegramTemporaryThreadEntry[];
|
|
377
379
|
} | undefined;
|
package/dist/lib/threads.js
CHANGED
|
@@ -2750,6 +2750,47 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2750
2750
|
});
|
|
2751
2751
|
return recorded && current() ? recorded : undefined;
|
|
2752
2752
|
},
|
|
2753
|
+
recordTemporaryThreadInputExpiry(expected, inputValue, authority, inspect) {
|
|
2754
|
+
const input = structuredClone(inputValue);
|
|
2755
|
+
if (!isTemporaryThreadInput(input) || typeof inspect !== "function")
|
|
2756
|
+
return undefined;
|
|
2757
|
+
const operator = authority.operatorUserId, executor = structuredClone(authority.executor);
|
|
2758
|
+
const bound = classifyTemporaryTarget(read(), expected).kind === "bound";
|
|
2759
|
+
const current = () => authority.isCurrent() && authority.operatorUserId === operator &&
|
|
2760
|
+
isDeepStrictEqual(authority.executor, executor) && input.updateIds.every(updateId => {
|
|
2761
|
+
const evidence = inspect(updateId);
|
|
2762
|
+
return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId &&
|
|
2763
|
+
evidence.operatorAuthorityId === `telegram-owner:${operator}`;
|
|
2764
|
+
}) && authority.isCurrent();
|
|
2765
|
+
return advanceTemporary(expected, { ...authority, isCurrent: current }, (entry, file) => {
|
|
2766
|
+
if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(value => isDeepStrictEqual(value, input)) ||
|
|
2767
|
+
entry.completedInputs?.some(value => isDeepStrictEqual(value, input)))
|
|
2768
|
+
return false;
|
|
2769
|
+
const cancelled = entry.cancelledInputs ?? [];
|
|
2770
|
+
if (cancelled.some(value => isDeepStrictEqual(value, input)))
|
|
2771
|
+
return "unchanged";
|
|
2772
|
+
const operations = file.operations.filter(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
|
|
2773
|
+
request.source.updateIds.some(id => input.updateIds.includes(id)));
|
|
2774
|
+
if (operations.some(({ request }) => !request.source.updateIds.every(id => input.updateIds.includes(id))))
|
|
2775
|
+
return false;
|
|
2776
|
+
// Forget only expired donor intent; canonical binding, recipient queue and immutable settlement proofs are untouched.
|
|
2777
|
+
file.operations = file.operations.filter(value => !operations.includes(value));
|
|
2778
|
+
// Once bound, this is no longer a disposable tab. Forget its temporary frame, not the binding or other journal sources.
|
|
2779
|
+
if (bound) {
|
|
2780
|
+
file.temporaryThreads = file.temporaryThreads?.filter(value => value !== entry);
|
|
2781
|
+
if (!file.temporaryThreads?.length)
|
|
2782
|
+
delete file.temporaryThreads;
|
|
2783
|
+
return true;
|
|
2784
|
+
}
|
|
2785
|
+
const forwarded = entry.forwardedInputs?.filter(value => !isDeepStrictEqual(value, input));
|
|
2786
|
+
if (forwarded?.length)
|
|
2787
|
+
entry.forwardedInputs = forwarded;
|
|
2788
|
+
else
|
|
2789
|
+
delete entry.forwardedInputs;
|
|
2790
|
+
entry.cancelledInputs = [...cancelled, input];
|
|
2791
|
+
return true;
|
|
2792
|
+
});
|
|
2793
|
+
},
|
|
2753
2794
|
recordTemporaryThreadForwardIssued(expected, inputValue, authority) {
|
|
2754
2795
|
const input = structuredClone(inputValue);
|
|
2755
2796
|
if (!isTemporaryThreadInput(input))
|
|
@@ -2808,7 +2849,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2808
2849
|
return entry;
|
|
2809
2850
|
});
|
|
2810
2851
|
},
|
|
2811
|
-
forgetPreviousWorld(authority) {
|
|
2852
|
+
forgetPreviousWorld(authority, preserveTemporaryTokens = []) {
|
|
2812
2853
|
if (!authority.isCurrent())
|
|
2813
2854
|
return undefined;
|
|
2814
2855
|
const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
|
|
@@ -2817,13 +2858,14 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2817
2858
|
const current = () => authority.isCurrent() && authority.operatorUserId === operator && isDeepStrictEqual(authority.executor, executor);
|
|
2818
2859
|
const previous = (value) => value.operatorUserId === operator && value.executor.instanceId !== executor.instanceId;
|
|
2819
2860
|
const before = storage.read(), file = structuredClone(before);
|
|
2820
|
-
const
|
|
2861
|
+
const forgetTemporary = (value) => previous(value) && !preserveTemporaryTokens.includes(value.token);
|
|
2862
|
+
const operations = file.operations.filter(previous), temporaryThreads = (file.temporaryThreads ?? []).filter(forgetTemporary);
|
|
2821
2863
|
if (!current())
|
|
2822
2864
|
return undefined;
|
|
2823
2865
|
if (!operations.length && !temporaryThreads.length)
|
|
2824
2866
|
return { operations, temporaryThreads };
|
|
2825
2867
|
file.operations = file.operations.filter(value => !previous(value));
|
|
2826
|
-
const remaining = (file.temporaryThreads ?? []).filter(value => !
|
|
2868
|
+
const remaining = (file.temporaryThreads ?? []).filter(value => !forgetTemporary(value));
|
|
2827
2869
|
if (remaining.length)
|
|
2828
2870
|
file.temporaryThreads = remaining;
|
|
2829
2871
|
else
|
package/dist/lib/updates.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { type TelegramTarget } from "./target.ts";
|
|
7
7
|
import type { TelegramBusEnvelope, TelegramBusFollowerView, TelegramBusForwardOwnership, TelegramBusForeignUpdateSettlement, TelegramProcessLiveness } from "./bus.ts";
|
|
8
8
|
import type { TelegramMessageOwnershipStore } from "./ownership.ts";
|
|
9
|
-
import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, type TelegramUpdateJournalEntryDigest, type TelegramUpdateJournalSourceCompletion, type TelegramUpdateJournalQueuedCompletion, type TelegramUpdateJournalQueuedReceiptEvidence, type TelegramInputJournalReceipt, type TelegramInputJournalSourceReference, type TelegramInputJournalStore, type TelegramJournaledUpdate, type TelegramUpdateJournalDeadQueueOwnerRecoveryResult, type TelegramUpdateJournalAppendResult, type TelegramUpdateJournalInputClaim, type TelegramUpdateJournalOperatorDispositionInput, type TelegramUpdateJournalOperatorDispositionResult, type TelegramUpdateJournalPendingAbandonmentInput, type TelegramUpdateJournalPendingAbandonmentResult, type TelegramUpdateJournalPendingRetentionEvidence, type TelegramUpdateJournalEntry, type TelegramUpdateJournalRoutingInput, type TelegramRoutingInputJournal, type TelegramUpdateJournalQueueDiscardResult, type TelegramUpdateJournalQueueHandoffAcceptResult, type TelegramUpdateJournalQueueHandoffCancelResult, type TelegramUpdateJournalQueueHandoffInput, type TelegramUpdateJournalQueueHandoffOfferResult, type TelegramUpdateJournalQueueOwner, type TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
|
|
9
|
+
import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, type TelegramUpdateJournalEntryDigest, type TelegramUpdateJournalSourceCompletion, type TelegramUpdateJournalQueuedCompletion, type TelegramUpdateJournalQueuedReceiptEvidence, type TelegramInputJournalReceipt, type TelegramInputJournalSourceReference, type TelegramInputJournalStore, type TelegramJournaledUpdate, type TelegramUpdateJournalDeadQueueOwnerRecoveryResult, type TelegramUpdateJournalAppendResult, type TelegramUpdateJournalInputClaim, type TelegramUpdateJournalOperatorDispositionInput, type TelegramUpdateJournalOperatorDispositionResult, type TelegramUpdateJournalPendingAbandonmentInput, type TelegramUpdateJournalPendingAbandonmentResult, type TelegramUpdateJournalPendingRetentionEvidence, type TelegramUpdateJournalEntry, type TelegramUpdateJournalRoutingInput, type TelegramRoutingInputJournal, type TelegramRoutingInputExpiryResult, type TelegramUpdateJournalQueueDiscardResult, type TelegramUpdateJournalQueueHandoffAcceptResult, type TelegramUpdateJournalQueueHandoffCancelResult, type TelegramUpdateJournalQueueHandoffInput, type TelegramUpdateJournalQueueHandoffOfferResult, type TelegramUpdateJournalQueueOwner, type TelegramUpdateJournalQueueOwnerIdentity } from "./journal.ts";
|
|
10
10
|
import { type PendingTelegramControlItem, type TelegramControlQueueHandoffPayload, type TelegramQueueAdmissionReceipt, type TelegramQueueHandoffPayload, type TelegramQueueHandoffStageResult, type TelegramQueueReactionDisposition, type TelegramQueueHandoffStagingRuntime, type TelegramQueueItem } from "./queue.ts";
|
|
11
11
|
import { type TelegramAuthorizationState, type TelegramUserPairingRuntimeDeps } from "./config.ts";
|
|
12
12
|
export interface TelegramReactionTypeEmoji {
|
|
@@ -189,6 +189,8 @@ export interface TelegramHeldSourcePreparation<TContext> {
|
|
|
189
189
|
/** Exact prepared worker owner, context, key and authority; the consumer adds its own domain fences. */
|
|
190
190
|
isCurrent: () => boolean;
|
|
191
191
|
signal: AbortSignal;
|
|
192
|
+
/** Chooser-clock sources that must survive cold spending until their fixed expiry. */
|
|
193
|
+
routingSourceIds?: readonly number[];
|
|
192
194
|
}
|
|
193
195
|
export interface TelegramDeferredAbandonmentRecoveryPage {
|
|
194
196
|
sources: {
|
|
@@ -504,7 +506,7 @@ export interface TelegramRoutingInputExpirySource {
|
|
|
504
506
|
original: TelegramUpdateJournalEntry;
|
|
505
507
|
journalBindingKey: string;
|
|
506
508
|
isCurrent(): boolean;
|
|
507
|
-
expire():
|
|
509
|
+
expire(): TelegramRoutingInputExpiryResult | undefined;
|
|
508
510
|
}
|
|
509
511
|
export interface TelegramUpdateWorkerJournalPort {
|
|
510
512
|
routingInputs?: TelegramRoutingInputJournal;
|
|
@@ -585,7 +587,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
|
585
587
|
getRecipientBindingKey?: () => string | undefined;
|
|
586
588
|
/** True selects legacy historical review/spending; retain protects unsupported originals without disposition authority. */
|
|
587
589
|
shouldReviewHistoricalInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | "retain" | Promise<boolean | "retain">;
|
|
588
|
-
/** New-world restart: spend
|
|
590
|
+
/** New-world restart: spend unclocked classified routing input without delivery, copy or completion. Chooser clocks wait for expiry; interrupted private abandonment keeps exact recovery. */
|
|
589
591
|
spendHistoricalInput?: boolean;
|
|
590
592
|
/** Holds protected live or retry sources before execution; never cancels or disposes them. */
|
|
591
593
|
shouldHoldPendingInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
@@ -633,6 +635,10 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
633
635
|
operatorUserId: number;
|
|
634
636
|
sourceUpdateIds: readonly number[];
|
|
635
637
|
}) => boolean;
|
|
638
|
+
isRoutingInputCurrent?: (input: {
|
|
639
|
+
updateId: number;
|
|
640
|
+
signal: AbortSignal;
|
|
641
|
+
}) => boolean;
|
|
636
642
|
supportsDeferredAbandonment?: (input: {
|
|
637
643
|
updateId: number;
|
|
638
644
|
signal: AbortSignal;
|
|
@@ -744,6 +750,7 @@ export interface TelegramUpdateAdmissionHandleDeps<TUpdate extends TelegramUpdat
|
|
|
744
750
|
abandonDeferred?: TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"];
|
|
745
751
|
armRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["armRoutingInput"];
|
|
746
752
|
selectRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["selectRoutingInput"];
|
|
753
|
+
isRoutingInputCurrent?: TelegramUpdateWorkerRuntime<TContext>["isRoutingInputCurrent"];
|
|
747
754
|
supportsDeferredAbandonment?: TelegramUpdateWorkerRuntime<TContext>["supportsDeferredAbandonment"];
|
|
748
755
|
inspectAbandoning?: TelegramUpdateWorkerRuntime<TContext>["inspectAbandoning"];
|
|
749
756
|
inspectHistorical?: TelegramUpdateWorkerRuntime<TContext>["inspectHistorical"];
|
|
@@ -1135,7 +1142,7 @@ export declare function executeTelegramCustodiedInput(input: {
|
|
|
1135
1142
|
export declare function createTelegramUpdateAdmissionHandle<TUpdate extends TelegramUpdateFlow & {
|
|
1136
1143
|
update_id: number;
|
|
1137
1144
|
}, TContext>(deps: TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>): (update: TUpdate, ctx: TContext, signal: AbortSignal) => Promise<TelegramUpdateAdmissionOutcome>;
|
|
1138
|
-
export declare function createTelegramCustodiedUpdateAdmissionHandle<TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow, TContext>(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>, "onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
1145
|
+
export declare function createTelegramCustodiedUpdateAdmissionHandle<TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow, TContext>(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>, "onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "isRoutingInputCurrent" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
1139
1146
|
journal: TelegramCustodyExecutionJournal;
|
|
1140
1147
|
recipientBindingKey: string;
|
|
1141
1148
|
onLateOutcomeError(error: unknown, updateId: number): void;
|