Tool surface: preset / tools / excludeTools filtering (core preset: 11,815 B vs 107,157 B) #49

Closed
opened 2026-10-24 20:29:48 +00:00 by dsh · 1 comment
Owner

Problem

Every registered tool's schema is bound into every request. dsh-gitea contributes 133 names / 107,157 B — 61.5 % of a 174,372 B tool payload — including in sessions that never call a gitea tool.

Measurements (2026-10-24, dsh-gitea 0.8.4, DSH 0.2.1-rc.2)

Corpus: 28 session logs under ~/.dsh/sessions, calls deduplicated by call id (1,020 unique gitea calls).

Fact Value
sessions scanned 28
sessions with zero gitea calls 12 (43 %)
sessions with gitea calls 16
avg categories touched per active session 2.8 of 5
active sessions touching ≥3 categories 10 / 16
active sessions touching ≥4 categories 5 / 16
distinct tools ever called 95
top-15 share of calls 70.3 %
top-25 share 80.8 %
tools called exactly once 23 (QA sweep)

Payload by subset (canonical names; aliases excluded):

Preset candidate Tools Bytes
core (15 most-used) 15 11,815
repos 15 8,948
issues 20 13,672
prs 28 18,293
ops 32 23,271
full (canonical) 96 65,159
full + aliases (today) 133 107,157

The 15 hot tools by call count: issue_create, labels, issue_comment, issue_get, issue_close, ci, issue_list, wiki, repo_contents, branches, org, releases, whoami, issue_search, issue_set_labels.

Context share: 191 tools ≈ 47k tokens ≈ 70 % of a 67,048-token first-turn prompt (~3.7 bytes/token, cross-checked against the session usage record).

Impact

  • A workspace that only files issues runs at ~13.7 KB instead of 107 KB.
  • The 43 % of sessions that never touch Gitea can opt out entirely — no in-session lazy mechanism can beat not loading at all.
  • Default stays full, so existing installs see no behaviour change.

Proposal

Mirror the interface already shipped in dsh-resend-mcp so operators carry one mental model:

