@llblab/pi-kit 0.35.1 → 0.35.3
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 +3 -3
- package/extensions/pi-actors/index.js +4 -0
- package/extensions/pi-claude-usage/index.ts +4 -0
- package/extensions/pi-clean-room/index.ts +4 -0
- package/extensions/pi-codex-usage/index.ts +4 -0
- package/extensions/pi-grow-loop/index.js +4 -0
- package/extensions/pi-state-flow/index.js +4 -0
- package/extensions/pi-telegram/index.js +4 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -11
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
- 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/extension.js +21 -1
- 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/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/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 +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +2 -1
- 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 +3 -2
- 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/multi-instance-bus.md +3 -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/extension.ts +21 -1
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +122 -0
- 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/runtime.ts +40 -7
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +8 -5
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +12 -5
- 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 +10 -9
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -1576,12 +1582,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1576
1582
|
target.threadId === undefined
|
|
1577
1583
|
)
|
|
1578
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();
|
|
1579
1587
|
return Threads.createTelegramWorkspaceThreadRenameRecipient({
|
|
1580
1588
|
store: threadStore,
|
|
1581
1589
|
instanceId: telegramInstanceId,
|
|
1582
1590
|
target,
|
|
1583
1591
|
assertAuthority() {
|
|
1584
|
-
if (telegramSessionContextStore.
|
|
1592
|
+
if (!telegramSessionContextStore.isCurrent(ctx, sessionGeneration))
|
|
1585
1593
|
throw new Error("Telegram Thread name dialog lost Pi context.");
|
|
1586
1594
|
},
|
|
1587
1595
|
getAuthority: threadNameRecipientAuthority.getAuthority,
|
|
@@ -1968,6 +1976,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1968
1976
|
const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
|
|
1969
1977
|
BusFollower.createTelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext>({
|
|
1970
1978
|
instanceId: telegramInstanceId,
|
|
1979
|
+
failureLog: runtimeDiagnostics.createFailureLog({
|
|
1980
|
+
category: "bus",
|
|
1981
|
+
failurePhase: "follower-heartbeat",
|
|
1982
|
+
outcomePhase: "follower-heartbeat-recovered",
|
|
1983
|
+
label: "follower heartbeat",
|
|
1984
|
+
}),
|
|
1971
1985
|
registrationState: telegramBusFollowerRegistrationState,
|
|
1972
1986
|
recordRuntimeEvent,
|
|
1973
1987
|
receiver: {
|
|
@@ -2370,6 +2384,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
2370
2384
|
Locks.TelegramLockEntry
|
|
2371
2385
|
>({
|
|
2372
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
|
+
}),
|
|
2373
2393
|
getAllowedUserId: configStore.getAllowedUserId,
|
|
2374
2394
|
callApi: callTelegramApi,
|
|
2375
2395
|
topicTargetStore: threadStore,
|
|
@@ -23,6 +23,118 @@ import {
|
|
|
23
23
|
} from "./paths.ts";
|
|
24
24
|
import * as Status from "./status.ts";
|
|
25
25
|
|
|
26
|
+
/** Sink shape shared with the runtime event recorder this domain already owns. */
|
|
27
|
+
export type TelegramFailureRecordSink = (
|
|
28
|
+
category: string,
|
|
29
|
+
errorOrMessage: unknown,
|
|
30
|
+
details: Record<string, unknown>,
|
|
31
|
+
) => void;
|
|
32
|
+
|
|
33
|
+
export interface TelegramFailureRunSummary {
|
|
34
|
+
signature: string;
|
|
35
|
+
failedAttempts: number;
|
|
36
|
+
suppressedFailures: number;
|
|
37
|
+
failedForMs: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TelegramFailureRunLog {
|
|
41
|
+
/** Records one failure; a repeated identical failure only extends the current run and returns false. */
|
|
42
|
+
record: (error: unknown, details?: Record<string, unknown>) => boolean;
|
|
43
|
+
/** Closes the run after a success, emitting its single outcome record. */
|
|
44
|
+
settled: () => TelegramFailureRunSummary | undefined;
|
|
45
|
+
/** Ends the run without an outcome record, for a stop or replacement that reports its own outcome. */
|
|
46
|
+
reset: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TelegramFailureRunLogInput {
|
|
50
|
+
category: string;
|
|
51
|
+
failurePhase: string;
|
|
52
|
+
outcomePhase: string;
|
|
53
|
+
/** Human-readable subject for the outcome record, e.g. `follower heartbeat`. */
|
|
54
|
+
label: string;
|
|
55
|
+
record?: TelegramFailureRecordSink;
|
|
56
|
+
/** Static context reapplied to every record of this phase, e.g. a session or profile key. */
|
|
57
|
+
context?: () => Record<string, unknown>;
|
|
58
|
+
getNowMs?: () => number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Stable identity of one failure kind: errno when present, otherwise the failure text. */
|
|
62
|
+
export function getTelegramFailureSignature(error: unknown): string {
|
|
63
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
64
|
+
if (typeof code === "string" && code) return code;
|
|
65
|
+
return error instanceof Error ? error.message : String(error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Consecutive identical failure runs for one diagnostic phase: a repeat extends the run instead of logging again,
|
|
70
|
+
* and a closing success or reset emits at most one outcome record. This never gates detection or recovery.
|
|
71
|
+
*/
|
|
72
|
+
export function createTelegramFailureRunLog(
|
|
73
|
+
input: TelegramFailureRunLogInput,
|
|
74
|
+
): TelegramFailureRunLog {
|
|
75
|
+
const getNowMs = input.getNowMs ?? Date.now;
|
|
76
|
+
let run: { signature: string; suppressed: number; startedAtMs: number; details: Record<string, unknown> } | undefined;
|
|
77
|
+
const emit = (errorOrMessage: unknown, details: Record<string, unknown>): void => {
|
|
78
|
+
try {
|
|
79
|
+
input.record?.(input.category, errorOrMessage, {
|
|
80
|
+
...(input.context?.() ?? {}),
|
|
81
|
+
...details,
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
// A diagnostic sink never converts a recorded failure into another failure.
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
record(error, details) {
|
|
89
|
+
const signature = getTelegramFailureSignature(error);
|
|
90
|
+
const admitted = { ...(details ?? {}) };
|
|
91
|
+
if (run?.signature === signature) {
|
|
92
|
+
run.suppressed += 1;
|
|
93
|
+
Object.assign(run.details, admitted);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const previous = run;
|
|
97
|
+
run = { signature, suppressed: 0, startedAtMs: getNowMs(), details: admitted };
|
|
98
|
+
emit(error, {
|
|
99
|
+
...admitted,
|
|
100
|
+
phase: input.failurePhase,
|
|
101
|
+
failureSignature: signature,
|
|
102
|
+
...(previous
|
|
103
|
+
? {
|
|
104
|
+
previousFailureSignature: previous.signature,
|
|
105
|
+
previousFailedAttempts: previous.suppressed + 1,
|
|
106
|
+
previousFailedForMs: getNowMs() - previous.startedAtMs,
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
settled() {
|
|
113
|
+
const closed = run;
|
|
114
|
+
if (!closed) return undefined;
|
|
115
|
+
run = undefined;
|
|
116
|
+
const summary: TelegramFailureRunSummary = {
|
|
117
|
+
signature: closed.signature,
|
|
118
|
+
failedAttempts: closed.suppressed + 1,
|
|
119
|
+
suppressedFailures: closed.suppressed,
|
|
120
|
+
failedForMs: getNowMs() - closed.startedAtMs,
|
|
121
|
+
};
|
|
122
|
+
emit(`Telegram ${input.label} recovered after ${summary.failedAttempts} failed attempts.`, {
|
|
123
|
+
...closed.details,
|
|
124
|
+
phase: input.outcomePhase,
|
|
125
|
+
failureSignature: summary.signature,
|
|
126
|
+
failedAttempts: summary.failedAttempts,
|
|
127
|
+
suppressedFailures: summary.suppressedFailures,
|
|
128
|
+
failedForMs: summary.failedForMs,
|
|
129
|
+
});
|
|
130
|
+
return summary;
|
|
131
|
+
},
|
|
132
|
+
reset() {
|
|
133
|
+
run = undefined;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
26
138
|
type TelegramLogPathInput = string | (() => string);
|
|
27
139
|
|
|
28
140
|
interface TelegramRuntimeJsonlEvent {
|
|
@@ -391,6 +503,10 @@ interface TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
|
391
503
|
error: unknown,
|
|
392
504
|
details?: Record<string, unknown>,
|
|
393
505
|
): void;
|
|
506
|
+
/** A phase-scoped failure log whose records travel through this runtime's own event pipeline. */
|
|
507
|
+
createFailureLog(
|
|
508
|
+
input: Omit<TelegramFailureRunLogInput, "record">,
|
|
509
|
+
): TelegramFailureRunLog;
|
|
394
510
|
bindStorage(ports: {
|
|
395
511
|
getBotToken(): string | undefined;
|
|
396
512
|
getProfileName(): string | undefined;
|
|
@@ -498,6 +614,12 @@ export function createTelegramRuntimeDiagnosticsRuntime<TContext>(
|
|
|
498
614
|
};
|
|
499
615
|
return {
|
|
500
616
|
events,
|
|
617
|
+
createFailureLog: (input) =>
|
|
618
|
+
createTelegramFailureRunLog({
|
|
619
|
+
...input,
|
|
620
|
+
record: (category, errorOrMessage, details) =>
|
|
621
|
+
recordRuntimeEvent(category, errorOrMessage, details),
|
|
622
|
+
}),
|
|
501
623
|
recordRuntimeEvent,
|
|
502
624
|
bindStorage(ports) {
|
|
503
625
|
getBotToken = ports.getBotToken;
|
|
@@ -167,13 +167,32 @@ export function resolveTelegramProfileTempFilePath(
|
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
/** Shared state/log paths for every profile, under the resolved agent dir (honors `PI_CODING_AGENT_DIR`). */
|
|
171
|
+
export function getTelegramDiagnosticsDisplayPaths(
|
|
172
|
+
_profileName?: string,
|
|
173
|
+
agentDir = resolveAgentDir(),
|
|
174
|
+
): {
|
|
171
175
|
state: string;
|
|
172
176
|
logs: string;
|
|
173
177
|
} {
|
|
178
|
+
// Storage paths are canonical, so also match a canonical home (e.g. /home -> /var/home).
|
|
179
|
+
const homes = [homedir()];
|
|
180
|
+
try {
|
|
181
|
+
homes.push(realpathSync(homes[0]!));
|
|
182
|
+
} catch {
|
|
183
|
+
/* An unreadable home only loses the ~ abbreviation. */
|
|
184
|
+
}
|
|
185
|
+
const display = (path: string): string => {
|
|
186
|
+
const home = homes.find(
|
|
187
|
+
(candidate) => path === candidate || path.startsWith(`${candidate}${sep}`),
|
|
188
|
+
);
|
|
189
|
+
return home
|
|
190
|
+
? `~${path.slice(home.length).split(sep).join("/")}`
|
|
191
|
+
: path;
|
|
192
|
+
};
|
|
174
193
|
return {
|
|
175
|
-
state:
|
|
176
|
-
logs:
|
|
194
|
+
state: display(resolveTelegramStatePath(agentDir)),
|
|
195
|
+
logs: display(resolveTelegramRuntimeLogPath(agentDir)),
|
|
177
196
|
};
|
|
178
197
|
}
|
|
179
198
|
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
type MaybePromise<T> = T | Promise<T>;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Structural port for run-collapsing diagnostics: a repeated identical failure extends its run instead of logging
|
|
11
|
+
* again, and the closing success records one outcome. The composition root owns the collapsed implementation.
|
|
12
|
+
*/
|
|
13
|
+
export interface TelegramRepeatedFailureLogPort {
|
|
14
|
+
record: (error: unknown, details?: Record<string, unknown>) => void;
|
|
15
|
+
settled: () => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export interface TelegramPollingConfig {
|
|
10
20
|
botToken?: string;
|
|
11
21
|
}
|
|
@@ -682,6 +692,8 @@ export interface TelegramStartupThreadCapabilityProbeDeps extends TelegramThread
|
|
|
682
692
|
export interface TelegramThreadCapabilityRuntimeDeps<
|
|
683
693
|
TContext,
|
|
684
694
|
> extends TelegramThreadCapabilityReaderDeps {
|
|
695
|
+
/** Collapses one sustained probe failure run; the caller supplies its owner. */
|
|
696
|
+
failureLog: TelegramRepeatedFailureLogPort;
|
|
685
697
|
lifecycle?: TelegramThreadCapabilityLifecycle;
|
|
686
698
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
687
699
|
ownsLock: (ctx: TContext) => boolean;
|
|
@@ -803,6 +815,8 @@ export interface TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner>
|
|
|
803
815
|
extends
|
|
804
816
|
TelegramThreadCapabilityReaderDeps,
|
|
805
817
|
TelegramThreadAwarePollingControls<TContext, TOwner> {
|
|
818
|
+
/** Collapses one sustained probe failure run; the composition root supplies its owner. */
|
|
819
|
+
failureLog: TelegramRepeatedFailureLogPort;
|
|
806
820
|
state: TelegramThreadCapabilityStateRuntime;
|
|
807
821
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
808
822
|
ownsLock: (ctx: TContext) => boolean;
|
|
@@ -882,6 +896,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
|
882
896
|
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
883
897
|
updateStatus: deps.updateStatus,
|
|
884
898
|
recordEvent: deps.recordEvent,
|
|
899
|
+
failureLog: deps.failureLog,
|
|
885
900
|
};
|
|
886
901
|
return {
|
|
887
902
|
monitor: createTelegramThreadCapabilityMonitor(capabilityDeps),
|
|
@@ -1269,8 +1284,34 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1269
1284
|
let generation = 0;
|
|
1270
1285
|
let transitionPromise: Promise<void> | undefined;
|
|
1271
1286
|
let consecutiveDisabledProbes = 0;
|
|
1287
|
+
// An absent leader endpoint repeats one identical probe failure every interval; the port collapses the run.
|
|
1288
|
+
// Running it through one guard keeps a faulty diagnostics port from breaking probes or the interval callback.
|
|
1289
|
+
const capabilityFailureLog: TelegramRepeatedFailureLogPort = {
|
|
1290
|
+
record(error, details) {
|
|
1291
|
+
try {
|
|
1292
|
+
deps.failureLog.record(error, details);
|
|
1293
|
+
} catch {
|
|
1294
|
+
// Monitor diagnostics cannot create an uncaught interval exception.
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
settled() {
|
|
1298
|
+
try {
|
|
1299
|
+
deps.failureLog.settled();
|
|
1300
|
+
} catch {
|
|
1301
|
+
// Monitor diagnostics cannot fail a completed probe.
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
reset() {
|
|
1305
|
+
try {
|
|
1306
|
+
deps.failureLog.reset();
|
|
1307
|
+
} catch {
|
|
1308
|
+
// Monitor diagnostics cannot fail a monitor stop.
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
};
|
|
1272
1312
|
const stop = (): void => {
|
|
1273
1313
|
generation += 1;
|
|
1314
|
+
capabilityFailureLog.reset();
|
|
1274
1315
|
if (interval) clearInterval(interval);
|
|
1275
1316
|
interval = undefined;
|
|
1276
1317
|
deps.lifecycle?.invalidate();
|
|
@@ -1287,6 +1328,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1287
1328
|
tracked = readTelegramThreadCapability(deps)
|
|
1288
1329
|
.then(async (threadModeEnabled) => {
|
|
1289
1330
|
if (!isCurrent()) return;
|
|
1331
|
+
capabilityFailureLog.settled();
|
|
1290
1332
|
if (threadModeEnabled === undefined) {
|
|
1291
1333
|
if (
|
|
1292
1334
|
deps.topicTargetStore.getBotState().threadMode !== "enabled" &&
|
|
@@ -1357,11 +1399,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1357
1399
|
})
|
|
1358
1400
|
.catch((error) => {
|
|
1359
1401
|
if (!isCurrent()) return;
|
|
1360
|
-
|
|
1361
|
-
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
1362
|
-
} catch {
|
|
1363
|
-
// Monitor diagnostics cannot create an unhandled interval rejection.
|
|
1364
|
-
}
|
|
1402
|
+
capabilityFailureLog.record(error);
|
|
1365
1403
|
})
|
|
1366
1404
|
.finally(() => {
|
|
1367
1405
|
if (transitionPromise === tracked) transitionPromise = undefined;
|
|
@@ -1384,11 +1422,7 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
1384
1422
|
} catch {
|
|
1385
1423
|
/* Timer shutdown must not escape the callback. */
|
|
1386
1424
|
}
|
|
1387
|
-
|
|
1388
|
-
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
1389
|
-
} catch {
|
|
1390
|
-
// Monitor diagnostics cannot create an uncaught interval exception.
|
|
1391
|
-
}
|
|
1425
|
+
capabilityFailureLog.record(error);
|
|
1392
1426
|
}
|
|
1393
1427
|
}, intervalMs);
|
|
1394
1428
|
interval.unref?.();
|