@llblab/pi-telegram 0.52.0 → 0.52.1
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 +5 -4
- package/BACKLOG.md +16 -1
- package/CHANGELOG.md +6 -0
- package/README.md +3 -2
- package/dist/lib/bindings.js +1 -0
- package/dist/lib/bus-api.js +4 -3
- package/dist/lib/extension.js +3 -0
- package/dist/lib/journal.d.ts +7 -1
- package/dist/lib/journal.js +72 -11
- 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/telegram-api.d.ts +2 -0
- package/dist/lib/telegram-api.js +4 -0
- 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 +9 -7
- package/docs/ui-style.md +1 -1
- package/lib/bindings.ts +1 -0
- package/lib/bus-api.ts +4 -2
- package/lib/extension.ts +4 -0
- package/lib/journal.ts +80 -11
- package/lib/queue.ts +26 -3
- package/lib/routing.ts +91 -36
- package/lib/telegram-api.ts +5 -0
- 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) => {
|
|
@@ -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
|
@@ -795,6 +795,10 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
|
|
|
795
795
|
}
|
|
796
796
|
return targetPath;
|
|
797
797
|
}
|
|
798
|
+
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
799
|
+
export function formatTelegramCallbackAnswerText(text) {
|
|
800
|
+
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
801
|
+
}
|
|
798
802
|
export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
|
|
799
803
|
try {
|
|
800
804
|
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;
|
package/dist/lib/updates.js
CHANGED
|
@@ -1701,24 +1701,34 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1701
1701
|
return false;
|
|
1702
1702
|
let nextAtMs, changed = false;
|
|
1703
1703
|
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
|
|
1704
|
-
// Lost
|
|
1704
|
+
// Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
|
|
1705
1705
|
const candidates = [...snapshot.entries, ...[...deferredSources.values()]
|
|
1706
1706
|
.filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
|
|
1707
1707
|
.map(source => source.entry)];
|
|
1708
1708
|
for (const entry of candidates) {
|
|
1709
1709
|
const lifetime = entry.routingInput;
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1710
|
+
if (!lifetime || entry.state !== "pending" || !binding || claims.get(entry.updateId) === "queued" ||
|
|
1711
|
+
unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
|
|
1712
1712
|
continue;
|
|
1713
|
-
|
|
1713
|
+
// Classify the first startup snapshot before constructing any historical expiry carrier.
|
|
1714
|
+
if (expectedOwner.startupUpdateIds?.has(entry.updateId) && !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput) {
|
|
1715
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
|
|
1714
1716
|
continue;
|
|
1717
|
+
}
|
|
1715
1718
|
if (getNowMs() < lifetime.expiresAtMs) {
|
|
1716
1719
|
nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs);
|
|
1717
1720
|
continue;
|
|
1718
1721
|
}
|
|
1719
1722
|
const existing = deferredSources.get(entry.updateId);
|
|
1720
|
-
if (existing && !isDeepStrictEqual(existing.entry, entry))
|
|
1721
|
-
|
|
1723
|
+
if (existing && !isDeepStrictEqual(existing.entry, entry)) {
|
|
1724
|
+
// Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
|
|
1725
|
+
const previous = existing.entry.routingInput;
|
|
1726
|
+
if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
|
|
1727
|
+
(previous && (!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
|
|
1728
|
+
(previous.phase === "selected" && lifetime.phase !== "selected"))))
|
|
1729
|
+
continue;
|
|
1730
|
+
existing.entry = structuredClone(entry);
|
|
1731
|
+
}
|
|
1722
1732
|
const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true };
|
|
1723
1733
|
deferredSources.set(entry.updateId, source);
|
|
1724
1734
|
if (!claims.has(entry.updateId))
|
|
@@ -1738,13 +1748,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1738
1748
|
return { ...committed, duplicate: true };
|
|
1739
1749
|
claims.set(entry.updateId, "abandoning");
|
|
1740
1750
|
committed = expireRouting({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
|
|
1751
|
+
// Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
|
|
1741
1752
|
claims.delete(entry.updateId);
|
|
1742
1753
|
deferredSources.delete(entry.updateId);
|
|
1743
1754
|
state.journalEntryCount = committed.entryCount;
|
|
1744
1755
|
state.journalSerializedBytes = committed.serializedBytes;
|
|
1745
1756
|
updateClaimCounts();
|
|
1746
1757
|
notifyStateChange();
|
|
1747
|
-
changed
|
|
1758
|
+
changed ||= !committed.duplicate;
|
|
1748
1759
|
return committed;
|
|
1749
1760
|
} }, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
1750
1761
|
}
|
|
@@ -1964,7 +1975,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1964
1975
|
transition("deferred", entry.updateId);
|
|
1965
1976
|
continue;
|
|
1966
1977
|
}
|
|
1967
|
-
if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
1978
|
+
if (review && historical && spendHistorical && !entry.routingInput && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
1968
1979
|
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
1969
1980
|
continue;
|
|
1970
1981
|
}
|
|
@@ -1975,10 +1986,6 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1975
1986
|
}
|
|
1976
1987
|
}
|
|
1977
1988
|
if (entry.state === "pending" && entry.routingInput) {
|
|
1978
|
-
if (historical && spendHistorical) {
|
|
1979
|
-
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
1980
|
-
continue;
|
|
1981
|
-
}
|
|
1982
1989
|
claims.set(entry.updateId, "historical");
|
|
1983
1990
|
transition("deferred", entry.updateId);
|
|
1984
1991
|
continue;
|
|
@@ -2123,7 +2130,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2123
2130
|
expectedOwner.heldSourcesPreparedIssued = true;
|
|
2124
2131
|
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
2125
2132
|
void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
|
|
2126
|
-
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent
|
|
2133
|
+
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent,
|
|
2134
|
+
routingSourceIds: [...deferredSources].filter(([, source]) => source.journalBindingKey === key && !!source.entry.routingInput)
|
|
2135
|
+
.map(([id]) => id) }) : undefined).catch(error => {
|
|
2127
2136
|
try {
|
|
2128
2137
|
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
|
|
2129
2138
|
}
|
|
@@ -2286,6 +2295,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2286
2295
|
},
|
|
2287
2296
|
armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
|
|
2288
2297
|
selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
|
|
2298
|
+
isRoutingInputCurrent(input) {
|
|
2299
|
+
return owner?.controller.signal === input.signal && !input.signal.aborted &&
|
|
2300
|
+
deferredSources.has(input.updateId) && claims.get(input.updateId) !== "abandoning";
|
|
2301
|
+
},
|
|
2289
2302
|
inspectDeferredSource(input) {
|
|
2290
2303
|
const expectedOwner = owner;
|
|
2291
2304
|
const source = deferredSources.get(input.updateId);
|
|
@@ -2346,7 +2359,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2346
2359
|
return;
|
|
2347
2360
|
}
|
|
2348
2361
|
const claim = claims.get(input.updateId);
|
|
2349
|
-
if (claim === "abandoning" || claim === "historical" || claim === "retained"
|
|
2362
|
+
if (claim === "abandoning" || claim === "historical" || claim === "retained" ||
|
|
2363
|
+
(!claim && routingJournal?.inspectExpiry(input.updateId)))
|
|
2350
2364
|
return;
|
|
2351
2365
|
if (input.outcome.kind === "complete") {
|
|
2352
2366
|
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
@@ -3551,9 +3565,10 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3551
3565
|
generation,
|
|
3552
3566
|
updateId: update.update_id,
|
|
3553
3567
|
signal,
|
|
3554
|
-
isCurrent: () => !signal.aborted && !suspended
|
|
3568
|
+
isCurrent: () => !signal.aborted && !suspended && ((!routingLifetime && !routingClockUnknown) || immediate || settlementReported ||
|
|
3569
|
+
deps.isRoutingInputCurrent?.({ updateId: update.update_id, signal }) !== false),
|
|
3555
3570
|
assertCurrent() {
|
|
3556
|
-
if (
|
|
3571
|
+
if (!execution.isCurrent()) {
|
|
3557
3572
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
3558
3573
|
}
|
|
3559
3574
|
},
|
|
@@ -3567,7 +3582,7 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3567
3582
|
let settlementReported = false;
|
|
3568
3583
|
let outcome;
|
|
3569
3584
|
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
3570
|
-
if (
|
|
3585
|
+
if (!execution.isCurrent())
|
|
3571
3586
|
return;
|
|
3572
3587
|
if (next.kind === "deferred" && next.routingReview) {
|
|
3573
3588
|
if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
@@ -3586,7 +3601,7 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3586
3601
|
}
|
|
3587
3602
|
void Promise.resolve()
|
|
3588
3603
|
.then(() => {
|
|
3589
|
-
if (
|
|
3604
|
+
if (!execution.isCurrent())
|
|
3590
3605
|
return;
|
|
3591
3606
|
return deps.onLateOutcome(next, {
|
|
3592
3607
|
updateId: update.update_id,
|
|
@@ -4487,6 +4502,7 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
4487
4502
|
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
4488
4503
|
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
4489
4504
|
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
4505
|
+
isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
|
|
4490
4506
|
supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
|
|
4491
4507
|
inspectAbandoning: input => worker?.inspectAbandoning?.(input),
|
|
4492
4508
|
inspectHistorical: input => worker?.inspectHistorical?.(input),
|