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 +16 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/notify/daemon-control.ts +3 -1
- package/src/agent/notify/session-endpoint.ts +11 -17
- package/src/agent/notify/telegram-daemon.ts +44 -35
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
|
@@ -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
|
-
|
|
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<
|
|
32
|
+
private server: Server<undefined> | undefined;
|
|
37
33
|
|
|
38
|
-
private readonly sockets = new Set<ServerWebSocket<
|
|
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<
|
|
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
|
-
|
|
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") !==
|
|
107
|
+
if (url.searchParams.get("token") !== this.token) {
|
|
114
108
|
return new Response("unauthorized", { status: 401 });
|
|
115
109
|
}
|
|
116
|
-
if (server.upgrade(req
|
|
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
|
-
|
|
115
|
+
this.sockets.add(ws);
|
|
122
116
|
try {
|
|
123
|
-
ws.send(JSON.stringify(
|
|
117
|
+
ws.send(JSON.stringify(this.snapshot()));
|
|
124
118
|
} catch {}
|
|
125
119
|
},
|
|
126
|
-
message: (ws, raw) =>
|
|
120
|
+
message: (ws, raw) => this.handleMessage(ws, raw),
|
|
127
121
|
close: ws => {
|
|
128
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
+
resolve(false);
|
|
250
245
|
}
|
|
251
|
-
return
|
|
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
|
|
302
|
+
await sleep(2_000);
|
|
297
303
|
continue;
|
|
298
304
|
}
|
|
299
|
-
if (res.ok) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
}
|