jeo-code 0.7.37 → 0.7.39
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 +26 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- 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/src/util/provider-error.ts +6 -1
- package/src/util/retry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ 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.39] - 2026-07-05
|
|
10
|
+
_Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Anthropic's new `Refusal (reasoning_extraction): …` refusal message (Claude Fable 5) was not recognized as a refusal.** `isRefusalError` (`src/util/retry.ts`) only matched older shapes (`stop_reason=refusal`, `finish_reason=content_filter`, `(SAFETY)`, `(PROHIBITED_CONTENT)`, `(BLOCKLIST)`); Anthropic's `stop_details.category` refusal wording falls outside all of them, so `defaultRetryable` and `engine.ts`'s refusal ladder (free resend → context-reset + strip replayed thinking → strip `<project_context>` → abortable backoff) never engaged, and `friendlyProviderError` fell through to a raw passthrough — the user saw the bare provider text and the turn died. The regex now also matches the general `Refusal (<category>):` structural prefix so any current or future `stop_details.category` value is covered, not just `reasoning_extraction`. `friendlyProviderError` additionally appends a clarifying note specifically for `reasoning_extraction`: this category's wording reads like an accusation of extracting the model's internal reasoning, which is a common false positive for jeo's own thinking-block replay across multi-step tool use, not an actual violation.
|
|
14
|
+
|
|
15
|
+
### Verified
|
|
16
|
+
- `bun run typecheck` clean; full suite green.
|
|
17
|
+
- New coverage in `test/refusal-recovery.test.ts`: `isRefusalError` recognizes the new shape (plus a synthetic future category, proving the fix isn't overfit to one literal string) while still matching every pre-existing shape and never false-positiving on an unrelated error; `friendlyProviderError` category-aware messaging; an end-to-end refusal-ladder engagement test via `runAgentLoop` with a mocked provider throwing the new shape.
|
|
18
|
+
|
|
19
|
+
## [0.7.38] - 2026-07-05
|
|
20
|
+
_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._
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **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.
|
|
24
|
+
- **`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)`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `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).
|
|
28
|
+
- `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.
|
|
29
|
+
- `parseSteerCommand`/`parseCancelCommand` return inline object types instead of the one-call-site `ParsedSteerCommand`/`ParsedCancelCommand` exported interfaces (no external caller used either name).
|
|
30
|
+
- `TelegramDaemon#sendRequest` and `daemon-control.ts`'s `waitFor` now use `Promise.withResolvers()` instead of the `new Promise((resolve) => …)` executor form (repo convention).
|
|
31
|
+
|
|
32
|
+
### Verified
|
|
33
|
+
- `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).
|
|
34
|
+
|
|
9
35
|
## [0.7.37] - 2026-07-05
|
|
10
36
|
_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
37
|
|
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.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
211
|
+
- **[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
212
|
- **[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
213
|
- **[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
214
|
- **[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
|
-
- **[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.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
211
|
+
- **[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
212
|
- **[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
213
|
- **[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
214
|
- **[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
|
-
- **[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.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
238
|
+
- **[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
239
|
- **[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
240
|
- **[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
241
|
- **[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
|
-
- **[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.39]** (2026-07-05) — Fixes an unrecovered Anthropic refusal on Claude Fable 5: the new `stop_details.category: "reasoning_extraction"` refusal shape wasn't recognized, so the turn died with a raw, unfriendly error instead of engaging jeo's existing context-reset refusal-recovery ladder.
|
|
211
|
+
- **[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
212
|
- **[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
213
|
- **[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
214
|
- **[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
|
-
- **[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
|
}
|
|
@@ -61,7 +61,12 @@ export function friendlyProviderError(err: unknown): string {
|
|
|
61
61
|
return `${provider} rejected the request: the conversation no longer fits the model's context window. Run /compact, drop large attachments, or start a fresh session.`;
|
|
62
62
|
}
|
|
63
63
|
if (isRefusalError(err)) {
|
|
64
|
-
|
|
64
|
+
const base = `${provider} declined to answer (safety refusal — no content returned). Usually a content classifier tripped on recently read file/search content: /retry, /compact or /new to drop the triggering context, or switch model with /model. If this persists on a Claude subscription (OAuth) login, Anthropic restricts third-party OAuth clients — set ANTHROPIC_API_KEY or use another provider.`;
|
|
65
|
+
const category = /Refusal \((\w+)\)/i.exec(msg)?.[1];
|
|
66
|
+
if (category === "reasoning_extraction") {
|
|
67
|
+
return `${base} (Category: reasoning_extraction — this classifier flags requests that look like probing the model's internal reasoning; jeo's own thinking-block replay across multi-step tool use is a common benign trigger, not an actual violation. /retry after a /compact usually clears it.)`;
|
|
68
|
+
}
|
|
69
|
+
return base;
|
|
65
70
|
}
|
|
66
71
|
if (status === 404) {
|
|
67
72
|
return `${provider} does not recognize the requested model (HTTP 404). The id may be retired, gated, or mistyped — pick another with /model.`;
|
package/src/util/retry.ts
CHANGED
|
@@ -282,5 +282,5 @@ export function isUsageLimitError(err: unknown): boolean {
|
|
|
282
282
|
* capped backoff — gjc parity: a refusal is never terminal) is the correct recovery
|
|
283
283
|
* layer because it MUTATES the context between attempts. */
|
|
284
284
|
export function isRefusalError(err: unknown): boolean {
|
|
285
|
-
return /stop_reason=refusal|finish_reason=content_filter|\(content_filter\)|\(SAFETY\)|\(PROHIBITED_CONTENT\)|\(BLOCKLIST\)/i.test(errorMessageOf(err));
|
|
285
|
+
return /stop_reason=refusal|finish_reason=content_filter|\(content_filter\)|\(SAFETY\)|\(PROHIBITED_CONTENT\)|\(BLOCKLIST\)|Refusal \(\w+\)/i.test(errorMessageOf(err));
|
|
286
286
|
}
|