- id: dsh-gitea
  config:
    preset: core          # core | issues | prs | ops | repos | full | custom
    tools: []             # glob allowlist; non-empty replaces the preset
    excludeTools: []      # glob denylist, applied after
  • full is today's canonical set (byte-identical) and stays the default.
  • Unmatched patterns warn once at boot and are listed in the settings page — silent no-ops are a known failure class (cf. the resend plugin's missingPatterns handling).
  • The preset→tool table is generated from TOOL_DEFS so it cannot drift from the code.
  • Docs recommend core plus explicit tools additions; issues / prs / ops for focused workspaces.
  • The plugin's legacyAliases flag (#48) is independent: presets filter canonical definitions, the flag controls whether the 37 aliases are advertised at all.

Why this before any lazy loading

Presets are decided at mount: zero runtime cost, no request-prefix change, no KV-cache invalidation, and they survive DSH upgrades as a config edit. They are also the only lever that helps the 12/28 sessions that never call a gitea tool.

Acceptance criteria

  • config schema accepts preset, tools, excludeTools; an invalid preset is rejected at mount with a clear message
  • full produces a tool set byte-identical to 0.8.4 canonical
  • measured payload per preset recorded in README (table above, regenerable)
  • switching preset in cordis.patch.yml takes effect via HMR without a restart
  • allowlist and denylist covered by unit tests, including an unmatched pattern
  • settings page shows the active preset and tool count

Verification Plan

Same request/header measurement as #48, run once per preset:

zstd -dc ~/.dsh/sessions/<ws>/<session>/session.v4.jsonl.zstd > /tmp/s.jsonl
node -e 'const fs=require("fs");for(const l of fs.readFileSync("/tmp/s.jsonl","utf8").split("\n")){if(!l.includes("\"request/header\""))continue;const t=JSON.parse(l).data.header.tools;console.log(t.length,"tools",t.reduce((a,x)=>a+JSON.stringify(x).length,0),"B");break}'

Plus a live session with preset: core exercising the hot path (issue create, comment, close, label set, CI status).

Scope

  • No change to tool schemas or handlers; filtering only.
  • No dynamic registration — that is the two experiment issues.

Dependencies

Improved by #48 (baseline 65,159 B) but not blocked by it.

## Problem Every registered tool's schema is bound into every request. dsh-gitea contributes 133 names / 107,157 B — 61.5 % of a 174,372 B tool payload — including in sessions that never call a gitea tool. ## Measurements (2026-10-24, dsh-gitea 0.8.4, DSH 0.2.1-rc.2) Corpus: 28 session logs under `~/.dsh/sessions`, calls deduplicated by call id (1,020 unique gitea calls). | Fact | Value | |---|---| | sessions scanned | 28 | | sessions with zero gitea calls | 12 (43 %) | | sessions with gitea calls | 16 | | avg categories touched per active session | 2.8 of 5 | | active sessions touching ≥3 categories | 10 / 16 | | active sessions touching ≥4 categories | 5 / 16 | | distinct tools ever called | 95 | | top-15 share of calls | 70.3 % | | top-25 share | 80.8 % | | tools called exactly once | 23 (QA sweep) | Payload by subset (canonical names; aliases excluded): | Preset candidate | Tools | Bytes | |---|---|---| | `core` (15 most-used) | 15 | 11,815 | | `repos` | 15 | 8,948 | | `issues` | 20 | 13,672 | | `prs` | 28 | 18,293 | | `ops` | 32 | 23,271 | | `full` (canonical) | 96 | 65,159 | | `full` + aliases (today) | 133 | 107,157 | The 15 hot tools by call count: `issue_create`, `labels`, `issue_comment`, `issue_get`, `issue_close`, `ci`, `issue_list`, `wiki`, `repo_contents`, `branches`, `org`, `releases`, `whoami`, `issue_search`, `issue_set_labels`. Context share: 191 tools ≈ 47k tokens ≈ 70 % of a 67,048-token first-turn prompt (~3.7 bytes/token, cross-checked against the session `usage` record). ## Impact - A workspace that only files issues runs at ~13.7 KB instead of 107 KB. - The 43 % of sessions that never touch Gitea can opt out entirely — no in-session lazy mechanism can beat not loading at all. - Default stays `full`, so existing installs see no behaviour change. ## Proposal Mirror the interface already shipped in `dsh-resend-mcp` so operators carry one mental model: ```yaml - id: dsh-gitea config: preset: core # core | issues | prs | ops | repos | full | custom tools: [] # glob allowlist; non-empty replaces the preset excludeTools: [] # glob denylist, applied after ``` - `full` is today's canonical set (byte-identical) and stays the default. - Unmatched patterns warn once at boot and are listed in the settings page — silent no-ops are a known failure class (cf. the resend plugin's `missingPatterns` handling). - The preset→tool table is generated from `TOOL_DEFS` so it cannot drift from the code. - Docs recommend `core` plus explicit `tools` additions; `issues` / `prs` / `ops` for focused workspaces. - The plugin's `legacyAliases` flag (#48) is independent: presets filter canonical definitions, the flag controls whether the 37 aliases are advertised at all. ## Why this before any lazy loading Presets are decided at mount: zero runtime cost, no request-prefix change, no KV-cache invalidation, and they survive DSH upgrades as a config edit. They are also the only lever that helps the 12/28 sessions that never call a gitea tool. ## Acceptance criteria - [ ] config schema accepts `preset`, `tools`, `excludeTools`; an invalid preset is rejected at mount with a clear message - [ ] `full` produces a tool set byte-identical to 0.8.4 canonical - [ ] measured payload per preset recorded in README (table above, regenerable) - [ ] switching `preset` in `cordis.patch.yml` takes effect via HMR without a restart - [ ] allowlist and denylist covered by unit tests, including an unmatched pattern - [ ] settings page shows the active preset and tool count ## Verification Plan Same `request/header` measurement as #48, run once per preset: ```bash zstd -dc ~/.dsh/sessions/<ws>/<session>/session.v4.jsonl.zstd > /tmp/s.jsonl node -e 'const fs=require("fs");for(const l of fs.readFileSync("/tmp/s.jsonl","utf8").split("\n")){if(!l.includes("\"request/header\""))continue;const t=JSON.parse(l).data.header.tools;console.log(t.length,"tools",t.reduce((a,x)=>a+JSON.stringify(x).length,0),"B");break}' ``` Plus a live session with `preset: core` exercising the hot path (issue create, comment, close, label set, CI status). ## Scope - No change to tool schemas or handlers; filtering only. - No dynamic registration — that is the two `experiment` issues. ## Dependencies Improved by #48 (baseline 65,159 B) but not blocked by it.
Author
Owner

Fixed in 3d3537b, released as 0.8.5.

Implemented as proposed, with the dsh-resend-mcp interface as the reference (lib/selection.js, pure module; glob * only, unmatched patterns reported via missingPatterns/missingExcludePatterns, never fatal):

  • preset: core | issues | prs | repos | ops | full | none | custom. full is the default and resolves in TOOL_DEFS order, so it is byte-identical to 0.8.4. The four domain presets are disjoint and their union is exactly the 96 canonical tools — test/selection.test.mjs fails if a tool is renamed or dropped, so the table cannot drift.
  • tools: non-empty replaces the preset (custom requires it); excludeTools subtracts after. An unknown preset fails at mount with the accepted names; the settings route answers 400.
  • Added none as proposed in the design discussion: the 12/28 sessions that never call a Gitea tool can opt out with one word instead of excludeTools: ["*"].
  • Selection inputs are reduced to a signature: an unrelated settings write (e.g. baseUrl) does not re-register, so the model-facing prefix stays stable. A real surface change disposes and re-registers through the disposer tools.register() returns — live, no restart.
  • Settings page: new Tool Surface section (preset select, allowlist, denylist, alias checkbox) showing the active count and any pattern that matched nothing. /dsh-gitea/config returns toolSurface for the same data.

Measured on the live wire schema (bytes per preset): core 11,640 / 15 tools, issues 15,586 / 22, prs 17,480 / 27, repos 12,026 / 17, ops 20,067 / 30, none 0, full 65,159 / 96. Recorded in the README table. npm run check: 180 tests, leak scan and syntax clean.

Fixed in `3d3537b`, released as **0.8.5**. Implemented as proposed, with the `dsh-resend-mcp` interface as the reference (`lib/selection.js`, pure module; glob `*` only, unmatched patterns reported via `missingPatterns`/`missingExcludePatterns`, never fatal): - `preset`: `core` | `issues` | `prs` | `repos` | `ops` | `full` | `none` | `custom`. `full` is the default and resolves in `TOOL_DEFS` order, so it is byte-identical to 0.8.4. The four domain presets are disjoint and their union is exactly the 96 canonical tools — `test/selection.test.mjs` fails if a tool is renamed or dropped, so the table cannot drift. - `tools`: non-empty replaces the preset (`custom` requires it); `excludeTools` subtracts after. An unknown preset fails at mount with the accepted names; the settings route answers 400. - Added **`none`** as proposed in the design discussion: the 12/28 sessions that never call a Gitea tool can opt out with one word instead of `excludeTools: ["*"]`. - Selection inputs are reduced to a signature: an unrelated settings write (e.g. `baseUrl`) does not re-register, so the model-facing prefix stays stable. A real surface change disposes and re-registers through the disposer `tools.register()` returns — live, no restart. - Settings page: new **Tool Surface** section (preset select, allowlist, denylist, alias checkbox) showing the active count and any pattern that matched nothing. `/dsh-gitea/config` returns `toolSurface` for the same data. Measured on the live wire schema (bytes per preset): `core` 11,640 / 15 tools, `issues` 15,586 / 22, `prs` 17,480 / 27, `repos` 12,026 / 17, `ops` 20,067 / 30, `none` 0, `full` 65,159 / 96. Recorded in the README table. `npm run check`: 180 tests, leak scan and syntax clean.
dsh closed this issue 2026-10-24 20:45:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dsh-plugins/dsh-gitea#49
No description provided.