jeo-code 0.7.37 → 0.7.38

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.7.38] - 2026-07-05
10
+ _Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes._
11
+
12
+ ### Fixed
13
+ - **Chat authorization on inbound Telegram commands (security).** `TelegramDaemon.handleUpdate` now drops any update whose `message.chat.id` does not match the configured `notifications.telegram.chatId` — previously `handleInboundText` ran for ANY update the bot received, so `/steer`/`/cancel`/`/subagents` were reachable by any Telegram user who found the bot (bot usernames are publicly enumerable), not just the account that ran `jeo notify setup`. Unauthorized updates are dropped silently (no reply), so probing the bot doesn't confirm it's live. `pollTelegramLoop` now delegates through `handleUpdate` instead of calling `handleInboundText` directly.
14
+ - **`pollTelegramLoop` hot-loop on a Telegram API error response.** A `getUpdates` call that resolves with `{ ok: false }` (e.g. `409 Conflict` from a second long-poll owner, `401` from a revoked token) previously looped immediately with no delay; only a thrown/network error had the existing 2s backoff. Both paths now share the same `sleep(2_000)`.
15
+
16
+ ### Changed
17
+ - `formatNotifyEvent`/`formatSubagentsList` shared status→icon logic deduplicated into one `STATUS_ICON: Record<SubagentRecord["status"], string>` lookup (was two separately-maintained icon mappings, one keyed by event kind, one by an inline ternary chain keyed by status).
18
+ - `SessionNotifyEndpoint`'s `WsData` payload type (`{ authed: true }`) removed — the value was written into every upgraded socket's `data` but never read anywhere; the server/socket types are now `Server<undefined>`/`ServerWebSocket<undefined>`, and the `fetch` handler is a lexical-`this` arrow function instead of a `const self = this` capture.
19
+ - `parseSteerCommand`/`parseCancelCommand` return inline object types instead of the one-call-site `ParsedSteerCommand`/`ParsedCancelCommand` exported interfaces (no external caller used either name).
20
+ - `TelegramDaemon#sendRequest` and `daemon-control.ts`'s `waitFor` now use `Promise.withResolvers()` instead of the `new Promise((resolve) => …)` executor form (repo convention).
21
+
22
+ ### Verified
23
+ - `bun run typecheck` clean; `bun test` 2371 pass / 0 fail (3 new `handleUpdate` cases: paired-chat command dispatches, other-chat command is silently dropped including a `/cancel` attempt, no-text update is ignored; all pre-existing notify-daemon/session-endpoint/daemon-control suites pass unchanged against the refactor).
24
+
9
25
  ## [0.7.37] - 2026-07-05
10
26
  _Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack)._
11
27
 
package/README.ja.md CHANGED
@@ -207,11 +207,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
207
207
  ## 変更履歴 (Changelog)
208
208
 
209
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
210
211
  - **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
211
212
  - **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
212
213
  - **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
213
214
  - **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
214
- - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
215
215
 
216
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
217
217
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -207,11 +207,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
207
207
  ## 변경 이력 (Changelog)
208
208
 
209
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
210
211
  - **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
211
212
  - **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
212
213
  - **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
213
214
  - **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
214
- - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
215
215
 
216
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
217
217
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -234,11 +234,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
234
234
  ## Changelog
235
235
 
236
236
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
237
+ - **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
237
238
  - **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
238
239
  - **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
239
240
  - **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
240
241
  - **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
241
- - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
242
242
 
243
243
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
244
244
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -207,11 +207,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
207
207
  ## 更新日志 (Changelog)
208
208
 
209
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.38]** (2026-07-05) — Ponytail review of 0.7.37's Telegram daemon: closes a real trust-boundary gap (any Telegram user could steer/cancel subagents, not just the paired chat) and a `getUpdates`-failure hot-loop, plus bloat/style cleanup — no new surface area, no config changes.
210
211
  - **[0.7.37]** (2026-07-05) — Remote subagent visibility/control over Telegram (gjc Telegram-daemon parity, scoped to jeo's subagent surface only — no forum topics, no inline keyboards, no image attachments; see CHANGELOG 0.7.34 for what jeo intentionally does not replicate from gjc's full notification stack).
