@llblab/pi-telegram 0.26.8 → 0.26.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -214,7 +214,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
214
214
  - Telegram API methods currently used include polling, message editing, draft streaming, callback queries, reactions, file download, and media upload endpoints
215
215
  - Pi integration depends on lifecycle hooks such as `before_agent_start`, `agent_start`, `message_start`, `message_update`, and `agent_end`
216
216
  - `ctx.ui.input()` provides placeholder text rather than an editable prefilled value; when a real default must appear already filled in, prefer `ctx.ui.editor()`
217
- - For `/telegram-setup`, prefer the locally saved bot token over environment variables on repeat setup runs; env vars are the bootstrap path when no local token exists, and persisted `telegram.json` writes must remain atomic plus private because status/setup/polling paths may read it concurrently
217
+ - For `/telegram-setup`, prefer the locally saved bot token over environment variables on repeat setup runs; env vars are the bootstrap path when no local token exists, and persisted `telegram.json` writes must remain atomic plus private because status/setup/polling paths may read it concurrently; keep same-parent temp-file replacement and retry only bounded transient destination-contention errors (`EPERM`, `EACCES`, `EBUSY`) rather than deleting the live config or weakening transaction serialization
218
218
  - Command help plus prompt-template commands and status/model/thinking/queue controls are driven through `/start`'s Telegram inline application menu and callback queries; the Queue button shows the queued-item count, model-menu scope/pagination controls stay at the top under Main menu, the model pagination indicator opens a compact page picker, and thinking-menu text stays a compact heading because the current level is marked by button state; `/status`, `/model`, `/thinking`, and `/queue` are hidden compatibility shortcuts
219
219
  - Shared inline-keyboard structure belongs to `keyboard`; application-control button labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue` while core queue mechanics stay in `queue`
220
220
  - Telegram `/settings` options should open nested detail submenus by default: boolean options show a description plus Back, `on`, and `off`; list options show Back plus selectable values. One-shot actions such as syncing may run directly without a submenu when there is no meaningful choice or description step.
package/BACKLOG.md CHANGED
@@ -2,14 +2,37 @@
2
2
 
3
3
  _This backlog tracks only open release-relevant work: hotfixes, bounded maintenance, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
4
4
 
5
- ## P0 — Windows Process-Startup Timing Hotfix
5
+ ## P0 — Windows Graceful Disconnect Test Budget
6
+
7
+ Context: the graceful follower-disconnect integration overrides the production 30-second bus budget with 500 milliseconds even though the path persists cleanup intent, performs close/delete operations, updates binding state, and returns an authenticated response. A loaded Windows runner exceeded that artificial deadline while the same focused path completes promptly under normal scheduling.
8
+
9
+ Open work:
10
+
11
+ - [x] Give this integration-only bus round trip a realistic bounded timeout without changing production transport defaults, retries, cleanup ordering, or assertions.
12
+ - [ ] Validate repeatedly and across hosted platforms before publishing `0.26.10`.
13
+
14
+ Done when: slow Windows scheduling can complete the real graceful cleanup path, genuine hangs remain bounded, exact cleanup evidence stays asserted, and Linux/macOS/Windows CI passes.
15
+
16
+ ## P1 — Windows Filesystem And Timer Resilience Release
17
+
18
+ Context: hosted Windows exposed two independent assumptions: atomic config replacement can transiently fail with `EPERM` while another system component holds the destination, and a five-millisecond ownership-refresh interval may not receive CPU within a 250-millisecond test deadline under runner load.
19
+
20
+ Open work:
21
+
22
+ - [x] Retry only transient Windows config-replacement errors within the existing file transaction, preserving atomic temp-file rename and bounded failure.
23
+ - [x] Recheck the ownership predicate at the polling boundary and give that interval regression a realistic bounded scheduling budget without changing runtime cadence or assertions.
24
+ - [x] Validate focused regressions and the full suite across hosted platforms; publish and verify `0.26.9`.
25
+
26
+ Done when: concurrent config transactions survive transient destination contention, slow timer scheduling still reaches the ownership assertion, genuine failures remain bounded and diagnostic, and Linux/macOS/Windows CI passes.
27
+
28
+ ## P1 — Windows Process-Startup Timing Release
6
29
 
7
30
  Context: the process-lock regression can start a full Pi child more slowly than the three-second marker deadline on hosted Windows. Unlike the previous boundary case, the empty final marker proves the parent had not completed extension startup yet; runtime lock behavior was never reached.
8
31
 
9
32
  Open work:
10
33
 
11
34
  - [x] Give the cold-start readiness marker and parent watchdog a Windows-safe bounded budget without weakening the no-child-poll assertion.
12
- - [ ] Validate the focused process test and full suite across hosted platforms; publish and verify `0.26.8`.
35
+ - [x] Validate the focused process test and full suite across hosted platforms; publish and verify `0.26.8`.
13
36
 
14
37
  Done when: slow Windows startup reaches the same lock assertion deterministically, genuine hangs remain bounded and diagnostic, and CI passes on Linux, macOS, and Windows.
15
38
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.10: Windows Graceful Disconnect Test Budget
4
+
5
+ - `Windows CI`: Expanded only the graceful follower-disconnect integration's bus-response budget from 500 milliseconds to five seconds. Impact: loaded Windows runners can complete persisted cleanup intent, close/delete operations, binding mutation, and the authenticated response without changing the production 30-second transport default, retry policy, cleanup ordering, or exact assertions.
6
+
7
+ ## 0.26.9: Windows Filesystem And Timer Resilience
8
+
9
+ - `Config Persistence`: Retried bounded transient `EPERM`, `EACCES`, and `EBUSY` failures when atomically replacing `telegram.json` inside its existing cross-process transaction, cleaning the unique temp file on terminal failure. Impact: Windows antivirus/indexing contention no longer aborts otherwise serialized concurrent profile updates, while the live config is never deleted or rewritten non-atomically.
10
+ - `Ownership Regression`: Rechecked the ownership predicate at the polling boundary and expanded only the five-millisecond interval test's scheduling budget from 250 milliseconds to two seconds. Impact: loaded Windows runners can exercise the real heartbeat-refresh assertion without changing runtime cadence or weakening the exact ownership evidence.
11
+
3
12
  ## 0.26.8: Windows Process-Startup Timing
4
13
 
5
14
  - `Windows CI`: Expanded only the full-Pi child readiness budget from three to ten seconds and aligned its parent watchdog at fifteen seconds. Impact: slow hosted-Windows cold starts reach the actual shared-lock assertion instead of failing before extension startup, while genuine hangs remain bounded and the test still rejects every child `getUpdates` call.
@@ -113,7 +113,7 @@ interface TelegramConfig {
113
113
 
114
114
  Bot/session identity always persists under `profiles.<name>`. The ordinary setup path uses `profiles.default`; `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Named profiles use the same shape. Shared handlers plus `assistant`, `voice`, `time`, and `threads` remain top-level. On the first `0.24.0` load, unambiguous legacy root identity moves atomically into `profiles.default`; identical duplicates collapse, complementary fields merge, and conflicting values fail closed without modifying the file.
