dsh-agy-link 0.4.43 → 0.4.44
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.md +2 -0
- package/dist/client.js +47 -1
- package/dist/index.js +119 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.44 (2026-10-09)
|
|
4
|
+
|
|
5
|
+
### English
|
|
6
|
+
|
|
7
|
+
- **Fix: a conversation stalled on a tool step no longer dead-ends after a host restart.** Continuation spans resolve their run from an **in-memory** `RunRegistry`; after DSH restarts (or the run is evicted) the recording is gone, and `stream()` used to fail that turn — and every retry of it — with "agy run … is no longer available (server restarted?) — please resend your message", an advice that could never succeed because the stale tool result stays at the end of the message list. The adapter now logs a warning and falls back to fresh-turn prompt assembly instead: the history digest carries the recorded tool result, and the session's `sessions.json` binding still points at the same agy conversation, so the next turn heals the conversation with no user action. Related hardening in the same change: (1) the `agy_tool` mirror now bypasses DSH's auto-review / permission gates via a `tools/pre-execute` hook (it only replays output agy already executed — routing it through the reviewer caused redundant review LLM calls, timeouts, and crashes on reviewer tool-call endings); (2) review-mode calls (`REVIEW_POLICY` system prompt) and calls with explicit `tools: []` are treated as auxiliary and never cut spans on tools; (3) on Windows, `.mjs`/`.js`/`.cjs` bins are spawned via `process.execPath` instead of failing with `EFTYPE` (this also makes the full test suite pass natively on Windows — the 33 pre-existing platform failures are gone). (PR #40)
|
|
8
|
+
- **Add: hide models from the DSH model picker (`hiddenModels`).** The Antigravity settings section (below account-pool scheduling) now renders a "Model visibility" row with one toggle per known model: hidden models disappear from the `/model` picker, everything else stays visible (deny-list semantics, so newly discovered models appear by default). Hiding is picker-only by design — `resolveModel` still accepts hidden ids, so in-flight sessions and direct model references keep working, and no catalog-cache invalidation is needed. The list is served from `/status` (`catalogModels` + `hiddenModels`) through the same sanitizer that feeds `listModels`, so the UI and the picker can never disagree; writes go through the existing `POST /config` allowlist (validated as an array of model ids) and persist to the runtime-overrides file immediately. `DSH_AGY_HIDDEN_MODELS` (comma-separated) also works as an env fallback. The plugin now also emits the payload-free `llm/adapters-updated` event whenever picker output can change (`hiddenModels` writes, boot-probe discovery, re-login, manual quota refresh) — the webview picker caches its catalog snapshot and only re-reads after that event, so without it toggles would not show up until the next app restart. (PR #39)
|
|
9
|
+
- **Fix: the Windows node-shim spawn merged from #40 now really runs the script instead of booting a second app instance.** `process.execPath` is the Electron binary in the packaged desktop host, so the `isNode` branch now sets `ELECTRON_RUN_AS_NODE=1`; plain node ignores the variable, so POSIX and the test suite are unaffected.
|
|
10
|
+
|
|
11
|
+
### 中文 (Chinese)
|
|
12
|
+
|
|
13
|
+
- **修复:停在工具步骤上的会话在宿主重启后不再进入死胡同。** 续接 span 依赖的 run 录像存放在**纯内存**的 `RunRegistry` 里;DSH 重启(或录像被淘汰)后录像即消失,`stream()` 会让该轮失败——而且每次重试都失败,报 "agy run … is no longer available (server restarted?) — please resend your message"。这个"请重发消息"的建议永远不可能成功:失效的工具结果始终钉在消息列表末尾,续接检测每轮都命中同一个 runId。现在适配器会记录一条警告并**回退到全新轮次的 prompt 组装**:历史 digest 携带已记录的工具结果,会话在 `sessions.json` 里的 conversation 绑定仍指向同一个 agy 会话,下一轮即自动自愈,无需用户做任何事。同一批提交中的相关加固:(1) `agy_tool` 镜像现在通过 `tools/pre-execute` 钩子绕过 DSH 的 auto-review / 权限门(它只是重放 agy 已经执行过的输出——送审会导致重复审查 LLM 调用、审查超时,以及审查员以 tool-calls 结尾时的崩溃);(2) 审查模式调用(`REVIEW_POLICY` 系统提示)与显式 `tools: []` 的调用按辅助调用对待,不再在工具步骤上切分 span;(3) Windows 下 `.mjs`/`.js`/`.cjs` 形式的 bin 改为经 `process.execPath` 启动,不再触发 `EFTYPE`(这也让整个测试套件在 Windows 上原生全绿——33 个既有平台失败清零)。(PR #40)
|
|
14
|
+
- **新增:在 DSH 模型选择器中隐藏指定模型(`hiddenModels`)。** Antigravity 设置区(号池调度下方)新增"模型显示"一行,每个已知模型一个开关:关闭的模型从 `/model` 选择器中消失,其余照常显示(黑名单语义——新发现的模型默认可见)。隐藏只作用于选择器:`resolveModel` 仍接受被隐藏的模型 id,进行中的会话与直接引用不受影响,也无需清空模型目录缓存。模型列表由 `/status`(`catalogModels` + `hiddenModels`)提供,与 `listModels` 共用同一清洗逻辑,UI 与选择器永远不会不一致;写入走现有 `POST /config` 白名单(校验必须是模型 id 数组),立即持久化到 runtime-overrides 文件。环境变量 `DSH_AGY_HIDDEN_MODELS`(逗号分隔)亦可作为回退。插件现在会在所有会影响选择器输出的时机发出无负载的 `llm/adapters-updated` 事件(`hiddenModels` 写入、启动探测发现模型、重新登录、手动刷新额度)——webview 的模型选择器缓存目录快照、只在该事件后重读,缺少它时开关要到下次重启应用才会生效。(PR #39)
|
|
15
|
+
- **修复:合入自 #40 的 Windows node 脚本启动现在真正执行脚本,而不是再启动一个 App 实例。** 打包桌面端里 `process.execPath` 是 Electron 二进制,因此 `isNode` 分支现在会设置 `ELECTRON_RUN_AS_NODE=1`;纯 node 会忽略该变量,POSIX 与测试不受影响。
|
|
16
|
+
|
|
3
17
|
## 0.4.43 (2026-10-06)
|
|
4
18
|
|
|
5
19
|
### English
|
|
6
20
|
|
|
21
|
+
|
|
7
22
|
- **Fix (Windows): Chinese tool output is no longer mangled (issue #37).** Windows pipes carry bytes in the child's console code page: on a zh-CN system agy captures pwsh output as CP936 (GBK), and the plugin decoded every stream as UTF-8 — so the Chinese refusal text arrived as mojibake, and because that text is what agy stores in its conversation database the damage was permanent. stdout/stderr are now read as raw bytes and decoded **per line** through the console code page (`chcp` probe → GBK/Big5/Shift_JIS/EUC-KR/Windows-125x), falling back to UTF-8 whenever a line is valid UTF-8; POSIX behaviour is bit-for-bit unchanged (the probe returns `utf-8`). Lines — not chunks — are the decoding unit because a multi-byte character can straddle a chunk boundary but never a newline.
|
|
8
23
|
- **Fix: `dsh plugin add git+https://…` installs now build the plugin (issue #38).** `dist/` is not committed and only `prepack` existed, which never runs for git dependencies — so a hub/git install produced a package without `dist/` and failed to load. A `prepare` script now builds on git installs (npm/pnpm install devDependencies for git deps first); npm consumers are unaffected because `prepare` does not run for published tarballs. The install also aborted **earlier** than that: `@deepseek-ai/dsh-client-locale` was declared as a **non-optional** peer, so npm's automatic peer installation tried to fetch the unpublished `@deepseek-ai/dsh-paths@^0.0.1-rc.1` chain from the public registry and failed with a 404 — every `@deepseek-ai/*` peer is now optional (the host provides them at runtime and the bundle keeps them external), matching how third-party DSH plugins declare theirs.
|
|
9
24
|
- **Fix: the live "User location is not supported for the API use" refusal is now recognized, explained and routed around.** agy exits 3 with a `FAILED_PRECONDITION (code 400)` result envelope for this refusal, but the classifier only knew the older issue-#32 wording — so it surfaced as a bare `PROCESS_EXIT`, the sticky account never moved, and every retry hit the same unsupported location (hence the recurring failure). The signature is now matched, the failure carries a dedicated, retryable `ELIGIBILITY` code plus the actionable hint, and the refused account is rotated off the sticky pointer before the error surfaces (no cooldown penalty) — the automatic retry is then served by an account whose own proxy/egress region is supported.
|
|
@@ -17,6 +32,7 @@
|
|
|
17
32
|
|
|
18
33
|
### 中文 (Chinese)
|
|
19
34
|
|
|
35
|
+
|
|
20
36
|
- **修复(Windows):中文工具输出不再乱码(issue #37)。** Windows 管道里的字节使用**子进程的控制台代码页**:中文系统上 agy 捕获到的 pwsh 输出是 CP936(GBK),而插件把整条流按 UTF-8 解码——中文报错于是变成乱码;又因为这段文本正是 agy 写入会话库的内容,损坏不可追回。现在 stdout/stderr 以**原始字节**读取并按**行**用控制台代码页解码(`chcp` 探测 → GBK/Big5/Shift_JIS/EUC-KR/Windows-125x),凡是合法的 UTF-8 行仍按 UTF-8 处理;POSIX 行为逐字节不变(探测直接返回 `utf-8`)。以「行」而非「块」为解码单位,是因为多字节字符可能跨块但绝不会跨换行。
|
|
21
37
|
- **修复:`dsh plugin add git+https://…` 安装现在会构建插件(issue #38)。** 仓库里没有提交 `dist/`,而只有 `prepack`——它**不会**在 git 依赖安装时执行,所以市场/git 安装得到的包没有 `dist/`,加载失败。现在新增 `prepare` 脚本,git 安装时会构建(npm/pnpm 会先为 git 依赖安装 devDependencies);从 npm 安装的用户不受影响,因为 `prepare` 不会对已发布的 tarball 执行。安装其实在更早一步就已经失败:`@deepseek-ai/dsh-client-locale` 曾被声明为**非可选** peer,npm 的自动 peer 安装因此去公共 registry 拉取未发布的 `@deepseek-ai/dsh-paths@^0.0.1-rc.1` 依赖链并报 404 —— 现在所有 `@deepseek-ai/*` peer 都标为 optional(运行时由宿主提供,打包时保持 external),与第三方 DSH 插件的声明方式一致。
|
|
22
38
|
- **修复:线上「User location is not supported for the API use」区域拒绝现在被正确识别、解释并绕开。** 该拒绝报文是 agy 以退出码 3 + `FAILED_PRECONDITION (code 400)` 结果信封抛出的,而分类器只认得 issue #32 时期的旧措辞——于是它被当成裸 `PROCESS_EXIT`,粘性账号也不会移动,每次重试都撞同一个不支持的地区,所以你会觉得"总是出现这个问题"。现在该签名已被匹配,失败会带专用且**可重试**的 `ELIGIBILITY` 错误码与可操作提示,同时在错误抛出前把被拒账号从粘性指针上换掉(**不扣冷却**),自动重试就会落到代理/出口地区受支持的账号上。
|
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ dsh plugin --profile web add dsh-agy-link
|
|
|
133
133
|
| `permissionMode` | `DSH_AGY_MODE` | `skip` | 权限模式:`skip`(无人值守执行)/ `plan` / `accept-edits` |
|
|
134
134
|
| `defaultModel` | `DSH_AGY_DEFAULT_MODEL` | `(agy 默认)` | 默认模型 slug |
|
|
135
135
|
| `defaultEffort` | `DSH_AGY_DEFAULT_EFFORT` | `(模型默认)` | 思考预算:`low` / `medium` / `high` |
|
|
136
|
+
| `hiddenModels` | `DSH_AGY_HIDDEN_MODELS` | `[]` | 在 `/model` 选择器中隐藏的模型 slug 列表(黑名单语义,新模型默认可见;建议直接在设置面板"模型显示"中点选管理) |
|
|
136
137
|
| `timeoutMs` | `DSH_AGY_TIMEOUT_MS` | `600000` | 单轮活跃看门狗超时(毫秒) |
|
|
137
138
|
| `workspaceRoot` | `DSH_AGY_WORKSPACE_ROOT` | 会话 cwd | agy 工作区根目录(默认跟随当前会话工作区) |
|
|
138
139
|
|
|
@@ -269,6 +270,7 @@ dsh plugin --profile web add dsh-agy-link
|
|
|
269
270
|
| `permissionMode` | `DSH_AGY_MODE` | `skip` | `skip` (unattended execution) / `plan` / `accept-edits` |
|
|
270
271
|
| `defaultModel` | `DSH_AGY_DEFAULT_MODEL` | `(agy default)` | Default model slug |
|
|
271
272
|
| `defaultEffort` | `DSH_AGY_DEFAULT_EFFORT` | `(model default)` | Thinking budget: `low` / `medium` / `high` |
|
|
273
|
+
| `hiddenModels` | `DSH_AGY_HIDDEN_MODELS` | `[]` | Model slugs hidden from the `/model` picker (deny-list, newly discovered models stay visible; manage them via the "Model visibility" row in the settings panel) |
|
|
272
274
|
| `timeoutMs` | `DSH_AGY_TIMEOUT_MS` | `600000` | Activity watchdog timeout in milliseconds |
|
|
273
275
|
| `workspaceRoot` | `DSH_AGY_WORKSPACE_ROOT` | session cwd | Working directory root |
|
|
274
276
|
|
package/dist/client.js
CHANGED
|
@@ -121,6 +121,9 @@ window.__ModuleLoader__.load({
|
|
|
121
121
|
"pool.label": "号池调度:",
|
|
122
122
|
"pool.sequential": "顺次耗尽",
|
|
123
123
|
"pool.roundRobin": "轮询均衡",
|
|
124
|
+
"models.label": "模型显示:",
|
|
125
|
+
"models.empty": "模型目录还没有可用模型,发一次对话或点“刷新额度”后再来管理",
|
|
126
|
+
"models.hint": "点击切换显示/隐藏;关闭的模型不在模型选择器中列出,进行中的会话不受影响。",
|
|
124
127
|
"header.refreshQuota": "刷新额度",
|
|
125
128
|
"header.refreshing": "刷新中",
|
|
126
129
|
"console.close": "关闭(Esc)",
|
|
@@ -212,6 +215,9 @@ window.__ModuleLoader__.load({
|
|
|
212
215
|
"pool.label": "Account-pool scheduling:",
|
|
213
216
|
"pool.sequential": "Use sequentially",
|
|
214
217
|
"pool.roundRobin": "Round-robin",
|
|
218
|
+
"models.label": "Model visibility:",
|
|
219
|
+
"models.empty": "No models in the catalog yet — send a message or click \"Refresh quota\", then come back to manage them",
|
|
220
|
+
"models.hint": "Click to show/hide. Hidden models are not listed in the model picker; ongoing sessions are unaffected.",
|
|
215
221
|
"header.refreshQuota": "Refresh quota",
|
|
216
222
|
"header.refreshing": "Refreshing...",
|
|
217
223
|
"console.close": "Close (Esc)",
|
|
@@ -303,6 +309,9 @@ window.__ModuleLoader__.load({
|
|
|
303
309
|
"pool.label": "Agendamento do pool de contas:",
|
|
304
310
|
"pool.sequential": "Usar em sequência",
|
|
305
311
|
"pool.roundRobin": "Rodízio",
|
|
312
|
+
"models.label": "Visibilidade dos modelos:",
|
|
313
|
+
"models.empty": "Ainda não há modelos no catálogo — envie uma mensagem ou clique em \"Atualizar cota\" e volte para gerenciá-los",
|
|
314
|
+
"models.hint": "Clique para mostrar/ocultar. Modelos ocultos não aparecem no seletor de modelos; as sessões em andamento não são afetadas.",
|
|
306
315
|
"header.refreshQuota": "Atualizar cota",
|
|
307
316
|
"header.refreshing": "Atualizando...",
|
|
308
317
|
"console.close": "Fechar (Esc)",
|
|
@@ -394,6 +403,9 @@ window.__ModuleLoader__.load({
|
|
|
394
403
|
"pool.label": "Programación del pool de cuentas:",
|
|
395
404
|
"pool.sequential": "Usar en secuencia",
|
|
396
405
|
"pool.roundRobin": "Turno rotativo",
|
|
406
|
+
"models.label": "Visibilidad de modelos:",
|
|
407
|
+
"models.empty": "Aún no hay modelos en el catálogo: envía un mensaje o haz clic en \"Actualizar cuota\" y vuelve para gestionarlos",
|
|
408
|
+
"models.hint": "Haz clic para mostrar/ocultar. Los modelos ocultos no aparecen en el selector de modelos; las sesiones en curso no se ven afectadas.",
|
|
397
409
|
"header.refreshQuota": "Actualizar cuota",
|
|
398
410
|
"header.refreshing": "Actualizando...",
|
|
399
411
|
"console.close": "Cerrar (Esc)",
|
|
@@ -2750,6 +2762,12 @@ body.dark,
|
|
|
2750
2762
|
await refresh();
|
|
2751
2763
|
setLoadingAction(null);
|
|
2752
2764
|
};
|
|
2765
|
+
const toggleModelHidden = async (id, hide) => {
|
|
2766
|
+
const next = new Set(status?.hiddenModels ?? []);
|
|
2767
|
+
if (hide) next.add(id);
|
|
2768
|
+
else next.delete(id);
|
|
2769
|
+
await setCfg("hiddenModels", Array.from(next));
|
|
2770
|
+
};
|
|
2753
2771
|
const setPrimary = async (id) => {
|
|
2754
2772
|
setLoadingAction(`primary:${id}`);
|
|
2755
2773
|
await postJson("/plugins/agy-link/pool/primary", { id });
|
|
@@ -2804,6 +2822,8 @@ body.dark,
|
|
|
2804
2822
|
};
|
|
2805
2823
|
const authPhase = status?.auth?.phase ?? "unknown";
|
|
2806
2824
|
const pool = status?.pool;
|
|
2825
|
+
const catalogModels = status?.catalogModels ?? [];
|
|
2826
|
+
const hiddenModelSet = new Set(status?.hiddenModels ?? []);
|
|
2807
2827
|
const accounts = pool?.accounts ?? [];
|
|
2808
2828
|
const isAuthed = authPhase === "ok" || accounts.length > 0;
|
|
2809
2829
|
const isBusy = loadingAction !== null;
|
|
@@ -3386,7 +3406,33 @@ body.dark,
|
|
|
3386
3406
|
type: "button",
|
|
3387
3407
|
style: pool?.mode === "round-robin" ? S.segBtnActive : S.segBtn,
|
|
3388
3408
|
onClick: () => void setMode("round-robin")
|
|
3389
|
-
}, t("pool.roundRobin"))))
|
|
3409
|
+
}, t("pool.roundRobin")))), h("div", { style: {
|
|
3410
|
+
display: "flex",
|
|
3411
|
+
flexDirection: "column",
|
|
3412
|
+
gap: "6px"
|
|
3413
|
+
} }, h("span", { style: {
|
|
3414
|
+
color: "var(--agy-text-primary)",
|
|
3415
|
+
fontWeight: 600,
|
|
3416
|
+
fontSize: "12.5px"
|
|
3417
|
+
} }, t("models.label")), catalogModels.length === 0 ? h("span", { style: S.muted }, t("models.empty")) : h("div", { style: {
|
|
3418
|
+
display: "flex",
|
|
3419
|
+
flexWrap: "wrap",
|
|
3420
|
+
gap: "6px"
|
|
3421
|
+
} }, catalogModels.map((m) => {
|
|
3422
|
+
const isHidden = hiddenModelSet.has(m.id);
|
|
3423
|
+
return h("button", {
|
|
3424
|
+
type: "button",
|
|
3425
|
+
key: m.id,
|
|
3426
|
+
title: m.id,
|
|
3427
|
+
disabled: loadingAction === "config:hiddenModels",
|
|
3428
|
+
style: isHidden ? {
|
|
3429
|
+
...S.segBtn,
|
|
3430
|
+
opacity: .45,
|
|
3431
|
+
textDecoration: "line-through"
|
|
3432
|
+
} : S.segBtnActive,
|
|
3433
|
+
onClick: () => void toggleModelHidden(m.id, !isHidden)
|
|
3434
|
+
}, m.name);
|
|
3435
|
+
})), h("span", { style: S.muted }, t("models.hint"))))));
|
|
3390
3436
|
};
|
|
3391
3437
|
/** Modal dialog container rendered into document.body */
|
|
3392
3438
|
const AgyModalDialog = () => {
|
package/dist/index.js
CHANGED
|
@@ -92,6 +92,7 @@ function defaultConfig() {
|
|
|
92
92
|
compactionMaxChars: 8e5,
|
|
93
93
|
workspaceRoot: "",
|
|
94
94
|
fallbackModels: DEFAULT_FALLBACK_MODELS,
|
|
95
|
+
hiddenModels: [],
|
|
95
96
|
askTool: false,
|
|
96
97
|
mediaDir: "",
|
|
97
98
|
mediaTtlMs: 864e5,
|
|
@@ -259,6 +260,20 @@ function asNum(v) {
|
|
|
259
260
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
260
261
|
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
261
262
|
}
|
|
263
|
+
/** Trimmed, de-duplicated string list; non-string / blank entries are dropped. */
|
|
264
|
+
function asStringArray(v) {
|
|
265
|
+
if (!Array.isArray(v)) return void 0;
|
|
266
|
+
const out = [];
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const x of v) {
|
|
269
|
+
if (typeof x !== "string") continue;
|
|
270
|
+
const s = x.trim();
|
|
271
|
+
if (s === "" || seen.has(s)) continue;
|
|
272
|
+
seen.add(s);
|
|
273
|
+
out.push(s);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
262
277
|
const MODES = [
|
|
263
278
|
"skip",
|
|
264
279
|
"plan",
|
|
@@ -294,6 +309,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
294
309
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
295
310
|
workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
296
311
|
fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
|
|
312
|
+
hiddenModels: asStringArray(get("hiddenModels")) ?? base.hiddenModels,
|
|
297
313
|
askTool: asBool(get("askTool")) ?? base.askTool,
|
|
298
314
|
mediaDir: asString(get("mediaDir")) ?? base.mediaDir,
|
|
299
315
|
mediaTtlMs: asNum(get("mediaTtlMs")) ?? base.mediaTtlMs,
|
|
@@ -325,6 +341,10 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
325
341
|
if (t && t > 0) cfg.timeoutMs = t;
|
|
326
342
|
}
|
|
327
343
|
if (env.DSH_AGY_EXTRA_ARGS) cfg.extraArgs = env.DSH_AGY_EXTRA_ARGS.split(/\s+/).filter(Boolean);
|
|
344
|
+
if (env.DSH_AGY_HIDDEN_MODELS) {
|
|
345
|
+
const list = asStringArray(env.DSH_AGY_HIDDEN_MODELS.split(","));
|
|
346
|
+
if (list) cfg.hiddenModels = list;
|
|
347
|
+
}
|
|
328
348
|
if (env.DSH_AGY_WORKSPACE_ROOT) cfg.workspaceRoot = env.DSH_AGY_WORKSPACE_ROOT;
|
|
329
349
|
if (env.DSH_AGY_MEDIA_DIR) cfg.mediaDir = env.DSH_AGY_MEDIA_DIR;
|
|
330
350
|
if (env.DSH_AGY_MEDIA_TTL_MS) {
|
|
@@ -1475,6 +1495,36 @@ function buildFallbackCatalog(defs) {
|
|
|
1475
1495
|
efforts: Array.isArray(d.efforts) && d.efforts.length > 0 ? d.efforts.filter((e) => typeof e === "string" && e.trim() !== "") : null
|
|
1476
1496
|
}));
|
|
1477
1497
|
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Sanitized (id, name) projection of a catalog: trimmed non-empty ids with
|
|
1500
|
+
* first occurrence winning — exactly what listModels serves to DSH. Shared
|
|
1501
|
+
* with the settings UI's model-visibility list so the picker and the UI can
|
|
1502
|
+
* never disagree about which models exist.
|
|
1503
|
+
*/
|
|
1504
|
+
function sanitizeCatalogEntries(models) {
|
|
1505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1506
|
+
const entries = [];
|
|
1507
|
+
const dropped = [];
|
|
1508
|
+
for (const m of models) {
|
|
1509
|
+
if (!m || typeof m.id !== "string") continue;
|
|
1510
|
+
const id = m.id.trim();
|
|
1511
|
+
if (id === "") continue;
|
|
1512
|
+
if (seen.has(id)) {
|
|
1513
|
+
dropped.push(id);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
seen.add(id);
|
|
1517
|
+
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
1518
|
+
entries.push({
|
|
1519
|
+
id,
|
|
1520
|
+
name
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
entries,
|
|
1525
|
+
dropped
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1478
1528
|
var ModelCatalog = class {
|
|
1479
1529
|
discover;
|
|
1480
1530
|
ttlMs;
|
|
@@ -2379,9 +2429,24 @@ function killTree(child) {
|
|
|
2379
2429
|
escalating.delete(child);
|
|
2380
2430
|
});
|
|
2381
2431
|
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Environment for spawning a script through `process.execPath`.
|
|
2434
|
+
*
|
|
2435
|
+
* In the packaged desktop host `process.execPath` is the **Electron binary**,
|
|
2436
|
+
* not node: without this flag the child would boot a second GUI instance
|
|
2437
|
+
* instead of executing the script. Plain node ignores the variable, so the
|
|
2438
|
+
* POSIX/test paths are unaffected.
|
|
2439
|
+
*/
|
|
2440
|
+
function nodeScriptEnv(env) {
|
|
2441
|
+
return {
|
|
2442
|
+
...env,
|
|
2443
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2382
2446
|
function startAgyProcess(opts) {
|
|
2383
2447
|
const started = Date.now();
|
|
2384
2448
|
const viaCmd = IS_WIN && isCmdShim(opts.bin);
|
|
2449
|
+
const isNode = IS_WIN && /\.(mjs|js|cjs)$/i.test(opts.bin);
|
|
2385
2450
|
const env = withAgyQuietEnv(opts.env ?? process.env);
|
|
2386
2451
|
const child = viaCmd ? spawn(process.env.ComSpec ?? "cmd.exe", [
|
|
2387
2452
|
"/d",
|
|
@@ -2398,6 +2463,15 @@ function startAgyProcess(opts) {
|
|
|
2398
2463
|
],
|
|
2399
2464
|
windowsVerbatimArguments: true,
|
|
2400
2465
|
windowsHide: true
|
|
2466
|
+
}) : isNode ? spawn(process.execPath, [opts.bin, ...opts.args], {
|
|
2467
|
+
cwd: opts.cwd,
|
|
2468
|
+
env: nodeScriptEnv(env),
|
|
2469
|
+
stdio: [
|
|
2470
|
+
"pipe",
|
|
2471
|
+
"pipe",
|
|
2472
|
+
"pipe"
|
|
2473
|
+
],
|
|
2474
|
+
windowsHide: true
|
|
2401
2475
|
}) : spawn(opts.bin, opts.args, {
|
|
2402
2476
|
cwd: opts.cwd,
|
|
2403
2477
|
env,
|
|
@@ -3269,29 +3343,16 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3269
3343
|
}
|
|
3270
3344
|
async listModels(_provider) {
|
|
3271
3345
|
this.deps.catalog.refreshIfNeeded();
|
|
3272
|
-
const
|
|
3273
|
-
const
|
|
3274
|
-
const models = [];
|
|
3275
|
-
const dropped = [];
|
|
3276
|
-
for (const m of cat.models) {
|
|
3277
|
-
if (!m || typeof m.id !== "string") continue;
|
|
3278
|
-
const id = m.id.trim();
|
|
3279
|
-
if (id === "") continue;
|
|
3280
|
-
if (seen.has(id)) {
|
|
3281
|
-
dropped.push(id);
|
|
3282
|
-
continue;
|
|
3283
|
-
}
|
|
3284
|
-
seen.add(id);
|
|
3285
|
-
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
3286
|
-
models.push({
|
|
3287
|
-
provider: PROVIDER_ID,
|
|
3288
|
-
id,
|
|
3289
|
-
name,
|
|
3290
|
-
inputModalities: ["text", "image"]
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3346
|
+
const cfg = this.deps.getConfig();
|
|
3347
|
+
const { entries, dropped } = sanitizeCatalogEntries(this.deps.catalog.get().models);
|
|
3293
3348
|
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
3294
|
-
|
|
3349
|
+
const hidden = new Set(cfg.hiddenModels.map((s) => s.trim()));
|
|
3350
|
+
return entries.filter((m) => !hidden.has(m.id)).map((m) => ({
|
|
3351
|
+
provider: PROVIDER_ID,
|
|
3352
|
+
id: m.id,
|
|
3353
|
+
name: m.name,
|
|
3354
|
+
inputModalities: ["text", "image"]
|
|
3355
|
+
}));
|
|
3295
3356
|
}
|
|
3296
3357
|
async resolveModel(_provider, model, _signal) {
|
|
3297
3358
|
const cfg = this.deps.getConfig();
|
|
@@ -3372,10 +3433,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3372
3433
|
const cfg = this.deps.getConfig();
|
|
3373
3434
|
const bin = this.deps.bin();
|
|
3374
3435
|
if (!bin) throw new LlmError("agy binary not found on PATH — install it via https://antigravity.google/docs/cli/install", Err.AGY_NOT_INSTALLED);
|
|
3375
|
-
const
|
|
3376
|
-
|
|
3436
|
+
const isReview = typeof options.system === "string" && options.system.includes("REVIEW_POLICY");
|
|
3437
|
+
const isAux = options.purpose === "compaction" || options.purpose === "session-title" || isReview;
|
|
3438
|
+
if (isAux && !isReview && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
|
|
3377
3439
|
const toolNames = new Set((options.tools ?? []).map((t) => t.name));
|
|
3378
3440
|
const isCodeMode = toolNames.has("run_code") && !toolNames.has("agy_tool");
|
|
3441
|
+
const hasToolSupport = options.tools === void 0 || toolNames.has("agy_tool") || toolNames.has("run_code");
|
|
3379
3442
|
const sessionKey = options.sessionId !== void 0 ? String(options.sessionId) : "";
|
|
3380
3443
|
let workspaceRoot = cfg.workspaceRoot;
|
|
3381
3444
|
if (workspaceRoot === "") {
|
|
@@ -3389,28 +3452,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3389
3452
|
const continuation = detectContinuation(options.messages);
|
|
3390
3453
|
if (continuation !== null) {
|
|
3391
3454
|
const rec = this.deps.runs.get(continuation.runId);
|
|
3392
|
-
if (rec
|
|
3393
|
-
yield
|
|
3394
|
-
type: "usage",
|
|
3395
|
-
usage: {
|
|
3396
|
-
inputTokens: 0,
|
|
3397
|
-
outputTokens: 0
|
|
3398
|
-
}
|
|
3399
|
-
};
|
|
3400
|
-
yield {
|
|
3401
|
-
type: "finish",
|
|
3402
|
-
reason: {
|
|
3403
|
-
kind: "error",
|
|
3404
|
-
failure: {
|
|
3405
|
-
message: "agy run " + continuation.runId + " is no longer available (server restarted?) — please resend your message",
|
|
3406
|
-
code: Err.AGY_ERROR
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
};
|
|
3455
|
+
if (rec !== void 0) {
|
|
3456
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
|
|
3410
3457
|
return;
|
|
3411
3458
|
}
|
|
3412
|
-
|
|
3413
|
-
return;
|
|
3459
|
+
this.warnOnce("stale-continuation:" + continuation.runId, "agy run " + continuation.runId + " is no longer available in memory (server restarted?) — falling back to fresh turn prompt assembly");
|
|
3414
3460
|
}
|
|
3415
3461
|
const catalog = this.deps.catalog.get();
|
|
3416
3462
|
const rawModel = options.model;
|
|
@@ -3797,7 +3843,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3797
3843
|
message: "internal error: " + brief(String(err))
|
|
3798
3844
|
});
|
|
3799
3845
|
});
|
|
3800
|
-
yield* this.driveSpan(rec, 0, !isAux, isCodeMode);
|
|
3846
|
+
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
|
|
3801
3847
|
}
|
|
3802
3848
|
/**
|
|
3803
3849
|
* Stream one span of a recording: map events from `from` until the mapper
|
|
@@ -29958,6 +30004,11 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29958
30004
|
let lastRun = null;
|
|
29959
30005
|
let lastParser = new StreamJsonParser();
|
|
29960
30006
|
const getConfig = () => resolveConfig(entryConfig);
|
|
30007
|
+
const notifyPicker = () => {
|
|
30008
|
+
try {
|
|
30009
|
+
ctx.emit("llm/adapters-updated");
|
|
30010
|
+
} catch {}
|
|
30011
|
+
};
|
|
29961
30012
|
const bin = () => {
|
|
29962
30013
|
if (binCache === void 0 || binCache === null) binCache = resolveAgyBin(getConfig());
|
|
29963
30014
|
return binCache;
|
|
@@ -30085,6 +30136,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30085
30136
|
log("version probe failed — continuing with fallback catalog");
|
|
30086
30137
|
}
|
|
30087
30138
|
await catalog.refreshIfNeeded().catch(() => void 0);
|
|
30139
|
+
notifyPicker();
|
|
30088
30140
|
})();
|
|
30089
30141
|
}, 4e3);
|
|
30090
30142
|
bootProbeTimer.unref?.();
|
|
@@ -30121,6 +30173,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30121
30173
|
const toolsSvcRef = { current: null };
|
|
30122
30174
|
const askToolDispose = { current: null };
|
|
30123
30175
|
const mirrorToolDispose = { current: null };
|
|
30176
|
+
const preExecuteDispose = { current: null };
|
|
30124
30177
|
const syncAskTool = () => {
|
|
30125
30178
|
const want = getConfig().askTool && bin() !== null;
|
|
30126
30179
|
if (want && askToolDispose.current === null && toolsSvcRef.current) {
|
|
@@ -30150,7 +30203,17 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30150
30203
|
toolsSvcRef.current = sub.get("tools");
|
|
30151
30204
|
syncAskTool();
|
|
30152
30205
|
syncMirrorTool();
|
|
30206
|
+
const unlisten = sub.on("tools/pre-execute", async (exec, next) => {
|
|
30207
|
+
if (exec.name === "agy_tool") return { kind: "allow" };
|
|
30208
|
+
return next();
|
|
30209
|
+
}, {
|
|
30210
|
+
prepend: true,
|
|
30211
|
+
global: true
|
|
30212
|
+
});
|
|
30213
|
+
preExecuteDispose.current = typeof unlisten === "function" ? unlisten : null;
|
|
30153
30214
|
return () => {
|
|
30215
|
+
if (preExecuteDispose.current !== null) preExecuteDispose.current();
|
|
30216
|
+
preExecuteDispose.current = null;
|
|
30154
30217
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
30155
30218
|
askToolDispose.current = null;
|
|
30156
30219
|
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
@@ -30213,6 +30276,8 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30213
30276
|
count: cat.models.length,
|
|
30214
30277
|
lastError: cat.lastError ?? null
|
|
30215
30278
|
},
|
|
30279
|
+
catalogModels: sanitizeCatalogEntries(cat.models).entries,
|
|
30280
|
+
hiddenModels: cfg.hiddenModels,
|
|
30216
30281
|
bindings: Object.keys(store.all()).length,
|
|
30217
30282
|
lastRun
|
|
30218
30283
|
});
|
|
@@ -30259,7 +30324,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30259
30324
|
if (acc) quota.refreshAccountQuota(acc).catch(() => void 0);
|
|
30260
30325
|
} else quota.refreshAllQuotas().catch(() => void 0);
|
|
30261
30326
|
store.clear();
|
|
30262
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
30327
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
30263
30328
|
}
|
|
30264
30329
|
sendJson(res, 200, st);
|
|
30265
30330
|
})();
|
|
@@ -30534,7 +30599,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30534
30599
|
const acc = pool.getAccount(id);
|
|
30535
30600
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
30536
30601
|
} else await quota.refreshAllQuotas(true);
|
|
30537
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
30602
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
30538
30603
|
sendJson(res, 200, {
|
|
30539
30604
|
ok: true,
|
|
30540
30605
|
pool: pool.getPoolData()
|
|
@@ -30578,14 +30643,20 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30578
30643
|
"defaultModel",
|
|
30579
30644
|
"defaultEffort",
|
|
30580
30645
|
"askTool",
|
|
30581
|
-
"workspaceRoot"
|
|
30646
|
+
"workspaceRoot",
|
|
30647
|
+
"hiddenModels"
|
|
30582
30648
|
].includes(key)) {
|
|
30583
30649
|
sendJson(res, 400, { error: "key not settable" });
|
|
30584
30650
|
return;
|
|
30585
30651
|
}
|
|
30652
|
+
if (key === "hiddenModels" && (!Array.isArray(body.value) || !body.value.every((x) => typeof x === "string"))) {
|
|
30653
|
+
sendJson(res, 400, { error: "hiddenModels must be an array of model ids" });
|
|
30654
|
+
return;
|
|
30655
|
+
}
|
|
30586
30656
|
setOverride(key, body.value);
|
|
30587
30657
|
syncAskTool();
|
|
30588
30658
|
syncMirrorTool();
|
|
30659
|
+
if (key === "hiddenModels") notifyPicker();
|
|
30589
30660
|
sendJson(res, 200, {
|
|
30590
30661
|
ok: true,
|
|
30591
30662
|
key,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.44",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|