@llblab/pi-kit 0.35.0 → 0.35.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/BACKLOG.md +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -6
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +22 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +75 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +11 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +30 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +61 -46
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +36 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +18 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +33 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +31 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +12 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +21 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +26 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +57 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +6 -2
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +111 -15
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +30 -21
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +64 -56
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +122 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +22 -3
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +44 -10
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +40 -7
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -10
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +37 -20
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +69 -17
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -111,6 +111,7 @@ import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
|
111
111
|
const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
112
112
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
113
113
|
const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
114
|
+
const TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS = 1_500;
|
|
114
115
|
const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
|
|
115
116
|
export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
|
|
116
117
|
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
@@ -326,6 +327,20 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
|
|
|
326
327
|
timeoutMs?: number;
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
/** Structural port for run-collapsing diagnostics, implemented by the logging domain. */
|
|
331
|
+
export interface TelegramFailureRunLogPort {
|
|
332
|
+
record: (error: unknown, details?: Record<string, unknown>) => boolean;
|
|
333
|
+
settled: () => TelegramFailureRunSummaryPort | undefined;
|
|
334
|
+
reset: () => void;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface TelegramFailureRunSummaryPort {
|
|
338
|
+
signature: string;
|
|
339
|
+
failedAttempts: number;
|
|
340
|
+
suppressedFailures: number;
|
|
341
|
+
failedForMs: number;
|
|
342
|
+
}
|
|
343
|
+
|
|
329
344
|
export interface TelegramBusFollowerApiCallerDeps {
|
|
330
345
|
socketPath: TelegramBusSocketPathSource;
|
|
331
346
|
instanceId: string;
|
|
@@ -337,11 +352,23 @@ export interface TelegramBusFollowerApiCallerDeps {
|
|
|
337
352
|
) => Promise<string | undefined>;
|
|
338
353
|
getNowMs?: () => number;
|
|
339
354
|
timeoutMs?: number;
|
|
355
|
+
/** Receives one diagnostic per call slower than `slowCallMs`; observation only. */
|
|
356
|
+
recordRuntimeEvent?: (
|
|
357
|
+
category: string,
|
|
358
|
+
error: unknown,
|
|
359
|
+
details?: Record<string, unknown>,
|
|
360
|
+
) => void;
|
|
361
|
+
slowCallMs?: number;
|
|
340
362
|
}
|
|
341
363
|
|
|
342
364
|
export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
343
365
|
TContext extends { cwd?: string },
|
|
344
366
|
> {
|
|
367
|
+
/**
|
|
368
|
+
* Collapses one sustained heartbeat failure run. Optional at this unit boundary, where an absent port records
|
|
369
|
+
* every failure; the runtime assembly requires it so production always supplies the logging domain's owner.
|
|
370
|
+
*/
|
|
371
|
+
failureLog?: TelegramFailureRunLogPort;
|
|
345
372
|
instanceId: string;
|
|
346
373
|
createRequestId: () => string;
|
|
347
374
|
protocolIdentity: TelegramBusProtocolIdentity;
|
|
@@ -673,6 +700,8 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
|
|
|
673
700
|
TContext extends { cwd?: string },
|
|
674
701
|
> {
|
|
675
702
|
instanceId: string;
|
|
703
|
+
/** Collapses one sustained heartbeat failure run; the composition root supplies the logging domain's owner. */
|
|
704
|
+
failureLog: TelegramFailureRunLogPort;
|
|
676
705
|
registrationState: TelegramBusFollowerRegistrationState;
|
|
677
706
|
recordRuntimeEvent: (
|
|
678
707
|
category: string,
|
|
@@ -789,6 +818,7 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
789
818
|
});
|
|
790
819
|
registration = createTelegramBusFollowerRegistrationRuntime({
|
|
791
820
|
...ports.registration,
|
|
821
|
+
failureLog: ports.failureLog,
|
|
792
822
|
...sharedRuntimeDeps,
|
|
793
823
|
startReceiving: receiver.start,
|
|
794
824
|
stopReceiving: receiver.stop,
|
|
@@ -1495,6 +1525,7 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
1495
1525
|
instanceId: deps.instanceId,
|
|
1496
1526
|
getAuthSecret: deps.getApiAuthSecret,
|
|
1497
1527
|
getRegistrationGeneration: deps.getRegistrationGeneration,
|
|
1528
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1498
1529
|
}),
|
|
1499
1530
|
agentMessages: createTelegramBusAgentMessageClient({
|
|
1500
1531
|
...sharedClientDeps,
|
|
@@ -1881,7 +1912,13 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
1881
1912
|
): (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown> {
|
|
1882
1913
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1883
1914
|
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1884
|
-
|
|
1915
|
+
const slowCallMs = deps.slowCallMs ?? TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS;
|
|
1916
|
+
const caller = async (
|
|
1917
|
+
method: string,
|
|
1918
|
+
args: unknown[],
|
|
1919
|
+
localOptions: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined,
|
|
1920
|
+
timing: { registeredAtMs?: number },
|
|
1921
|
+
): Promise<unknown> => {
|
|
1885
1922
|
const assertion = localOptions?.assertAuthority;
|
|
1886
1923
|
const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
|
|
1887
1924
|
assertCurrent();
|
|
@@ -1889,6 +1926,7 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
1889
1926
|
deps,
|
|
1890
1927
|
timeoutMs,
|
|
1891
1928
|
);
|
|
1929
|
+
timing.registeredAtMs = getNowMs();
|
|
1892
1930
|
assertCurrent();
|
|
1893
1931
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
1894
1932
|
let response: TelegramBusEnvelope | undefined;
|
|
@@ -1950,6 +1988,45 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
1950
1988
|
}
|
|
1951
1989
|
throw new Error(message ?? "Telegram bus API call failed.");
|
|
1952
1990
|
};
|
|
1991
|
+
return async (method, args, localOptions) => {
|
|
1992
|
+
const startedAtMs = getNowMs();
|
|
1993
|
+
const timing: { registeredAtMs?: number } = {};
|
|
1994
|
+
let outcome = "ok";
|
|
1995
|
+
try {
|
|
1996
|
+
return await caller(method, args, localOptions, timing);
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
outcome = "error";
|
|
1999
|
+
throw error;
|
|
2000
|
+
} finally {
|
|
2001
|
+
const elapsedMs = getNowMs() - startedAtMs;
|
|
2002
|
+
if (elapsedMs >= slowCallMs) {
|
|
2003
|
+
const apiMethod =
|
|
2004
|
+
(method === "call" || method === "callMultipart") &&
|
|
2005
|
+
typeof args[0] === "string"
|
|
2006
|
+
? args[0]
|
|
2007
|
+
: method;
|
|
2008
|
+
try {
|
|
2009
|
+
deps.recordRuntimeEvent?.(
|
|
2010
|
+
"bus",
|
|
2011
|
+
`Telegram bus follower API call was slow (${elapsedMs}ms).`,
|
|
2012
|
+
{
|
|
2013
|
+
phase: "follower-api-slow",
|
|
2014
|
+
method: apiMethod,
|
|
2015
|
+
elapsedMs,
|
|
2016
|
+
registrationWaitMs:
|
|
2017
|
+
(timing.registeredAtMs ?? getNowMs()) - startedAtMs,
|
|
2018
|
+
...(timing.registeredAtMs === undefined
|
|
2019
|
+
? {}
|
|
2020
|
+
: { transportMs: getNowMs() - timing.registeredAtMs }),
|
|
2021
|
+
outcome,
|
|
2022
|
+
},
|
|
2023
|
+
);
|
|
2024
|
+
} catch {
|
|
2025
|
+
// Diagnostics cannot change the API call outcome.
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
1953
2030
|
}
|
|
1954
2031
|
|
|
1955
2032
|
async function resolveTelegramBusFollowerRegistration(
|
|
@@ -2521,10 +2598,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
2521
2598
|
election: TelegramBusFollowerElection,
|
|
2522
2599
|
) => {
|
|
2523
2600
|
if (!isCurrent()) return;
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2601
|
+
if (deps.getActiveContext) {
|
|
2602
|
+
// Pi hands every event and command a fresh context object, so a manually connected follower never holds
|
|
2603
|
+
// the stored session object. Promote through the live session context instead of rescheduling: heartbeat
|
|
2604
|
+
// failures re-request recovery faster than the retry grace, so a reschedule-only path never fires.
|
|
2605
|
+
const activeCtx = deps.getActiveContext();
|
|
2606
|
+
if (!activeCtx) {
|
|
2607
|
+
scheduleRecovery(reason, ctx, binding);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
ctx = activeCtx;
|
|
2528
2611
|
}
|
|
2529
2612
|
deps.setLifecyclePhase("electing");
|
|
2530
2613
|
safeUpdateStatus(ctx);
|
|
@@ -2765,6 +2848,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2765
2848
|
(!deps.registrationState ||
|
|
2766
2849
|
deps.registrationState.getGeneration() === generation);
|
|
2767
2850
|
const stopHeartbeat = () => {
|
|
2851
|
+
// A stopped heartbeat ends the current failure run, so a later run logs its own first failure.
|
|
2852
|
+
heartbeatFailureLog.reset();
|
|
2768
2853
|
if (!heartbeatInterval) return;
|
|
2769
2854
|
clearInterval(heartbeatInterval);
|
|
2770
2855
|
heartbeatInterval = undefined;
|
|
@@ -2793,6 +2878,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2793
2878
|
}
|
|
2794
2879
|
});
|
|
2795
2880
|
};
|
|
2881
|
+
/** A sustained leader outage repeats one identical failure every interval; the port collapses the run. */
|
|
2882
|
+
const heartbeatFailureLog: TelegramFailureRunLogPort =
|
|
2883
|
+
deps.failureLog ?? {
|
|
2884
|
+
record(error) {
|
|
2885
|
+
try {
|
|
2886
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
2887
|
+
phase: "follower-heartbeat",
|
|
2888
|
+
});
|
|
2889
|
+
} catch {
|
|
2890
|
+
// Diagnostics cannot replace heartbeat recovery.
|
|
2891
|
+
}
|
|
2892
|
+
return true;
|
|
2893
|
+
},
|
|
2894
|
+
settled: () => undefined,
|
|
2895
|
+
reset: () => undefined,
|
|
2896
|
+
};
|
|
2796
2897
|
const sendHeartbeat = async () => {
|
|
2797
2898
|
const leaderSocketPath = activeLeaderSocketPath;
|
|
2798
2899
|
const registrationGeneration = activeRegistrationGeneration;
|
|
@@ -2818,6 +2919,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2818
2919
|
);
|
|
2819
2920
|
if (!isCurrentHeartbeat()) return;
|
|
2820
2921
|
if (response?.kind === "bus.ack" && response.ok) {
|
|
2922
|
+
heartbeatFailureLog.settled();
|
|
2821
2923
|
const heartbeatResult = isRecord(response.result)
|
|
2822
2924
|
? response.result
|
|
2823
2925
|
: undefined;
|
|
@@ -2850,13 +2952,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2850
2952
|
}
|
|
2851
2953
|
} catch (error) {
|
|
2852
2954
|
if (!isCurrentHeartbeat()) return;
|
|
2853
|
-
|
|
2854
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
2855
|
-
phase: "follower-heartbeat",
|
|
2856
|
-
});
|
|
2857
|
-
} catch {
|
|
2858
|
-
// Diagnostics cannot replace heartbeat recovery.
|
|
2859
|
-
}
|
|
2955
|
+
heartbeatFailureLog.record(error);
|
|
2860
2956
|
if (!heartbeatContext) return;
|
|
2861
2957
|
try {
|
|
2862
2958
|
await deps.onHeartbeatFailure?.(error, heartbeatContext);
|
|
@@ -3120,11 +3216,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
3120
3216
|
return false;
|
|
3121
3217
|
},
|
|
3122
3218
|
setContext(ctx) {
|
|
3219
|
+
// Pi hands every event a fresh context object. Only a replaced session drops an in-flight attempt.
|
|
3123
3220
|
if (
|
|
3124
3221
|
registrationAttempt &&
|
|
3125
|
-
(registrationAttempt.
|
|
3126
|
-
|
|
3127
|
-
deps.getSessionGeneration?.() ||
|
|
3222
|
+
(registrationAttempt.sessionGeneration !==
|
|
3223
|
+
deps.getSessionGeneration?.() ||
|
|
3128
3224
|
registrationAttempt.sessionId !== deps.getSessionId?.(ctx))
|
|
3129
3225
|
)
|
|
3130
3226
|
registrationAttempt = undefined;
|
|
@@ -167,6 +167,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
167
167
|
callApi: <TResponse>(
|
|
168
168
|
method: string,
|
|
169
169
|
body: Record<string, unknown>,
|
|
170
|
+
options?: TelegramApiCallOptions,
|
|
170
171
|
) => Promise<TResponse>;
|
|
171
172
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
172
173
|
getSyncState: () => Sync.TelegramSyncState;
|
|
@@ -1925,6 +1926,8 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1925
1926
|
text: connectedAnnouncement.text,
|
|
1926
1927
|
parse_mode: connectedAnnouncement.parseMode,
|
|
1927
1928
|
},
|
|
1929
|
+
// A deleted Thread is this probe's expected answer; the recovery below records it.
|
|
1930
|
+
{ isExpectedError: Threads.isTelegramTopicTargetStaleError },
|
|
1928
1931
|
);
|
|
1929
1932
|
assertRestoreRegistration();
|
|
1930
1933
|
if (recoverableTarget || probeRequiredRecord) {
|
|
@@ -1968,7 +1971,19 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1968
1971
|
error instanceof Error ? error.message : String(error),
|
|
1969
1972
|
);
|
|
1970
1973
|
await deps.topicTargetStore.persist();
|
|
1974
|
+
const staleTarget = result.target;
|
|
1971
1975
|
result = await provisionTarget();
|
|
1976
|
+
deps.recordRuntimeEvent(
|
|
1977
|
+
"telegram",
|
|
1978
|
+
"Telegram follower restore probe found its Thread deleted; re-created it.",
|
|
1979
|
+
{
|
|
1980
|
+
phase: "topic-lifecycle-restore-probe-recreated",
|
|
1981
|
+
instanceId: registration.instanceId,
|
|
1982
|
+
chatId: staleTarget.chatId,
|
|
1983
|
+
staleThreadId: staleTarget.threadId,
|
|
1984
|
+
threadId: result.target.threadId,
|
|
1985
|
+
},
|
|
1986
|
+
);
|
|
1972
1987
|
connectedAnnouncement =
|
|
1973
1988
|
createTelegramBusInstanceLifecycleAnnouncement({
|
|
1974
1989
|
target: result.target,
|
|
@@ -3297,33 +3297,42 @@ export async function sendTelegramBusLocalEnvelope(
|
|
|
3297
3297
|
};
|
|
3298
3298
|
const attempts = Math.max(1, resolvedOptions.retry?.attempts ?? 1);
|
|
3299
3299
|
const delayMs = Math.max(0, resolvedOptions.retry?.delayMs ?? 0);
|
|
3300
|
+
// Log the first failure, then only the outcome: a reloading leader would otherwise log every attempt.
|
|
3300
3301
|
for (let attempt = 1; ; attempt += 1) {
|
|
3301
3302
|
try {
|
|
3302
|
-
|
|
3303
|
+
const response = await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
|
|
3304
|
+
if (attempt > 1)
|
|
3305
|
+
resolvedOptions.recordTransportEvent?.("client-recovered", {
|
|
3306
|
+
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
3307
|
+
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
3308
|
+
attempt,
|
|
3309
|
+
attempts,
|
|
3310
|
+
});
|
|
3311
|
+
return response;
|
|
3303
3312
|
} catch (error) {
|
|
3304
3313
|
const info = classifyTelegramBusTransportError(error);
|
|
3305
|
-
|
|
3306
|
-
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
3307
|
-
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
3308
|
-
attempt,
|
|
3309
|
-
attempts,
|
|
3310
|
-
...info,
|
|
3311
|
-
});
|
|
3312
|
-
if (
|
|
3314
|
+
const final =
|
|
3313
3315
|
error instanceof TelegramBusLocalAuthorityError ||
|
|
3314
3316
|
attempt >= attempts ||
|
|
3315
|
-
!isRetryableTelegramBusTransportError(error)
|
|
3316
|
-
)
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3317
|
+
!isRetryableTelegramBusTransportError(error);
|
|
3318
|
+
if (attempt === 1 || final)
|
|
3319
|
+
resolvedOptions.recordTransportEvent?.("client-failed", {
|
|
3320
|
+
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
3321
|
+
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
3322
|
+
attempt,
|
|
3323
|
+
attempts,
|
|
3324
|
+
...info,
|
|
3325
|
+
});
|
|
3326
|
+
if (final) throw error;
|
|
3327
|
+
if (attempt === 1)
|
|
3328
|
+
resolvedOptions.recordTransportEvent?.("client-retry", {
|
|
3329
|
+
...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
|
|
3330
|
+
...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
|
|
3331
|
+
attempt,
|
|
3332
|
+
attempts,
|
|
3333
|
+
delayMs,
|
|
3334
|
+
...info,
|
|
3335
|
+
});
|
|
3327
3336
|
await delayTelegramBusTransportRetry(delayMs);
|
|
3328
3337
|
}
|
|
3329
3338
|
}
|
|
@@ -2281,6 +2281,8 @@ export interface TelegramSelectedCommandAdmission {
|
|
|
2281
2281
|
/** Independent exact recipient/target lifetime, including inside captured menu transport adapters. */
|
|
2282
2282
|
assertRecipientCurrent(): void;
|
|
2283
2283
|
reportCompleted(): boolean;
|
|
2284
|
+
/** Optional recipient release barrier after acceptance; rejection suppresses effects without replay. */
|
|
2285
|
+
beforeEffects?(): Promise<void>;
|
|
2284
2286
|
}
|
|
2285
2287
|
|
|
2286
2288
|
/**
|
|
@@ -3068,7 +3070,8 @@ export function createTelegramCommandHandler<
|
|
|
3068
3070
|
? copySelectedMenuCarrier(original)
|
|
3069
3071
|
: copySelectedReplyAnchor(original);
|
|
3070
3072
|
const assertRecipientCurrent = admission.assertRecipientCurrent,
|
|
3071
|
-
report = admission.reportCompleted
|
|
3073
|
+
report = admission.reportCompleted,
|
|
3074
|
+
beforeEffects = admission.beforeEffects;
|
|
3072
3075
|
const effects: TelegramHeldCommandEffects<TContext> | undefined =
|
|
3073
3076
|
parsed.name === "status"
|
|
3074
3077
|
? undefined
|
|
@@ -3100,8 +3103,11 @@ export function createTelegramCommandHandler<
|
|
|
3100
3103
|
throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
|
|
3101
3104
|
};
|
|
3102
3105
|
return issueSelectedOnce(assertCurrent, async () => {
|
|
3103
|
-
if (plan.completion === "before-effect"
|
|
3104
|
-
return false;
|
|
3106
|
+
if (plan.completion === "before-effect") {
|
|
3107
|
+
if (!report.call(admission)) return false;
|
|
3108
|
+
if (beforeEffects) await beforeEffects();
|
|
3109
|
+
assertRecipientCurrent();
|
|
3110
|
+
}
|
|
3105
3111
|
return prepared.run({
|
|
3106
3112
|
message,
|
|
3107
3113
|
command: parsed,
|
|
@@ -3137,7 +3143,8 @@ export function createTelegramCommandHandler<
|
|
|
3137
3143
|
from: { ...original.from },
|
|
3138
3144
|
};
|
|
3139
3145
|
const assertRecipientCurrent = admission.assertRecipientCurrent,
|
|
3140
|
-
reportCompleted = admission.reportCompleted
|
|
3146
|
+
reportCompleted = admission.reportCompleted,
|
|
3147
|
+
beforeEffects = admission.beforeEffects;
|
|
3141
3148
|
const assertAdmissionCurrent = createSelectedSemanticGuard(
|
|
3142
3149
|
admission,
|
|
3143
3150
|
() => captured.assertExecutionCurrent?.(original),
|
|
@@ -3163,7 +3170,7 @@ export function createTelegramCommandHandler<
|
|
|
3163
3170
|
);
|
|
3164
3171
|
},
|
|
3165
3172
|
},
|
|
3166
|
-
{ assertSemanticCurrent, assertRecipientCurrent, reportCompleted },
|
|
3173
|
+
{ assertSemanticCurrent, assertRecipientCurrent, reportCompleted, beforeEffects },
|
|
3167
3174
|
),
|
|
3168
3175
|
);
|
|
3169
3176
|
};
|
|
@@ -3607,6 +3614,7 @@ export function createTelegramCommandHandler<
|
|
|
3607
3614
|
const assertSourceCurrent = admission.assertSourceCurrent,
|
|
3608
3615
|
assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
3609
3616
|
const reportCompleted = admission.reportCompleted,
|
|
3617
|
+
beforeEffects = admission.beforeEffects,
|
|
3610
3618
|
assertExecutionCurrent = deps.assertExecutionCurrent;
|
|
3611
3619
|
const assertIssuanceCurrent = () => {
|
|
3612
3620
|
assertSourceCurrent.call(admission);
|
|
@@ -3639,9 +3647,12 @@ export function createTelegramCommandHandler<
|
|
|
3639
3647
|
};
|
|
3640
3648
|
return issueSelectedOnce(
|
|
3641
3649
|
assertIssuanceCurrent,
|
|
3642
|
-
async () =>
|
|
3643
|
-
reportCompleted.call(admission)
|
|
3644
|
-
|
|
3650
|
+
async () => {
|
|
3651
|
+
if (!reportCompleted.call(admission)) return false;
|
|
3652
|
+
if (beforeEffects) await beforeEffects();
|
|
3653
|
+
assertRecipientCurrent();
|
|
3654
|
+
return handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args);
|
|
3655
|
+
},
|
|
3645
3656
|
);
|
|
3646
3657
|
},
|
|
3647
3658
|
});
|
|
@@ -3766,6 +3777,7 @@ async function handleTelegramHelpCommand<
|
|
|
3766
3777
|
assertSemanticCurrent(): void;
|
|
3767
3778
|
assertRecipientCurrent(): void;
|
|
3768
3779
|
reportCompleted(): boolean;
|
|
3780
|
+
beforeEffects?(): Promise<void>;
|
|
3769
3781
|
},
|
|
3770
3782
|
): Promise<boolean> {
|
|
3771
3783
|
const assertSemanticCurrent =
|
|
@@ -3788,6 +3800,7 @@ async function handleTelegramHelpCommand<
|
|
|
3788
3800
|
if (admission) {
|
|
3789
3801
|
assertSemanticCurrent();
|
|
3790
3802
|
if (!admission.reportCompleted()) return false;
|
|
3803
|
+
if (admission.beforeEffects) await admission.beforeEffects();
|
|
3791
3804
|
admission.assertRecipientCurrent();
|
|
3792
3805
|
}
|
|
3793
3806
|
const assertEffectCurrent =
|
|
@@ -861,6 +861,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
861
861
|
const promptDispatchRuntime = Runtime.createTelegramPromptDispatchRuntime({
|
|
862
862
|
lifecycle,
|
|
863
863
|
typing,
|
|
864
|
+
failureLog: runtimeDiagnostics.createFailureLog({
|
|
865
|
+
category: "typing",
|
|
866
|
+
failurePhase: "typing-presence",
|
|
867
|
+
outcomePhase: "typing-presence-recovered",
|
|
868
|
+
label: "typing presence",
|
|
869
|
+
}),
|
|
864
870
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
865
871
|
isThreadedMode() {
|
|
866
872
|
return deliveryTargetPolicyRuntime.getTargetPolicyView().threaded === true;
|
|
@@ -1302,6 +1308,32 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1302
1308
|
delivery: deliveryLifecycleRuntime,
|
|
1303
1309
|
api: telegramApiTargetActivityRuntime,
|
|
1304
1310
|
});
|
|
1311
|
+
const prepareTemporaryThreadJournalInspection = function (
|
|
1312
|
+
target: Targets.TelegramTarget,
|
|
1313
|
+
requiredJournalBindingKeys: readonly string[],
|
|
1314
|
+
ownInputs?: readonly Threads.TelegramTemporaryThreadInput[],
|
|
1315
|
+
): Parameters<typeof Journal.isTelegramThreadCleanupJournalNamespaceClear>[0] | undefined {
|
|
1316
|
+
const binding = resolveTelegramUpdateJournalBinding();
|
|
1317
|
+
const botToken = configStore.getBotToken();
|
|
1318
|
+
if (!binding || !botToken) return undefined;
|
|
1319
|
+
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1320
|
+
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({ botToken, botId: getTelegramBotId() });
|
|
1321
|
+
return {
|
|
1322
|
+
directory: Paths.resolveTelegramTempDir(), profile, botIdentity,
|
|
1323
|
+
pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
|
|
1324
|
+
requiredJournalBindingKeys: [binding.recoveryKey, ...requiredJournalBindingKeys],
|
|
1325
|
+
...(target.threadId !== undefined && ownInputs
|
|
1326
|
+
? { cleanup: { target: { chatId: target.chatId, threadId: target.threadId }, ownInputs } } : {}),
|
|
1327
|
+
limits: { maxDirectoryEntries: 10_000, maxFiles: 4096, maxBytes: 64 * 1024 * 1024,
|
|
1328
|
+
maxEntries: 10_000, maxWork: 100_000 },
|
|
1329
|
+
withSourceReference(path, operation) {
|
|
1330
|
+
return telegramJournalReferenceRegistry.withReference({
|
|
1331
|
+
referenceClass: "operator-disposition",
|
|
1332
|
+
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity }),
|
|
1333
|
+
}, operation);
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
};
|
|
1305
1337
|
const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
|
|
1306
1338
|
configStore,
|
|
1307
1339
|
callApi: callTelegramApi,
|
|
@@ -1407,64 +1439,25 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1407
1439
|
},
|
|
1408
1440
|
);
|
|
1409
1441
|
},
|
|
1410
|
-
inspectTemporaryThreadSources(
|
|
1411
|
-
target,
|
|
1412
|
-
|
|
1413
|
-
ownInputs,
|
|
1414
|
-
) {
|
|
1415
|
-
const binding = resolveTelegramUpdateJournalBinding();
|
|
1416
|
-
const botToken = configStore.getBotToken();
|
|
1417
|
-
if (!binding || !botToken) return undefined;
|
|
1418
|
-
const profile = configStore.getActiveProfileName() ?? "default";
|
|
1419
|
-
const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({
|
|
1420
|
-
botToken,
|
|
1421
|
-
botId: getTelegramBotId(),
|
|
1422
|
-
});
|
|
1442
|
+
inspectTemporaryThreadSources(target, requiredJournalBindingKeys, ownInputs) {
|
|
1443
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1444
|
+
if (!input) return undefined;
|
|
1423
1445
|
return withTelegramJournalSourceSerialization(function () {
|
|
1424
|
-
return Journal.isTelegramThreadCleanupJournalNamespaceClear(
|
|
1425
|
-
directory: Paths.resolveTelegramTempDir(),
|
|
1426
|
-
profile,
|
|
1427
|
-
botIdentity,
|
|
1428
|
-
pollingPath: telegramLeaderJournalPath.resolve(
|
|
1429
|
-
configStore.getActiveProfileName(),
|
|
1430
|
-
),
|
|
1431
|
-
requiredJournalBindingKeys: [
|
|
1432
|
-
binding.recoveryKey,
|
|
1433
|
-
...requiredJournalBindingKeys,
|
|
1434
|
-
],
|
|
1435
|
-
...(target.threadId !== undefined && ownInputs
|
|
1436
|
-
? {
|
|
1437
|
-
cleanup: {
|
|
1438
|
-
target: { chatId: target.chatId, threadId: target.threadId },
|
|
1439
|
-
ownInputs,
|
|
1440
|
-
},
|
|
1441
|
-
}
|
|
1442
|
-
: {}),
|
|
1443
|
-
limits: {
|
|
1444
|
-
maxDirectoryEntries: 10_000,
|
|
1445
|
-
maxFiles: 4096,
|
|
1446
|
-
maxBytes: 64 * 1024 * 1024,
|
|
1447
|
-
maxEntries: 10_000,
|
|
1448
|
-
maxWork: 100_000,
|
|
1449
|
-
},
|
|
1450
|
-
withSourceReference(path, operation) {
|
|
1451
|
-
return telegramJournalReferenceRegistry.withReference(
|
|
1452
|
-
{
|
|
1453
|
-
referenceClass: "operator-disposition",
|
|
1454
|
-
recoveryKey: Journal.createTelegramUpdateJournalBindingKey({
|
|
1455
|
-
path,
|
|
1456
|
-
profileName: profile,
|
|
1457
|
-
botIdentity,
|
|
1458
|
-
}),
|
|
1459
|
-
},
|
|
1460
|
-
operation,
|
|
1461
|
-
);
|
|
1462
|
-
},
|
|
1463
|
-
})
|
|
1464
|
-
? []
|
|
1465
|
-
: undefined;
|
|
1446
|
+
return Journal.isTelegramThreadCleanupJournalNamespaceClear(input) ? [] : undefined;
|
|
1466
1447
|
});
|
|
1467
1448
|
},
|
|
1449
|
+
recoverTemporaryThreadDeadQueueOwners(target, requiredJournalBindingKeys, ownInputs, isCurrent) {
|
|
1450
|
+
const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
|
|
1451
|
+
if (!input || !isCurrent()) return 0;
|
|
1452
|
+
return Journal.recoverTelegramThreadCleanupDeadQueueOwners({
|
|
1453
|
+
...input, isCurrent, withSourceSerialization: withTelegramJournalSourceSerialization,
|
|
1454
|
+
recoveryOwner: {
|
|
1455
|
+
instanceId: telegramInstanceId, processId: telegramProcessId,
|
|
1456
|
+
processBirthId: telegramQueueProcessBirthId,
|
|
1457
|
+
sessionGeneration: telegramSessionContextStore.getGeneration(),
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
},
|
|
1468
1461
|
hasWorkspaceRestoreAuthority() {
|
|
1469
1462
|
return (
|
|
1470
1463
|
lockRuntime.owns() &&
|
|
@@ -1589,12 +1582,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1589
1582
|
target.threadId === undefined
|
|
1590
1583
|
)
|
|
1591
1584
|
return undefined;
|
|
1585
|
+
// Pi creates a fresh context object per event and command, so authority follows the session, not the object.
|
|
1586
|
+
const sessionGeneration = telegramSessionContextStore.getGeneration();
|
|
1592
1587
|
return Threads.createTelegramWorkspaceThreadRenameRecipient({
|
|
1593
1588
|
store: threadStore,
|
|
1594
1589
|
instanceId: telegramInstanceId,
|
|
1595
1590
|
target,
|
|
1596
1591
|
assertAuthority() {
|
|
1597
|
-
if (telegramSessionContextStore.
|
|
1592
|
+
if (!telegramSessionContextStore.isCurrent(ctx, sessionGeneration))
|
|
1598
1593
|
throw new Error("Telegram Thread name dialog lost Pi context.");
|
|
1599
1594
|
},
|
|
1600
1595
|
getAuthority: threadNameRecipientAuthority.getAuthority,
|
|
@@ -1981,6 +1976,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1981
1976
|
const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
|
|
1982
1977
|
BusFollower.createTelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext>({
|
|
1983
1978
|
instanceId: telegramInstanceId,
|
|
1979
|
+
failureLog: runtimeDiagnostics.createFailureLog({
|
|
1980
|
+
category: "bus",
|
|
1981
|
+
failurePhase: "follower-heartbeat",
|
|
1982
|
+
outcomePhase: "follower-heartbeat-recovered",
|
|
1983
|
+
label: "follower heartbeat",
|
|
1984
|
+
}),
|
|
1984
1985
|
registrationState: telegramBusFollowerRegistrationState,
|
|
1985
1986
|
recordRuntimeEvent,
|
|
1986
1987
|
receiver: {
|
|
@@ -2383,6 +2384,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
2383
2384
|
Locks.TelegramLockEntry
|
|
2384
2385
|
>({
|
|
2385
2386
|
state: telegramThreadCapabilityState,
|
|
2387
|
+
failureLog: runtimeDiagnostics.createFailureLog({
|
|
2388
|
+
category: "bus",
|
|
2389
|
+
failurePhase: "capability-monitor",
|
|
2390
|
+
outcomePhase: "capability-monitor-recovered",
|
|
2391
|
+
label: "Threaded Mode capability probe",
|
|
2392
|
+
}),
|
|
2386
2393
|
getAllowedUserId: configStore.getAllowedUserId,
|
|
2387
2394
|
callApi: callTelegramApi,
|
|
2388
2395
|
topicTargetStore: threadStore,
|
|
@@ -3071,6 +3078,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
3071
3078
|
deleteMessage: deleteTelegramMessage,
|
|
3072
3079
|
sendGuestReply,
|
|
3073
3080
|
editGuestReply,
|
|
3081
|
+
editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
|
|
3074
3082
|
stopGuestPlaceholder: guestPlaceholderRuntime.stop,
|
|
3075
3083
|
finalizeMarkdownPreview,
|
|
3076
3084
|
preparePreviewDelivery,
|