115
115
 
116
- The file is global across Pi instances. Cooperating instances serialize recursive delta merges through `telegram.json.transaction`, preserve unrelated global/profile changes from newer disk snapshots, and merge `lastUpdateId` monotonically. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
116
+ The file is global across Pi instances. Cooperating instances serialize recursive delta merges through `telegram.json.transaction`, preserve unrelated global/profile changes from newer disk snapshots, and merge `lastUpdateId` monotonically. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. Same-parent temp-file replacement retries bounded transient `EPERM`, `EACCES`, and `EBUSY` destination contention without deleting the live config or leaving transaction serialization. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
117
117
 
118
118
  Hidden/default semantics are represented by absence:
119
119
 
package/lib/config.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  readFileSync,
13
13
  renameSync,
14
14
  statSync,
15
+ unlinkSync,
15
16
  writeFileSync,
16
17
  } from "node:fs";
17
18
  import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
@@ -27,6 +28,35 @@ import type { TelegramInboundHandlerConfig } from "./inbound.ts";
27
28
  import { withTelegramFileTransaction } from "./locks.ts";
28
29
 
29
30
  const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
31
+ const CONFIG_REPLACE_RETRY_ATTEMPTS = 5;
32
+ const CONFIG_REPLACE_RETRY_DELAY_MS = 25;
33
+
34
+ function isRetryableConfigReplaceError(error: unknown): boolean {
35
+ const code = (error as { code?: unknown })?.code;
36
+ return code === "EPERM" || code === "EACCES" || code === "EBUSY";
37
+ }
38
+
39
+ function sleepConfigReplaceRetry(ms: number): void {
40
+ if (ms <= 0) return;
41
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
42
+ }
43
+
44
+ function replaceTelegramConfigFile(tempPath: string, configPath: string): void {
45
+ for (let attempt = 0; attempt < CONFIG_REPLACE_RETRY_ATTEMPTS; attempt += 1) {
46
+ try {
47
+ renameSync(tempPath, configPath);
48
+ return;
49
+ } catch (error) {
50
+ if (
51
+ !isRetryableConfigReplaceError(error) ||
52
+ attempt === CONFIG_REPLACE_RETRY_ATTEMPTS - 1
53
+ ) {
54
+ throw error;
55
+ }
56
+ sleepConfigReplaceRetry(CONFIG_REPLACE_RETRY_DELAY_MS * (attempt + 1));
57
+ }
58
+ }
59
+ }
30
60
 
31
61
  function getConfigPath(): string {
32
62
  return resolveTelegramConfigPath();
@@ -374,8 +404,16 @@ function writeTelegramConfigInTransaction(
374
404
  mode: 0o600,
375
405
  });
376
406
  chmodSync(tempConfigPath, 0o600);
377
- renameSync(tempConfigPath, configPath);
378
- chmodSync(configPath, 0o600);
407
+ try {
408
+ replaceTelegramConfigFile(tempConfigPath, configPath);
409
+ chmodSync(configPath, 0o600);
410
+ } finally {
411
+ try {
412
+ unlinkSync(tempConfigPath);
413
+ } catch {
414
+ /* rename consumed the temp file or cleanup is best effort */
415
+ }
416
+ }
379
417
  }
380
418
 
381
419
  export function getTelegramProfileFields(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.26.8",
3
+ "version": "0.26.10",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"