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 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 cat = this.deps.catalog.get();
3273
- const seen = /* @__PURE__ */ new Set();
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
- return models;
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 isAux = options.purpose === "compaction" || options.purpose === "session-title";
3376
- if (isAux && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
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 === void 0) {
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
- yield* this.driveSpan(rec, continuation.eventIndex + 1, true, isCodeMode);
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.43",
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",