@llblab/pi-kit 0.35.1 → 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.
Files changed (43) hide show
  1. package/BACKLOG.md +1 -0
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +1 -1
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -1
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -11
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
  7. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +22 -0
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +75 -16
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +1 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +11 -1
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +30 -19
  12. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +21 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +36 -0
  14. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +80 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +2 -1
  16. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +18 -3
  17. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +13 -0
  18. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +33 -12
  19. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +11 -0
  20. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +31 -8
  21. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +2 -0
  22. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +2 -1
  23. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +5 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +3 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +6 -2
  27. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +57 -12
  28. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  29. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +3 -1
  30. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +111 -15
  31. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -0
  32. package/node_modules/@llblab/pi-telegram/lib/bus.ts +30 -21
  33. package/node_modules/@llblab/pi-telegram/lib/extension.ts +21 -1
  34. package/node_modules/@llblab/pi-telegram/lib/logging.ts +122 -0
  35. package/node_modules/@llblab/pi-telegram/lib/paths.ts +22 -3
  36. package/node_modules/@llblab/pi-telegram/lib/polling.ts +44 -10
  37. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +40 -7
  38. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +8 -5
  39. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +6 -1
  40. package/node_modules/@llblab/pi-telegram/lib/threads.ts +12 -5
  41. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +69 -17
  42. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  43. package/package.json +2 -2
package/BACKLOG.md CHANGED
@@ -4,6 +4,7 @@ Package pins, resource order and bundled runtime ownership remain authoritative
4
4
 
5
5
  ## Carried checks
6
6
 
7
+ - **Installed 0.35.2 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, connect a new Thread with every letter slot occupied and confirm it appears in seconds without the leader stalling or its followers going stale. Repeat the operator's earlier checks for leader failover after `/telegram-disconnect`, follower takeover, and collapsed repeated log records. Packed validation does not certify installed clients.
7
8
  - **Installed 0.35.1 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, verify Reroute removes an exhausted temporary tab and Restore keeps the new tab while deleting its unprotected predecessor. Confirm protected work survives. Guest PNG/JPEG, document and MP3 replacement plus staging cleanup remain explicitly deferred upstream live checks; verify unsupported OGG/Opus feedback separately. Upstream idle-leader acceptance and kit packaging checks do not certify this installation.
8
9
  - **Installed 0.35.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of both peers, verify same-process follower session replacement keeps the Thread, letter slot and title without a second connected notice. Check shared `threads.displayMode` through Settings across profiles and confirm the retired profile key is ignored without migration. Package validation does not certify installed clients.
9
10
  - **Installed 0.34.3 typing smoke (operator-owned):** After separately authorized installation/reload of both peers, verify native typing during streaming, UI prompts and compaction without recurring local authorization churn. Check actual client versions, including iOS; successful HTTP issuance and synthetic tests do not prove client visibility. Use disposable sessions and preserve accepted work.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.35.2: Telegram Connect Latency and Session Identity
6
+
7
+ - `Telegram connect`: Advances only the exact Telegram pin to `0.57.2`. A new Thread now connects in seconds instead of roughly 20 seconds when every letter slot is occupied: rotation no longer re-reads the shared journal namespace once per binding inside one synchronous pass, while post-fence protection checks still read fresh evidence. Other pins, package membership and resource order are unchanged.
8
+ - `Session identity`: A follower connected with `/telegram-connect` becomes leader when the old leader stops answering, instead of retrying a dead socket indefinitely, because authority now follows the session rather than the exact Pi context object. A promoted follower keeps its Thread, slot and name and still defers to a live lower-slot follower.
9
+ - `Log noise`: Expected-and-recovered failures no longer fill the log. Private-chat graceful cleanup deletes the Thread directly, the restore probe records one lifecycle event instead of an API error, bus attempts record only the first and final failure plus recovery, and repeated heartbeat, capability-probe and typing-presence failures collapse into one record per run with a recovery record. Status diagnostics print the paths of the agent directory in use.
10
+
5
11
  ## 0.35.1: Telegram Guest Media and Routing Cleanup
6
12
 
7
13
  - `Telegram cleanup`: Advances only the exact Telegram pin to `0.57.1`. Exhausted Reroute tabs and Restore's old tab are no longer blocked by unrelated targetless creation attempts, while accepted work and uncertain deletion stay protected. Fresh local menu Reroute and supported Restore hold command effects through their cleanup decision. Other pins, package membership and resource order are unchanged.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.1` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
20
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
21
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.1` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
21
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.2` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
22
22
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
23
23
 