211
212
  - **[0.7.36]** (2026-07-04) — Removes two specific guardrails per explicit user direction: `jeo approve`'s human-only identity gate (the agent can now approve its own reviewed `jeo ralplan` plan) and the disk-staleness write/edit clobber guard (a write/edit no longer refuses just because the file changed on disk since the last read). Both removals were scoped narrowly after enumerating every guardrail in the codebase — the catastrophic-command hard block (`rm -rf /`, fork bombs, raw-disk `dd`/`mkfs`), the read-only subagent role tool gating, the deep-interview mutation lock, and the blind-edit (never-read-this-session) guard are all UNCHANGED.
212
213
  - **[0.7.35]** (2026-07-04) — `jeo team` now runs every plan step through the SAME subagent execution core as the `task`/`subagent` tools, and gains gjc-style concurrent workers: a plan can mark a contiguous run of independent steps to execute in parallel, each isolated in its own git worktree, merged back in order.
213
214
  - **[0.7.34]** (2026-07-04) — gajae-code 0.8.0/0.8.1 parity audit: closes a real cross-provider lone-surrogate JSON bug and makes turn-boundary compaction Esc-cancellable; the rest of gjc's 0.8.x release (Telegram daemon, ACP, cmux, coordinator MCP, tool-discovery-mode, an internal paged transcript viewport) is gjc-specific architecture jeo intentionally does not replicate, or was already-equivalent (confirmed by a 4-way parallel audit: ast_grep formatting, session-picker empty-filter clamping, PageUp/PageDown).
214
- - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
215
215
 
216
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
217
217
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.37",
3
+ "version": "0.7.38",
4
4
 
5
5
 
6
6
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
@@ -106,7 +106,9 @@ async function waitFor(predicate: () => Promise<boolean>, timeoutMs: number, ste
106
106
  const deadline = Date.now() + timeoutMs;
107
107
  while (Date.now() < deadline) {
108
108
  if (await predicate()) return true;
109
- await new Promise(r => setTimeout(r, stepMs));
109
+ const { promise, resolve } = Promise.withResolvers<void>();
110
+ setTimeout(resolve, stepMs);
111
+ await promise;
110
112
  }
111
113
  return false;
112
114
  }
@@ -28,14 +28,10 @@ import type { SubagentRecord, SubagentRegistry } from "../subagent-registry";
28
28
 
29
29
  const SNAPSHOT_POLL_MS = 1_200;
30
30
 
31
- interface WsData {
32
- authed: true;
33
- }
34
-
35
31
  export class SessionNotifyEndpoint {
36
- private server: Server<WsData> | undefined;
32
+ private server: Server<undefined> | undefined;
37
33
 
38
- private readonly sockets = new Set<ServerWebSocket<WsData>>();
34
+ private readonly sockets = new Set<ServerWebSocket<undefined>>();
39
35
  private pollTimer: ReturnType<typeof setInterval> | undefined;
40
36
  private lastSnapshotJson = "";
41
37
  readonly sessionId: string;
@@ -66,7 +62,7 @@ export class SessionNotifyEndpoint {
66
62
  }
67
63
  }
68
64
 
