jeo-code 0.12.8 → 0.12.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/CHANGELOG.md +11 -1
- 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 +2 -1
- package/src/agent/notify/discord-attachments.ts +57 -9
- package/src/commands/launch/flags.ts +2 -1
- package/src/commands/launch.ts +13 -2
- package/src/commands/messenger.ts +10 -1
- package/src/messenger/config.ts +9 -2
- package/src/messenger/model-policy.ts +8 -0
- package/src/messenger/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [0.12.10] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Messenger computer use** — explicitly allow authorized messenger sessions to use the host computer tool, independently of the global computer setting. Host desktop and filesystem access are shared and not sandboxed.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Literal worker tool isolation** — prevent environment-provided tool lists from bypassing a messenger worker's explicit allowlist.
|
|
17
|
+
|
|
18
|
+
## [0.12.9] - 2026-11-02
|
|
19
|
+
≔14c4+
|
|
20
|
+
- **Discord model policy** — optionally pin a Discord worker model and fall back on persistent usage or quota exhaustion before any tool execution; other platforms and global routing retain their own settings.
|
|
11
21
|
|
|
12
22
|
### Added
|
|
13
23
|
- **Discord session files and control** — local opt-in enables authorized Discord prompts to use host session tools and exchange staged attachments; see the [Discord guide](docs/integrations/discord.md) for limits, public visibility, and revocation boundaries.
|
package/README.ja.md
CHANGED
|
@@ -417,11 +417,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
417
417
|
## 変更履歴 (Changelog)
|
|
418
418
|
|
|
419
419
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
420
|
-
- **[
|
|
420
|
+
- **[0.12.10]** (2026-11-02)
|
|
421
|
+
- **[0.12.9]** (2026-11-02)
|
|
421
422
|
- **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
|
|
422
423
|
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
423
424
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
424
|
-
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
425
425
|
|
|
426
426
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
427
427
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -418,11 +418,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
418
418
|
## 변경 이력 (Changelog)
|
|
419
419
|
|
|
420
420
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
421
|
-
- **[
|
|
421
|
+
- **[0.12.10]** (2026-11-02)
|
|
422
|
+
- **[0.12.9]** (2026-11-02)
|
|
422
423
|
- **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
|
|
423
424
|
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
424
425
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
425
|
-
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
426
426
|
|
|
427
427
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
428
428
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -410,11 +410,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
410
410
|
## Changelog
|
|
411
411
|
|
|
412
412
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
413
|
-
- **[
|
|
413
|
+
- **[0.12.10]** (2026-11-02)
|
|
414
|
+
- **[0.12.9]** (2026-11-02)
|
|
414
415
|
- **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
|
|
415
416
|
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
416
417
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
417
|
-
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
418
418
|
|
|
419
419
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
420
420
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -415,11 +415,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
415
415
|
## 更新日志 (Changelog)
|
|
416
416
|
|
|
417
417
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
418
|
-
- **[
|
|
418
|
+
- **[0.12.10]** (2026-11-02)
|
|
419
|
+
- **[0.12.9]** (2026-11-02)
|
|
419
420
|
- **[0.12.7]** (2026-10-18) — Repair desktop screenshot vision feedback, macOS scrolling, complete drag gestures, and keyboard normalization.
|
|
420
421
|
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
421
422
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
422
|
-
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
423
423
|
|
|
424
424
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
425
425
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeo-code",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"description": "Clean, highly optimized AI coding agent using spec-first loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.ts",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"changelog:sync": "bun scripts/sync-changelog.ts",
|
|
54
54
|
"test": "bun test",
|
|
55
55
|
"verify:browser": "bun scripts/verify-browser.ts",
|
|
56
|
+
"verify:discord": "bun scripts/verify-discord-integration.ts",
|
|
56
57
|
"verify:tmux": "bash scripts/tmux-verify.sh",
|
|
57
58
|
"dev:link": "bun scripts/dev-link.ts link",
|
|
58
59
|
"dev:doctor": "bun scripts/dev-link.ts doctor"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** Limit enforced on presented attachment entries, before validation or deduplication. */
|
|
5
|
+
export const MAX_ARTIFACTS = 10;
|
|
5
6
|
const MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
6
7
|
const MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
7
|
-
const
|
|
8
|
+
export const DISCORD_ATTACHMENT_CDN_HOSTS = ["cdn.discordapp.com", "media.discordapp.net"] as const;
|
|
9
|
+
export const MAX_DISCORD_ATTACHMENT_REDIRECTS = 5;
|
|
8
10
|
|
|
9
11
|
interface Attachment {
|
|
10
12
|
filename: string;
|
|
@@ -23,12 +25,62 @@ export interface DiscordAttachmentStage {
|
|
|
23
25
|
cleanup: () => Promise<void>;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Parse and validate one Discord CDN URL without accepting normalization that
|
|
30
|
+
* could conceal an ambiguous authority. Host matching is exact after URL's
|
|
31
|
+
* ASCII/case normalization; Unicode, percent-encoded authorities, trailing
|
|
32
|
+
* dots, user-info, and explicit ports are rejected.
|
|
33
|
+
*/
|
|
34
|
+
export function validateDiscordAttachmentUrl(value: string): URL {
|
|
35
|
+
if (typeof value !== "string") throw new Error("Untrusted Discord attachment URL");
|
|
36
|
+
const authority = value.match(/^https:\/\/([^/?#]*)/i)?.[1];
|
|
37
|
+
if (!authority || /[^\x00-\x7f]/.test(authority) || authority.includes("%") || authority.endsWith(".")) {
|
|
38
|
+
throw new Error("Untrusted Discord attachment URL");
|
|
39
|
+
}
|
|
40
|
+
if (authority.includes("@") || authority.includes(":")) throw new Error("Untrusted Discord attachment URL");
|
|
41
|
+
let url: URL;
|
|
42
|
+
try { url = new URL(value); } catch { throw new Error("Invalid Discord attachment URL"); }
|
|
43
|
+
if (url.protocol !== "https:" ||
|
|
44
|
+
!(DISCORD_ATTACHMENT_CDN_HOSTS as readonly string[]).includes(url.hostname) ||
|
|
45
|
+
url.username || url.password || url.port) {
|
|
46
|
+
throw new Error("Untrusted Discord attachment URL");
|
|
47
|
+
}
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchTrustedAttachment(
|
|
52
|
+
initialUrl: string,
|
|
53
|
+
fetchImpl: typeof fetch,
|
|
54
|
+
signal: AbortSignal,
|
|
55
|
+
): Promise<Response> {
|
|
56
|
+
let current = validateDiscordAttachmentUrl(initialUrl);
|
|
57
|
+
for (let redirects = 0; ; redirects++) {
|
|
58
|
+
const response = await fetchImpl(current.href, { redirect: "manual", signal });
|
|
59
|
+
if (response.status >= 300 && response.status < 400) {
|
|
60
|
+
const location = response.headers.get("location");
|
|
61
|
+
await response.body?.cancel().catch(() => {});
|
|
62
|
+
if (!location || redirects >= MAX_DISCORD_ATTACHMENT_REDIRECTS) throw new Error("Discord attachment redirect rejected");
|
|
63
|
+
// Validate an authority-bearing Location before URL resolution can erase
|
|
64
|
+
// percent-encoding, IDNA, case, or default-port evidence.
|
|
65
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(location)) validateDiscordAttachmentUrl(location);
|
|
66
|
+
else if (location.startsWith("//")) validateDiscordAttachmentUrl(`${current.protocol}${location}`);
|
|
67
|
+
current = validateDiscordAttachmentUrl(new URL(location, current).href);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Native fetch exposes the final URL. Test doubles commonly leave it blank.
|
|
71
|
+
if (response.url) validateDiscordAttachmentUrl(response.url);
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
26
76
|
/** Staged files are private and must be removed with cleanup after the request finishes. */
|
|
27
77
|
export async function stageDiscordAttachments(
|
|
28
78
|
attachments: unknown[],
|
|
29
79
|
options: DiscordAttachmentOptions,
|
|
30
80
|
): Promise<DiscordAttachmentStage> {
|
|
31
|
-
|
|
81
|
+
// Every presented entry consumes the limit, including duplicates and entries
|
|
82
|
+
// that would later fail metadata validation. Overflow rejects the whole input.
|
|
83
|
+
if (!Array.isArray(attachments) || attachments.length > MAX_ARTIFACTS) throw new Error("Invalid Discord attachment count");
|
|
32
84
|
const validated: Attachment[] = [];
|
|
33
85
|
let declaredTotal = 0;
|
|
34
86
|
for (const value of attachments) {
|
|
@@ -38,11 +90,7 @@ export async function stageDiscordAttachments(
|
|
|
38
90
|
!Number.isSafeInteger(item.size) || (item.size as number) < 0 || (item.size as number) > MAX_FILE_BYTES) {
|
|
39
91
|
throw new Error("Invalid Discord attachment metadata");
|
|
40
92
|
}
|
|
41
|
-
|
|
42
|
-
try { url = new URL(item.url); } catch { throw new Error("Invalid Discord attachment URL"); }
|
|
43
|
-
if (url.protocol !== "https:" || !CDN_HOSTS.includes(url.hostname) || url.username || url.password || url.port) {
|
|
44
|
-
throw new Error("Untrusted Discord attachment URL");
|
|
45
|
-
}
|
|
93
|
+
const url = validateDiscordAttachmentUrl(item.url);
|
|
46
94
|
declaredTotal += item.size as number;
|
|
47
95
|
if (declaredTotal > MAX_TOTAL_BYTES) throw new Error("Discord attachments exceed total size limit");
|
|
48
96
|
validated.push({ filename: item.filename, url: url.href, size: item.size as number });
|
|
@@ -69,7 +117,7 @@ export async function stageDiscordAttachments(
|
|
|
69
117
|
const signal = AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(30_000)]);
|
|
70
118
|
let response: Response;
|
|
71
119
|
try {
|
|
72
|
-
response = await (options.fetchImpl ?? fetch
|
|
120
|
+
response = await fetchTrustedAttachment(item.url, options.fetchImpl ?? fetch, signal);
|
|
73
121
|
} catch {
|
|
74
122
|
throw new Error("Discord attachment download failed or aborted");
|
|
75
123
|
}
|
|
@@ -252,12 +252,13 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
252
252
|
find: "find {globPattern} — find files by name",
|
|
253
253
|
search: "search {pattern, globPattern?, ignoreCase?, context?, maxMatches?} — grep (context: N lines around each match)",
|
|
254
254
|
ls: "ls {dirPath} — list a directory's entries (dirs first)",
|
|
255
|
+
computer: "computer {action, x?, y?, text?, key?, deltaX?, deltaY?, duration?, actions?} — desktop automation (screenshot, click, double_click, move, drag, scroll, type, keypress, wait, batch)",
|
|
255
256
|
};
|
|
256
257
|
|
|
257
258
|
export function buildToolProtocol(allowedTools: Set<string>): string {
|
|
258
259
|
const lines: string[] = ["You have these tools (call exactly ONE per step):"];
|
|
259
260
|
let num = 1;
|
|
260
|
-
for (const name of ["read", "write", "edit", "bash", "find", "search", "ls"]) {
|
|
261
|
+
for (const name of ["read", "write", "edit", "bash", "find", "search", "ls", "computer"]) {
|
|
261
262
|
if (allowedTools.has(name)) {
|
|
262
263
|
lines.push(`${num}. ${TOOL_DESCRIPTIONS[name]}`);
|
|
263
264
|
num++;
|
package/src/commands/launch.ts
CHANGED
|
@@ -67,6 +67,7 @@ import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFrom
|
|
|
67
67
|
import { RouteHistory } from "../agent/route-history";
|
|
68
68
|
|
|
69
69
|
import { isRateLimitError, isUsageLimitError, isConnectionError } from "../util/retry";
|
|
70
|
+
import { messengerUsageFallback } from "../messenger/model-policy";
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
import { listAliases } from "../ai/model-registry";
|
|
@@ -738,7 +739,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
738
739
|
// never persisted to config.
|
|
739
740
|
const routeHistory = new RouteHistory();
|
|
740
741
|
// Computer-use session override: undefined = follow config.computer.enabled, true/false = /computer on|off wins this session.
|
|
741
|
-
let sessionComputerOverride: boolean | undefined;
|
|
742
|
+
let sessionComputerOverride: boolean | undefined = flags.literalWorker && process.env.JEO_MESSENGER_COMPUTER_ENABLED === "1" ? true : undefined;
|
|
742
743
|
|
|
743
744
|
// Cache of live, credential-validated models per provider (refreshed by live pickers).
|
|
744
745
|
// Every successful discovery is also persisted (`writeModelCache`), so the NEXT launch
|
|
@@ -1241,7 +1242,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1241
1242
|
// global default (rememberModelPatch), so re-reading turnConfig.defaultModel here let
|
|
1242
1243
|
// that write silently switch THIS running session's model mid-run. Per-session model
|
|
1243
1244
|
// selection must stay session-local: running sessions never influence each other.
|
|
1244
|
-
const routingEnabled =
|
|
1245
|
+
const routingEnabled = flags.literalWorker && process.env.JEO_MESSENGER_FIXED_MODEL === "1"
|
|
1246
|
+
? false : sessionRouteOverride ?? !!turnConfig.routing?.enabled;
|
|
1245
1247
|
let routeCredentialNotice: string | undefined;
|
|
1246
1248
|
let routeVetoNote: string | undefined;
|
|
1247
1249
|
// Credential SCOPES that have shown an ACCOUNT-LEVEL failure (rate limit,
|
|
@@ -1968,6 +1970,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1968
1970
|
return null;
|
|
1969
1971
|
};
|
|
1970
1972
|
result = await runLoopWithModel(activeModel, activeThinking, turnSessionKey);
|
|
1973
|
+
const messengerFallback = flags.literalWorker && process.env.JEO_MESSENGER_FIXED_MODEL === "1"
|
|
1974
|
+
? messengerUsageFallback(process.env.JEO_MESSENGER_USAGE_FALLBACK_MODEL, activeModel, result)
|
|
1975
|
+
: undefined;
|
|
1976
|
+
if (messengerFallback) {
|
|
1977
|
+
activeModel = messengerFallback;
|
|
1978
|
+
turnSessionKey = sessionId ? deriveCacheSessionKey(sessionId, activeModel) : undefined;
|
|
1979
|
+
contextTokens = catalogMetadata(activeModel)?.contextTokens;
|
|
1980
|
+
result = await runLoopWithModel(activeModel, activeThinking, turnSessionKey);
|
|
1981
|
+
}
|
|
1971
1982
|
const routeFallbackMaxRaw = Number(jeoEnv("ROUTE_FALLBACK_MAX_ATTEMPTS") ?? NaN);
|
|
1972
1983
|
const routeFallbackMax = Number.isFinite(routeFallbackMaxRaw) && routeFallbackMaxRaw >= 0
|
|
1973
1984
|
? Math.floor(routeFallbackMaxRaw)
|
|
@@ -98,7 +98,9 @@ export function createMessengerCliReply(config: MessengerConfig, spawn: typeof B
|
|
|
98
98
|
const sourceEntry = process.argv[1]?.endsWith(".ts") ? process.argv[1] : undefined;
|
|
99
99
|
const tools = [...new Set(config.toolNames.map(name => name === "grep" ? "search" : name === "glob" ? "find" : name))];
|
|
100
100
|
const toolFlags = tools.length ? ["--tools", tools.join(",")] : ["--no-tools"];
|
|
101
|
-
const
|
|
101
|
+
const platformConfig = config.platforms[message.platform];
|
|
102
|
+
const modelFlags = platformConfig?.model ? ["--model", platformConfig.model] : [];
|
|
103
|
+
const command = [executable, ...(sourceEntry ? ["--no-env-file", sourceEntry] : []), "launch", "--print", "--no-tui", "--no-skills", "--literal-worker", ...toolFlags, ...modelFlags, "--resume", id, "--", "worker"];
|
|
102
104
|
const secretNames = new Set<string>();
|
|
103
105
|
for (const platform of Object.values(config.platforms)) {
|
|
104
106
|
if (!platform) continue;
|
|
@@ -112,6 +114,13 @@ export function createMessengerCliReply(config: MessengerConfig, spawn: typeof B
|
|
|
112
114
|
const env = Object.fromEntries(Object.entries(process.env).filter(([name, value]) => value !== undefined && !secretNames.has(name))) as Record<string, string>;
|
|
113
115
|
for (const name of secretNames) env[name] = "";
|
|
114
116
|
env.JEO_SESSION_DIR = config.sessionDir;
|
|
117
|
+
// The explicit tool allowlist is the opt-in for this worker only.
|
|
118
|
+
env.JEO_MESSENGER_COMPUTER_ENABLED = tools.includes("computer") ? "1" : "0";
|
|
119
|
+
// Never inherit another platform's fallback policy from the gateway environment.
|
|
120
|
+
delete env.JEO_MESSENGER_USAGE_FALLBACK_MODEL;
|
|
121
|
+
if (platformConfig?.usageFallbackModel) env.JEO_MESSENGER_USAGE_FALLBACK_MODEL = platformConfig.usageFallbackModel;
|
|
122
|
+
delete env.JEO_MESSENGER_FIXED_MODEL;
|
|
123
|
+
if (platformConfig?.model) env.JEO_MESSENGER_FIXED_MODEL = "1";
|
|
115
124
|
return pool.turn(id, command, config.cwd, env, [message.text, artifactPrompt].filter(Boolean).join("\n\n"), signal, onActivity);
|
|
116
125
|
};
|
|
117
126
|
return Object.assign(reply, { close: pool.close });
|
package/src/messenger/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ const TOOLS: Record<string, true> = {
|
|
|
12
12
|
bash: true,
|
|
13
13
|
edit: true,
|
|
14
14
|
write: true,
|
|
15
|
+
computer: true,
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
function object(value: unknown, label: string): Record<string, unknown> {
|
|
@@ -75,7 +76,7 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
|
|
|
75
76
|
platform === "slack"
|
|
76
77
|
? ["tokenEnv", "appTokenEnv", "allowedUserIds", "allowedChannelIds"]
|
|
77
78
|
: platform === "discord"
|
|
78
|
-
? ["tokenEnv", "allowedUserIds", "allowedChannelIds", "artifactDir"]
|
|
79
|
+
? ["tokenEnv", "allowedUserIds", "allowedChannelIds", "artifactDir", "model", "usageFallbackModel"]
|
|
79
80
|
: ["tokenEnv", "allowedUserIds", "allowedChannelIds"],
|
|
80
81
|
`${platform} config`,
|
|
81
82
|
);
|
|
@@ -89,6 +90,12 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
|
|
|
89
90
|
config.allowedChannelIds = ids(item.allowedChannelIds, `${platform}.allowedChannelIds`);
|
|
90
91
|
if (platform === "discord" && item.artifactDir !== undefined)
|
|
91
92
|
config.artifactDir = path.resolve(baseDir, string(item.artifactDir, "discord.artifactDir"));
|
|
93
|
+
if (platform === "discord" && item.model !== undefined)
|
|
94
|
+
config.model = string(item.model, "discord.model");
|
|
95
|
+
if (platform === "discord" && item.usageFallbackModel !== undefined) {
|
|
96
|
+
if (!config.model) throw new Error("discord.usageFallbackModel requires discord.model");
|
|
97
|
+
config.usageFallbackModel = string(item.usageFallbackModel, "discord.usageFallbackModel");
|
|
98
|
+
}
|
|
92
99
|
platforms[platform] = config;
|
|
93
100
|
}
|
|
94
101
|
const toolNames = input.toolNames === undefined ? [] : input.toolNames;
|
|
@@ -97,7 +104,7 @@ export function parseMessengerConfig(value: unknown, baseDir: string): Messenger
|
|
|
97
104
|
toolNames.some(name => typeof name !== "string" || !Object.hasOwn(TOOLS, name)) ||
|
|
98
105
|
new Set(toolNames).size !== toolNames.length
|
|
99
106
|
) {
|
|
100
|
-
throw new Error("toolNames must be a unique list drawn from read, grep, glob, bash, edit, write");
|
|
107
|
+
throw new Error("toolNames must be a unique list drawn from read, grep, glob, find, search, bash, edit, write, computer");
|
|
101
108
|
}
|
|
102
109
|
return {
|
|
103
110
|
cwd: path.resolve(baseDir, input.cwd === undefined ? "." : string(input.cwd, "cwd")),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isUsageLimitError } from "../util/retry";
|
|
2
|
+
|
|
3
|
+
/** Only retry a worker's first failed step, before any tools could have executed. */
|
|
4
|
+
export function messengerUsageFallback(fallback: string | undefined, current: string, result: { done: boolean; steps: number; stopClass?: unknown; doneReason?: string }): string | undefined {
|
|
5
|
+
if (!fallback || fallback === current || result.done || result.stopClass || result.steps !== 1) return undefined;
|
|
6
|
+
const reason = result.doneReason ?? "";
|
|
7
|
+
return isUsageLimitError(reason) || /usage\/quota limit reached/i.test(reason) ? fallback : undefined;
|
|
8
|
+
}
|
package/src/messenger/types.ts
CHANGED
|
@@ -45,6 +45,9 @@ export interface MessengerPlatformConfig {
|
|
|
45
45
|
allowedChannelIds?: string[];
|
|
46
46
|
/** Dedicated operator-approved directory for Discord output files. */
|
|
47
47
|
artifactDir?: string;
|
|
48
|
+
/** Optional platform-specific model and persistent-usage-limit fallback. */
|
|
49
|
+
model?: string;
|
|
50
|
+
usageFallbackModel?: string;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface MessengerConfig {
|