24
24
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -77,9 +77,10 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
77
77
  - Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Explicit `/telegram-connect` additionally grants one profile-admitted reset of matching untargeted unfinished Thread-creation metadata before Workspace identity allocation; it may leave an unknown Telegram tab behind, but never deletes a known target, Workspace binding or journal receipt. Automatic startup/re-registration and Restore-only admission cannot grant that reset. Known pending targets retain their exact binding key and slot; fresh provisioning uses unique operation IDs so late recovery evidence cannot attach to a replacement attempt. Follower reset intent uses authenticated `explicit-connect-recovery-v1` registration. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
78
78
  - Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
79
79
  - Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
80
+ - Session identity, not Pi context object identity, decides authority. Pi builds a fresh context object for every event, command, and tool call, so compare sessions through their stored generation and ID, never by object, and treat a fresh object of the same session as the same authority. How an instance came online — explicit `/telegram-connect`, session auto-restore, election promotion, or a same-process handoff — may decide only that it is online and which existing binding, slot, and target it takes; every post-online behavior, including election eligibility, promotion, routing, delivery, status, and Thread display, must read the same acknowledged registration state and must not branch on that mechanism. Only the documented connect-time gates (remembered-binding restore admission, restore/recovery envelope flags, capability negotiation, and predecessor handoff) may observe it. An in-flight registration survives a fresh same-session context and is dropped only by a replaced session generation or ID.
80
81
  - Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader serializes the shared preference write and owns title application.
81
82
  - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
82
- - Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
83
+ - Repeated identical diagnostics in one phase collapse into one run record plus one outcome record; the logging domain owns that recording policy, and other domains receive it as a structural port rather than growing a second owner. Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
83
84
 
84
85
  ### 4.3 Durable Admission And Settlement
85
86
 
@@ -2,14 +2,4 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
- - [ ] **Routing extensions and follow-up coverage.** Preserve live/unverifiable owners, offered receipts, exact source references, session/transport fences and one-shot deletion while extending beyond the current supported handoffs.
6
- - [ ] **Follower menu handoff.** Establish whether deletion-before-execution warrants a separately negotiated Reroute acceptance/release contract. Ordinary follower ACK confirms durable custody, not held execution; existing held-command RPCs belong to Restore relocation and cannot silently change ordinary forwarding. Require exact recipient generation, bounded observation and no replay after uncertain release.
7
- - [ ] **Non-menu and historical-source boundary.** Resolve the intended ordering for mutating/queue-producing commands and cold/revived originals before expanding the fresh local menu path. Queue receipt acceptance and historical retention must remain authoritative; never pre-complete sources, weaken fresh-source readiness or block accepted work solely to force deletion. Preserve current behavior until a supported handoff is proven.
8
- - [ ] **Protected-work live coverage.** Exercise Reroute and Restore with protected earlier work to confirm cleanup waits without losing work. Idle leader acceptance and native regressions do not prove every busy-session client scenario. No manual journal edits or deletion of live tabs to obtain evidence.
9
- - [ ] **Guest inline media live acceptance.** Explicitly deferred beyond 0.57.1 by operator release approval; not completed. After separately authorized reload, verify queued PNG/JPEG, documents and MP3 replace the acknowledged placeholder, with and without caption, and staging messages are removed. Confirm OGG/Opus attachment rejection and visible voice-markup explanation. Mocked Bot API coverage does not prove live-client rendering; use disposable artifacts and preserve accepted work.
10
- - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab and follower `/start` Restore with negotiated menu-alias support. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
11
- - [ ] **Steer follow-up live acceptance.** Release authorization explicitly defers these checks beyond 0.56.0; do not treat automated coverage or the accepted busy-run smoke as live proof for unrun scenarios. Preserve the [queue and dispatch contract](./docs/architecture.md#queue-and-dispatch-safety).
12
- - [ ] **Operator scenarios.** Verify final selector/counter rendering; idle packets, native pending input, compaction, model switching, abort/Continue, complete media groups, Reroute/Restore, negotiated leader/follower transfer, reply/preview ownership and session-generation replacement. Record actual Pi/OS/Telegram versions, peer capabilities and redacted outcomes. Reload and disposable live tests require operator intent; do not manipulate runtime files to obtain acceptance.
13
- - [ ] **Restore recipient refusal after leadership change.** Diagnose a retained routing chooser refusing the selected current leader with unavailable recipient authority. Identify the exact context/session/Workspace mismatch before proposing recovery; do not treat an absent follower registry as the cause of a leader-target refusal, bypass fences or mutate live state.
14
- - [ ] **Post-reload diagnostics.** Investigate recurring typing-authority refusals and Workspace publication retries observed alongside successful leader/follower reconnection. Establish whether they are expected fenced stale work or affect current-session delivery; successful registration alone does not establish error-free operation.
15
- - [ ] **Native Windows scenarios.** Verify named-pipe custody/ACK, journal/atomic rename, reused-context lifecycle and the phone scenarios on an actual Windows host. Record skipped tests and missing platform evidence explicitly; a hosted CI result does not establish phone or full live-runtime acceptance.
5
+ No open tasks.
@@ -4,6 +4,18 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.57.2: Connect Latency and Session Identity
8
+
9
+ - `Connect latency`: A new Thread no longer waits about 20 seconds when all 26 letter slots are occupied. Slot rotation read the complete journal namespace and every shared journal once per binding inside one synchronous pass, freezing the leader's event loop and staling healthy follower heartbeats. One occupancy snapshot now reads that binding-independent evidence once, and every fresh snapshot still re-reads it so post-fence checks see newly arrived work.
10
+ - `Session identity`: Follower promotion, Thread-name authority and in-flight registration now compare the session instead of the exact context object. A follower connected manually with `/telegram-connect` becomes leader when the old leader stops answering, instead of rescheduling promotion forever while registration retried a dead socket. Pi's fresh per-event context objects no longer change behavior; a follower with no live session still waits.
11
+ - `Bus retries`: A leader reload no longer logs a line for every follower bus attempt, and an absent leader no longer repeats identical heartbeat, capability-probe or typing-presence failures every interval or tick. Each request records its first failure, one `client-recovered` event when a later attempt succeeds, and its final failure; a repeated failure records once per run plus a recovery record with the suppressed count. Detection and recovery observation are unchanged.
12
+ - `Restore probe`: The follower restore probe now treats Telegram's `message thread not found` as its own expected answer instead of an API error. It still marks the target stale, re-creates the Thread and records one `topic-lifecycle` event with the old and new thread ids. Unexpected API failures keep their diagnostics.
13
+ - `Graceful shutdown`: Graceful cleanup in a private chat now deletes the Thread directly instead of first calling `closeForumTopic`, which Telegram rejects outside forums. Rebind protection still runs immediately before deletion, so a Thread re-bound during cleanup remains protected; forum cleanups still close before deleting.
14
+ - `Status diagnostics`: The connection status prints the state and log paths of the agent directory actually in use, so a second profile started with `PI_CODING_AGENT_DIR` no longer points at the default profile's files. Stored paths resolve through symlinked home directories before they are abbreviated. Fixes #349.
15
+ - `Slow-call diagnostics`: Follower API calls slower than 1.5 seconds record one diagnostic with the method, the elapsed time and the split between waiting for registration and transport. A late reply can now be attributed to recovery waiting or to delivery instead of staying unexplained. This is observation only.
16
+ - `Delivery coverage`: Automated checks now assert that commentary is delivered while its tool call is still running and that a final answer is delivered before automatic compaction completes, for every activity mode with draft previews on and off. Both paths already behaved this way; the coverage prevents regressions.
17
+
18
+
7
19
  ## 0.57.1: Guest Media and Routing Cleanup
8
20
 
9
21
  - `Guest attachments`: Queued photos, documents and MP3 audio now replace the acknowledged Guest Mode placeholder instead of being silently skipped. Upload failures and confirmed edit rejections report errors; uncertain edits are not replayed or overwritten. OGG/Opus attachments are rejected before queueing on this path, and voice markup explains the limitation. Live-client media acceptance remains a deferred follow-up. Fixes #346.
@@ -131,6 +131,18 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
131
131
  recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
132
132
  timeoutMs?: number;
133
133
  }