69
- private handleMessage(ws: ServerWebSocket<WsData>, raw: string | Buffer): void {
65
+ private handleMessage(ws: ServerWebSocket<undefined>, raw: string | Buffer): void {
70
66
  let msg: Record<string, unknown>;
71
67
  try {
72
68
  msg = JSON.parse(String(raw));
@@ -103,29 +99,27 @@ export class SessionNotifyEndpoint {
103
99
  * Idempotent no-op if already started. */
104
100
  async start(): Promise<void> {
105
101
  if (this.server) return;
106
- const self = this;
107
- this.server = Bun.serve<WsData>({
108
-
102
+ this.server = Bun.serve<undefined>({
109
103
  hostname: "127.0.0.1",
110
104
  port: 0,
111
- fetch(req, server) {
105
+ fetch: (req, server) => {
112
106
  const url = new URL(req.url);
113
- if (url.searchParams.get("token") !== self.token) {
107
+ if (url.searchParams.get("token") !== this.token) {
114
108
  return new Response("unauthorized", { status: 401 });
115
109
  }
116
- if (server.upgrade(req, { data: { authed: true } })) return undefined;
110
+ if (server.upgrade(req)) return undefined;
117
111
  return new Response("upgrade failed", { status: 500 });
118
112
  },
119
113
  websocket: {
120
114
  open: ws => {
121
- self.sockets.add(ws);
115
+ this.sockets.add(ws);
122
116
  try {
123
- ws.send(JSON.stringify(self.snapshot()));
117
+ ws.send(JSON.stringify(this.snapshot()));
124
118
  } catch {}
125
119
  },
126
- message: (ws, raw) => self.handleMessage(ws, raw),
120
+ message: (ws, raw) => this.handleMessage(ws, raw),
127
121
  close: ws => {
128
- self.sockets.delete(ws);
122
+ this.sockets.delete(ws);
129
123
  },
130
124
  },
131
125
  });
@@ -22,9 +22,15 @@ import * as path from "node:path";
22
22
  import { readGlobalConfig } from "../state";
23
23
  import { notifySessionsDir } from "./paths";
24
24
  import { acquireDaemonLock, isPidAlive } from "./daemon-control";
25
- import { TelegramApi } from "./telegram-api";
25
+ import { TelegramApi, type TelegramUpdate } from "./telegram-api";
26
26
  import type { SubagentRecord } from "../subagent-registry";
27
27
 
28
+ function sleep(ms: number): Promise<void> {
29
+ const { promise, resolve } = Promise.withResolvers<void>();
30
+ setTimeout(resolve, ms);
31
+ return promise;
32
+ }
33
+
28
34
  // ── Pure helpers (unit-testable without network/ws) ────────────────────────────
29
35
 
30
36
  export type NotifyEventKind = "started" | "completed" | "failed" | "cancelled";
@@ -53,7 +59,8 @@ export function diffSubagentTransitions(prev: SubagentRecord[], next: SubagentRe
53
59
  return events;
54
60
  }
55
61
 
56
- const NOTIFY_ICON: Record<NotifyEventKind, string> = { started: "▶", completed: "✅", failed: "❌", cancelled: "⏹" };
62
+ /** One icon per subagent status; a "started" event renders as the running icon. */
63
+ const STATUS_ICON: Record<SubagentRecord["status"], string> = { running: "▶", completed: "✅", failed: "❌", cancelled: "⏹" };
57
64
 
58
65
  function projectName(cwd: string): string {
59
66
  return cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd;
@@ -66,7 +73,7 @@ export function shortSessionId(sessionId: string): string {
66
73
  }
67
74
 
68
75
  export function formatNotifyEvent(shortId: string, cwd: string, ev: NotifyEvent): string {
69
- const header = `${NOTIFY_ICON[ev.kind]} [${shortId}] ${projectName(cwd)} · ${ev.record.role} '${ev.record.id}' ${ev.kind}`;
76
+ const header = `${STATUS_ICON[ev.kind === "started" ? "running" : ev.kind]} [${shortId}] ${projectName(cwd)} · ${ev.record.role} '${ev.record.id}' ${ev.kind}`;
70
77
  if (ev.kind === "started") return `${header}\n${ev.record.task}`;
71
78
  const resultPreview = (ev.record.result ?? "").slice(0, 500);
72
79
  return resultPreview ? `${header}\n${resultPreview}` : header;
@@ -79,29 +86,19 @@ export function formatSubagentsList(sessions: Array<{ sessionId: string; cwd: st
79
86
  for (const s of withRecords) {
80
87
  lines.push(`— ${shortSessionId(s.sessionId)} (${projectName(s.cwd)})`);
81
88
  for (const r of s.records) {
82
- const icon = r.status === "running" ? "▶" : r.status === "completed" ? "✅" : r.status === "failed" ? "❌" : "⏹";
83
- lines.push(` ${icon} ${r.role} '${r.id}': ${r.task}`);
89
+ lines.push(` ${STATUS_ICON[r.status]} ${r.role} '${r.id}': ${r.task}`);
84
90
  }
85
91
  }
86
92
  return lines.join("\n");
87
93
  }
88
94
 
89
- export interface ParsedSteerCommand {
90
- shortId: string;
91
- subagentId: string;
92
- message: string;
93
- }
94
- export function parseSteerCommand(text: string): ParsedSteerCommand | undefined {
95
+ export function parseSteerCommand(text: string): { shortId: string; subagentId: string; message: string } | undefined {
95
96
  const m = /^\/steer\s+(\S+)\s+(\S+)\s+([\s\S]+)$/.exec(text.trim());
96
97
  if (!m) return undefined;
97
98
  return { shortId: m[1]!, subagentId: m[2]!, message: m[3]!.trim() };
98
99
  }
99
100
 
100
- export interface ParsedCancelCommand {
101
- shortId: string;
102
- subagentId: string;
103
- }
104
- export function parseCancelCommand(text: string): ParsedCancelCommand | undefined {
101
+ export function parseCancelCommand(text: string): { shortId: string; subagentId: string } | undefined {
105
102
  const m = /^\/cancel\s+(\S+)\s+(\S+)\s*$/.exec(text.trim());
106
103
  if (!m) return undefined;
107
104
  return { shortId: m[1]!, subagentId: m[2]! };
@@ -232,23 +229,21 @@ export class TelegramDaemon {
232
229
 
233
230
  private sendRequest(conn: SessionConnection, frame: Record<string, unknown>): Promise<boolean> {
234
231
  const reqId = crypto.randomUUID();
235
- const timeoutMs = this.opts.ackTimeoutMs ?? 3_000;
236
- const p = new Promise<boolean>(resolve => {
237
- this.pendingAcks.set(reqId, resolve);
238
- setTimeout(() => {
239
- if (this.pendingAcks.has(reqId)) {
240
- this.pendingAcks.delete(reqId);
241
- resolve(false);
242
- }
243
- }, timeoutMs);
244
- });
232
+ const { promise, resolve } = Promise.withResolvers<boolean>();
233
+ this.pendingAcks.set(reqId, resolve);
234
+ setTimeout(() => {
235
+ if (this.pendingAcks.has(reqId)) {
236
+ this.pendingAcks.delete(reqId);
237
+ resolve(false);
238
+ }
239
+ }, this.opts.ackTimeoutMs ?? 3_000);
245
240
  try {
246
241
  conn.ws.send(JSON.stringify({ ...frame, reqId }));
247
242
  } catch {
248
243
  this.pendingAcks.delete(reqId);
249
- return Promise.resolve(false);
244
+ resolve(false);
250
245
  }
251
- return p;
246
+ return promise;
252
247
  }
253
248
 
254
249
  async handleInboundText(text: string): Promise<void> {
@@ -287,21 +282,35 @@ export class TelegramDaemon {
287
282
  if (trimmed.startsWith("/")) await this.notify(`Unrecognized command. ${HELP_TEXT}`);
288
283
  }
289
284
 
285
+ /** Trust boundary: a bot's username is publicly discoverable, so ANY Telegram
286
+ * user can message it. Only the paired chat (the one that ran
287
+ * `jeo notify setup`) may steer/cancel subagents; everything else is dropped
288
+ * without a reply (replying would leak that the bot is live). */
289
+ async handleUpdate(update: TelegramUpdate): Promise<void> {
290
+ const msg = update.message;
291
+ if (!msg?.text) return;
292
+ if (String(msg.chat.id) !== this.opts.chatId) return;
293
+ await this.handleInboundText(msg.text);
294
+ }
295
+
290
296
  private async pollTelegramLoop(): Promise<void> {
291
297
  while (!this.stopped) {
292
298
  let res;
293
299
  try {
294
300
  res = await this.opts.telegram.getUpdates(this.updateOffset, 25);
295
301
  } catch {
296
- await new Promise(r => setTimeout(r, 2_000));
302
+ await sleep(2_000);
297
303
  continue;
298
304
  }
299
- if (res.ok) {
300
- for (const update of res.result) {
301
- this.updateOffset = update.update_id + 1;
302
- const text = update.message?.text;
303
- if (text) await this.handleInboundText(text);
304
- }
305
+ if (!res.ok) {
306
+ // e.g. 409 Conflict (another getUpdates owner) or 401 (revoked token) —
307
+ // without a pause this would hot-loop against the API.
308
+ await sleep(2_000);
309
+ continue;
310
+ }
311
+ for (const update of res.result) {
312
+ this.updateOffset = update.update_id + 1;
313
+ await this.handleUpdate(update);
305
314
  }
306
315
  }
307
316
  }