134
+ /** Structural port for run-collapsing diagnostics, implemented by the logging domain. */
135
+ export interface TelegramFailureRunLogPort {
136
+ record: (error: unknown, details?: Record<string, unknown>) => boolean;
137
+ settled: () => TelegramFailureRunSummaryPort | undefined;
138
+ reset: () => void;
139
+ }
140
+ export interface TelegramFailureRunSummaryPort {
141
+ signature: string;
142
+ failedAttempts: number;
143
+ suppressedFailures: number;
144
+ failedForMs: number;
145
+ }
134
146
  export interface TelegramBusFollowerApiCallerDeps {
135
147
  socketPath: TelegramBusSocketPathSource;
136
148
  instanceId: string;
@@ -140,10 +152,18 @@ export interface TelegramBusFollowerApiCallerDeps {
140
152
  waitForRegistrationGeneration?: (timeoutMs?: number) => Promise<string | undefined>;
141
153
  getNowMs?: () => number;
142
154
  timeoutMs?: number;
155
+ /** Receives one diagnostic per call slower than `slowCallMs`; observation only. */
156
+ recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
157
+ slowCallMs?: number;
143
158
  }
144
159
  export interface TelegramBusFollowerRegistrationRuntimeDeps<TContext extends {
145
160
  cwd?: string;
146
161
  }> {
162
+ /**
163
+ * Collapses one sustained heartbeat failure run. Optional at this unit boundary, where an absent port records
164
+ * every failure; the runtime assembly requires it so production always supplies the logging domain's owner.
165
+ */
166
+ failureLog?: TelegramFailureRunLogPort;
147
167
  instanceId: string;
148
168
  createRequestId: () => string;
149
169
  protocolIdentity: TelegramBusProtocolIdentity;
@@ -291,6 +311,8 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<TContext extends {
291
311
  cwd?: string;
292
312
  }> {
293
313
  instanceId: string;
314
+ /** Collapses one sustained heartbeat failure run; the composition root supplies the logging domain's owner. */
315
+ failureLog: TelegramFailureRunLogPort;
294
316
  registrationState: TelegramBusFollowerRegistrationState;
295
317
  recordRuntimeEvent: (category: string, error: unknown, details?: Record<string, unknown>) => void;
296
318
  receiver: Omit<TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>, "instanceId" | "recordRuntimeEvent" | "getRegistrationGeneration">;
@@ -23,6 +23,7 @@ import * as WorkspaceIdentity from "./workspace-identity.js";
23
23
  const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
24
24
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
25
25
  const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
26
+ const TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS = 1_500;
26
27
  const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
27
28
  export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
28
29
  const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
@@ -213,6 +214,7 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
213
214
  });
214
215
  registration = createTelegramBusFollowerRegistrationRuntime({
215
216
  ...ports.registration,
217
+ failureLog: ports.failureLog,
216
218
  ...sharedRuntimeDeps,
217
219
  startReceiving: receiver.start,
218
220
  stopReceiving: receiver.stop,
@@ -677,6 +679,7 @@ export function createTelegramBusFollowerClientRuntime(deps) {
677
679
  instanceId: deps.instanceId,
678
680
  getAuthSecret: deps.getApiAuthSecret,
679
681
  getRegistrationGeneration: deps.getRegistrationGeneration,
682
+ recordRuntimeEvent: deps.recordRuntimeEvent,
680
683
  }),
681
684
  agentMessages: createTelegramBusAgentMessageClient({
682
685
  ...sharedClientDeps,
@@ -947,11 +950,13 @@ export function createTelegramBusFollowerSelectedMenuCaller(deps) {
947
950
  export function createTelegramBusFollowerApiCaller(deps) {
948
951
  const getNowMs = deps.getNowMs ?? Date.now;
949
952
  const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
950
- return async (method, args, localOptions) => {
953
+ const slowCallMs = deps.slowCallMs ?? TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS;
954
+ const caller = async (method, args, localOptions, timing) => {
951
955
  const assertion = localOptions?.assertAuthority;
952
956
  const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
953
957
  assertCurrent();
954
958
  const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
959
+ timing.registeredAtMs = getNowMs();
955
960
  assertCurrent();
956
961
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
957
962
  let response;
@@ -999,6 +1004,42 @@ export function createTelegramBusFollowerApiCaller(deps) {
999
1004
  }
1000
1005
  throw new Error(message ?? "Telegram bus API call failed.");
1001
1006
  };
1007
+ return async (method, args, localOptions) => {
1008
+ const startedAtMs = getNowMs();
1009
+ const timing = {};
1010
+ let outcome = "ok";
1011
+ try {
1012
+ return await caller(method, args, localOptions, timing);
1013
+ }
1014
+ catch (error) {
1015
+ outcome = "error";
1016
+ throw error;
1017
+ }
1018
+ finally {
1019
+ const elapsedMs = getNowMs() - startedAtMs;
1020
+ if (elapsedMs >= slowCallMs) {
1021
+ const apiMethod = (method === "call" || method === "callMultipart") &&
1022
+ typeof args[0] === "string"
1023
+ ? args[0]
1024
+ : method;
1025
+ try {
1026
+ deps.recordRuntimeEvent?.("bus", `Telegram bus follower API call was slow (${elapsedMs}ms).`, {
1027
+ phase: "follower-api-slow",
1028
+ method: apiMethod,
1029
+ elapsedMs,
1030
+ registrationWaitMs: (timing.registeredAtMs ?? getNowMs()) - startedAtMs,
1031
+ ...(timing.registeredAtMs === undefined
1032
+ ? {}
1033
+ : { transportMs: getNowMs() - timing.registeredAtMs }),
1034
+ outcome,
1035
+ });
1036
+ }
1037
+ catch {
1038
+ // Diagnostics cannot change the API call outcome.
1039
+ }
1040
+ }
1041
+ }
1042
+ };
1002
1043
  }
1003
1044
  async function resolveTelegramBusFollowerRegistration(deps, timeoutMs) {
1004
1045
  const current = deps.getRegistrationGeneration();
@@ -1447,10 +1488,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
1447
1488
  const promoteToLeader = async (reason, ctx, binding, election) => {
1448
1489
  if (!isCurrent())
1449
1490
  return;
1450
- const activeCtx = deps.getActiveContext?.();
1451
- if (deps.getActiveContext && activeCtx !== ctx) {
1452
- scheduleRecovery(reason, ctx, binding);
1453
- return;
1491
+ if (deps.getActiveContext) {
1492
+ // Pi hands every event and command a fresh context object, so a manually connected follower never holds
1493
+ // the stored session object. Promote through the live session context instead of rescheduling: heartbeat
1494
+ // failures re-request recovery faster than the retry grace, so a reschedule-only path never fires.
1495
+ const activeCtx = deps.getActiveContext();
1496
+ if (!activeCtx) {
1497
+ scheduleRecovery(reason, ctx, binding);
1498
+ return;
1499
+ }
1500
+ ctx = activeCtx;
1454
1501
  }
1455
1502
  deps.setLifecyclePhase("electing");
1456
1503
  safeUpdateStatus(ctx);
@@ -1629,6 +1676,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1629
1676
  (!deps.registrationState ||
1630
1677
  deps.registrationState.getGeneration() === generation);
1631
1678
  const stopHeartbeat = () => {
1679
+ // A stopped heartbeat ends the current failure run, so a later run logs its own first failure.
1680
+ heartbeatFailureLog.reset();
1632
1681
  if (!heartbeatInterval)
1633
1682
  return;
1634
1683
  clearInterval(heartbeatInterval);
@@ -1659,6 +1708,22 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1659
1708
  }
1660
1709
  });
1661
1710
  };
1711
+ /** A sustained leader outage repeats one identical failure every interval; the port collapses the run. */
1712
+ const heartbeatFailureLog = deps.failureLog ?? {
1713
+ record(error) {
1714
+ try {
1715
+ deps.recordRuntimeEvent?.("bus", error, {
1716
+ phase: "follower-heartbeat",
1717
+ });
1718
+ }
1719
+ catch {
1720
+ // Diagnostics cannot replace heartbeat recovery.
1721
+ }
1722
+ return true;
1723
+ },
1724
+ settled: () => undefined,
1725
+ reset: () => undefined,
1726
+ };
1662
1727
  const sendHeartbeat = async () => {
1663
1728
  const leaderSocketPath = activeLeaderSocketPath;
1664
1729
  const registrationGeneration = activeRegistrationGeneration;
@@ -1681,6 +1746,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1681
1746
  if (!isCurrentHeartbeat())
1682
1747
  return;
1683
1748
  if (response?.kind === "bus.ack" && response.ok) {
1749
+ heartbeatFailureLog.settled();
1684
1750
  const heartbeatResult = isRecord(response.result)
1685
1751
  ? response.result
1686
1752
  : undefined;
@@ -1707,14 +1773,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1707
1773
  catch (error) {
1708
1774
  if (!isCurrentHeartbeat())
1709
1775
  return;
1710
- try {
1711
- deps.recordRuntimeEvent?.("bus", error, {
1712
- phase: "follower-heartbeat",
1713
- });
1714
- }
1715
- catch {
1716
- // Diagnostics cannot replace heartbeat recovery.
1717
- }
1776
+ heartbeatFailureLog.record(error);
1718
1777
  if (!heartbeatContext)
1719
1778
  return;
1720
1779
  try {
@@ -1956,10 +2015,10 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1956
2015
  return false;
1957
2016
  },
1958
2017
  setContext(ctx) {
2018
+ // Pi hands every event a fresh context object. Only a replaced session drops an in-flight attempt.
1959
2019
  if (registrationAttempt &&
1960
- (registrationAttempt.ctx !== ctx ||
1961
- registrationAttempt.sessionGeneration !==
1962
- deps.getSessionGeneration?.() ||
2020
+ (registrationAttempt.sessionGeneration !==
2021
+ deps.getSessionGeneration?.() ||
1963
2022
  registrationAttempt.sessionId !== deps.getSessionId?.(ctx)))
1964
2023
  registrationAttempt = undefined;
1965
2024
  activeContext = ctx;
@@ -66,7 +66,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
66
66
  getLiveFollowers?: () => readonly TelegramBusFollowerView[];
67
67
  isFollowerProcessAlive?: (pid: number) => boolean;
68
68
  topicTargetStore: Threads.TelegramTopicTargetStore;
69
- callApi: <TResponse>(method: string, body: Record<string, unknown>) => Promise<TResponse>;
69
+ callApi: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
70
70
  getCurrentLeaderEpoch?: () => number | string | undefined;
71
71
  getSyncState: () => Sync.TelegramSyncState;
72
72
  setSyncState: (state: Sync.TelegramSyncState) => void;
@@ -1200,7 +1200,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1200
1200
  message_thread_id: connectedAnnouncement.target.threadId,
1201
1201
  text: connectedAnnouncement.text,
1202
1202
  parse_mode: connectedAnnouncement.parseMode,
1203
- });
1203
+ },
1204
+ // A deleted Thread is this probe's expected answer; the recovery below records it.
1205
+ { isExpectedError: Threads.isTelegramTopicTargetStaleError });
1204
1206
  assertRestoreRegistration();
1205
1207
  if (recoverableTarget || probeRequiredRecord) {
1206
1208
  const activatedRecord = deps.topicTargetStore.upsert({
@@ -1240,7 +1242,15 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1240
1242
  if (Threads.isTelegramTopicTargetStaleError(error)) {
1241
1243
  deps.topicTargetStore.markStaleByTarget(result.target, "deleted", error instanceof Error ? error.message : String(error));
1242
1244
  await deps.topicTargetStore.persist();
1245
+ const staleTarget = result.target;
1243
1246
  result = await provisionTarget();
1247
+ deps.recordRuntimeEvent("telegram", "Telegram follower restore probe found its Thread deleted; re-created it.", {
1248
+ phase: "topic-lifecycle-restore-probe-recreated",
1249
+ instanceId: registration.instanceId,
1250
+ chatId: staleTarget.chatId,
1251
+ staleThreadId: staleTarget.threadId,
1252
+ threadId: result.target.threadId,
1253
+ });
1244
1254
  connectedAnnouncement =
1245
1255
  createTelegramBusInstanceLifecycleAnnouncement({
1246
1256
  target: result.target,
@@ -1960,32 +1960,43 @@ export async function sendTelegramBusLocalEnvelope(options) {
1960
1960
  };
1961
1961
  const attempts = Math.max(1, resolvedOptions.retry?.attempts ?? 1);
1962
1962
  const delayMs = Math.max(0, resolvedOptions.retry?.delayMs ?? 0);
1963
+ // Log the first failure, then only the outcome: a reloading leader would otherwise log every attempt.
1963
1964
  for (let attempt = 1;; attempt += 1) {
1964
1965
  try {
1965
- return await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
1966
+ const response = await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
1967
+ if (attempt > 1)
1968
+ resolvedOptions.recordTransportEvent?.("client-recovered", {
1969
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
1970
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
1971
+ attempt,
1972
+ attempts,
1973
+ });
1974
+ return response;
1966
1975
  }
1967
1976
  catch (error) {
1968
1977
  const info = classifyTelegramBusTransportError(error);
1969
- resolvedOptions.recordTransportEvent?.("client-failed", {
1970
- ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
1971
- ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
1972
- attempt,
1973
- attempts,
1974
- ...info,
1975
- });
1976
- if (error instanceof TelegramBusLocalAuthorityError ||
1978
+ const final = error instanceof TelegramBusLocalAuthorityError ||
1977
1979
  attempt >= attempts ||
1978
- !isRetryableTelegramBusTransportError(error)) {
1980
+ !isRetryableTelegramBusTransportError(error);
1981
+ if (attempt === 1 || final)
1982
+ resolvedOptions.recordTransportEvent?.("client-failed", {
1983
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
1984
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
1985
+ attempt,
1986
+ attempts,
1987
+ ...info,
1988
+ });
1989
+ if (final)
1979
1990
  throw error;
1980
- }
1981
- resolvedOptions.recordTransportEvent?.("client-retry", {
1982
- ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
1983
- ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
1984
- attempt,
1985
- attempts,
1986
- delayMs,
1987
- ...info,
1988
- });
1991
+ if (attempt === 1)
1992
+ resolvedOptions.recordTransportEvent?.("client-retry", {
1993
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
1994
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
1995
+ attempt,
1996
+ attempts,
1997
+ delayMs,
1998
+ ...info,
1999
+ });
1989
2000
  await delayTelegramBusTransportRetry(delayMs);
1990
2001
  }
1991
2002
  }
@@ -658,6 +658,12 @@ export default function (pi) {
658
658
  const promptDispatchRuntime = Runtime.createTelegramPromptDispatchRuntime({
659
659
  lifecycle,
660
660
  typing,
661
+ failureLog: runtimeDiagnostics.createFailureLog({
662
+ category: "typing",
663
+ failurePhase: "typing-presence",
664
+ outcomePhase: "typing-presence-recovered",
665
+ label: "typing presence",
666
+ }),
661
667
  getDefaultChatId: proactivePushChatIdGetter,
662
668
  isThreadedMode() {
663
669
  return deliveryTargetPolicyRuntime.getTargetPolicyView().threaded === true;
@@ -1246,12 +1252,14 @@ export default function (pi) {
1246
1252
  if (telegramBusFollowerRegistrationState.isRegistered() ||
1247
1253
  target.threadId === undefined)
1248
1254
  return undefined;
1255
+ // Pi creates a fresh context object per event and command, so authority follows the session, not the object.
1256
+ const sessionGeneration = telegramSessionContextStore.getGeneration();
1249
1257
  return Threads.createTelegramWorkspaceThreadRenameRecipient({
1250
1258
  store: threadStore,
1251
1259
  instanceId: telegramInstanceId,
1252
1260
  target,
1253
1261
  assertAuthority() {
1254
- if (telegramSessionContextStore.get() !== ctx)
1262
+ if (!telegramSessionContextStore.isCurrent(ctx, sessionGeneration))
1255
1263
  throw new Error("Telegram Thread name dialog lost Pi context.");
1256
1264
  },
1257
1265
  getAuthority: threadNameRecipientAuthority.getAuthority,
@@ -1565,6 +1573,12 @@ export default function (pi) {
1565
1573
  });
1566
1574
  const telegramBusFollowerAssembly = BusFollower.createTelegramBusFollowerRuntimeAssembly({
1567
1575
  instanceId: telegramInstanceId,
1576
+ failureLog: runtimeDiagnostics.createFailureLog({
1577
+ category: "bus",
1578
+ failurePhase: "follower-heartbeat",
1579
+ outcomePhase: "follower-heartbeat-recovered",
1580
+ label: "follower heartbeat",
1581
+ }),
1568
1582
  registrationState: telegramBusFollowerRegistrationState,
1569
1583
  recordRuntimeEvent,
1570
1584
  receiver: {
@@ -1894,6 +1908,12 @@ export default function (pi) {
1894
1908
  });
1895
1909
  const telegramThreadCapabilityRuntime = Polling.createTelegramThreadCapabilityOrchestration({
1896
1910
  state: telegramThreadCapabilityState,
1911
+ failureLog: runtimeDiagnostics.createFailureLog({
1912
+ category: "bus",
1913
+ failurePhase: "capability-monitor",
1914
+ outcomePhase: "capability-monitor-recovered",
1915
+ label: "Threaded Mode capability probe",
1916
+ }),
1897
1917
  getAllowedUserId: configStore.getAllowedUserId,
1898
1918
  callApi: callTelegramApi,
1899
1919
  topicTargetStore: threadStore,
@@ -4,6 +4,40 @@
4
4
  * Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
5
5
  */
6
6
  import * as Status from "./status.ts";
7
+ /** Sink shape shared with the runtime event recorder this domain already owns. */
8
+ export type TelegramFailureRecordSink = (category: string, errorOrMessage: unknown, details: Record<string, unknown>) => void;
9
+ export interface TelegramFailureRunSummary {
10
+ signature: string;
11
+ failedAttempts: number;
12
+ suppressedFailures: number;
13
+ failedForMs: number;
14
+ }
15
+ export interface TelegramFailureRunLog {
16
+ /** Records one failure; a repeated identical failure only extends the current run and returns false. */
17
+ record: (error: unknown, details?: Record<string, unknown>) => boolean;
18
+ /** Closes the run after a success, emitting its single outcome record. */
19
+ settled: () => TelegramFailureRunSummary | undefined;
20
+ /** Ends the run without an outcome record, for a stop or replacement that reports its own outcome. */
21
+ reset: () => void;
22
+ }
23
+ export interface TelegramFailureRunLogInput {
24
+ category: string;
25
+ failurePhase: string;
26
+ outcomePhase: string;
27
+ /** Human-readable subject for the outcome record, e.g. `follower heartbeat`. */
28
+ label: string;
29
+ record?: TelegramFailureRecordSink;
30
+ /** Static context reapplied to every record of this phase, e.g. a session or profile key. */
31
+ context?: () => Record<string, unknown>;
32
+ getNowMs?: () => number;
33
+ }
34
+ /** Stable identity of one failure kind: errno when present, otherwise the failure text. */
35
+ export declare function getTelegramFailureSignature(error: unknown): string;
36
+ /**
37
+ * Consecutive identical failure runs for one diagnostic phase: a repeat extends the run instead of logging again,
38
+ * and a closing success or reset emits at most one outcome record. This never gates detection or recovery.
39
+ */
40
+ export declare function createTelegramFailureRunLog(input: TelegramFailureRunLogInput): TelegramFailureRunLog;
7
41
  type TelegramLogPathInput = string | (() => string);
8
42
  interface TelegramRuntimeJsonlEvent {
9
43
  at: number;
@@ -47,6 +81,8 @@ interface TelegramRuntimeDiagnosticsStatusPorts<TContext> {
47
81
  interface TelegramRuntimeDiagnosticsRuntime<TContext> {
48
82
  events: Status.TelegramRuntimeEventRecorder;
49
83
  recordRuntimeEvent(category: string, error: unknown, details?: Record<string, unknown>): void;
84
+ /** A phase-scoped failure log whose records travel through this runtime's own event pipeline. */
85
+ createFailureLog(input: Omit<TelegramFailureRunLogInput, "record">): TelegramFailureRunLog;
50
86
  bindStorage(ports: {
51
87
  getBotToken(): string | undefined;
52
88
  getProfileName(): string | undefined;
@@ -8,6 +8,82 @@ import { basename, dirname, join } from "node:path";
8
8
  import { withTelegramFileTransaction } from "./locks.js";
9
9
  import { resolveAgentDir, resolveTelegramPreviousSharedRuntimeLogPath, resolveTelegramProfileTempFilePath, resolveTelegramRuntimeLogPath, } from "./paths.js";
10
10
  import * as Status from "./status.js";
11
+ /** Stable identity of one failure kind: errno when present, otherwise the failure text. */
12
+ export function getTelegramFailureSignature(error) {
13
+ const code = error?.code;
14
+ if (typeof code === "string" && code)
15
+ return code;
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ /**
19
+ * Consecutive identical failure runs for one diagnostic phase: a repeat extends the run instead of logging again,
20
+ * and a closing success or reset emits at most one outcome record. This never gates detection or recovery.
21
+ */
22
+ export function createTelegramFailureRunLog(input) {
23
+ const getNowMs = input.getNowMs ?? Date.now;
24
+ let run;
25
+ const emit = (errorOrMessage, details) => {
26
+ try {
27
+ input.record?.(input.category, errorOrMessage, {
28
+ ...(input.context?.() ?? {}),
29
+ ...details,
30
+ });
31
+ }
32
+ catch {
33
+ // A diagnostic sink never converts a recorded failure into another failure.
34
+ }
35
+ };
36
+ return {
37
+ record(error, details) {
38
+ const signature = getTelegramFailureSignature(error);
39
+ const admitted = { ...(details ?? {}) };
40
+ if (run?.signature === signature) {
41
+ run.suppressed += 1;
42
+ Object.assign(run.details, admitted);
43
+ return false;
44
+ }
45
+ const previous = run;
46
+ run = { signature, suppressed: 0, startedAtMs: getNowMs(), details: admitted };
47
+ emit(error, {
48
+ ...admitted,
49
+ phase: input.failurePhase,
50
+ failureSignature: signature,
51
+ ...(previous
52
+ ? {
53
+ previousFailureSignature: previous.signature,
54
+ previousFailedAttempts: previous.suppressed + 1,
55
+ previousFailedForMs: getNowMs() - previous.startedAtMs,
56
+ }
57
+ : {}),
58
+ });
59
+ return true;
60
+ },
61
+ settled() {
62
+ const closed = run;
63
+ if (!closed)
64
+ return undefined;
65
+ run = undefined;
66
+ const summary = {
67
+ signature: closed.signature,
68
+ failedAttempts: closed.suppressed + 1,
69
+ suppressedFailures: closed.suppressed,
70
+ failedForMs: getNowMs() - closed.startedAtMs,
71
+ };
72
+ emit(`Telegram ${input.label} recovered after ${summary.failedAttempts} failed attempts.`, {
73
+ ...closed.details,
74
+ phase: input.outcomePhase,
75
+ failureSignature: summary.signature,
76
+ failedAttempts: summary.failedAttempts,
77
+ suppressedFailures: summary.suppressedFailures,
78
+ failedForMs: summary.failedForMs,
79
+ });
80
+ return summary;
81
+ },
82
+ reset() {
83
+ run = undefined;
84
+ },
85
+ };
86
+ }
11
87
  const DEFAULT_MAX_LOG_BYTES = 5 * 1024 * 1024;
12
88
  export function getTelegramRuntimeLogPath(agentDir = resolveAgentDir(), profileName) {
13
89
  return resolveTelegramProfileTempFilePath("logs", "jsonl", agentDir, profileName);
@@ -333,6 +409,10 @@ export function createTelegramRuntimeDiagnosticsRuntime(options = {}) {
333
409
  };
334
410
  return {
335
411
  events,
412
+ createFailureLog: (input) => createTelegramFailureRunLog({
413
+ ...input,
414
+ record: (category, errorOrMessage, details) => recordRuntimeEvent(category, errorOrMessage, details),
415
+ }),
336
416
  recordRuntimeEvent,
337
417
  bindStorage(ports) {
338
418
  getBotToken = ports.getBotToken;
@@ -43,7 +43,8 @@ export declare function resolveLegacyTelegramOwnersPath(agentDir?: string): stri
43
43
  export declare function resolveTelegramOwnersPath(): string;
44
44
  export declare function getTelegramProfilePathSuffix(profileName?: string): string;
45
45
  export declare function resolveTelegramProfileTempFilePath(baseName: string, extension: string, agentDir?: string, profileName?: string): string;
46
- export declare function getTelegramDiagnosticsDisplayPaths(_profileName?: string): {
46
+ /** Shared state/log paths for every profile, under the resolved agent dir (honors `PI_CODING_AGENT_DIR`). */
47
+ export declare function getTelegramDiagnosticsDisplayPaths(_profileName?: string, agentDir?: string): {
47
48
  state: string;
48
49
  logs: string;
49
50
  };