dsh-skill-invocation-monitor
English | 简体中文
A read-only DeepSeek Harness (DSH) plugin that puts
every skill invocation on one screen: which Agents are running, which Skill each one is calling,
whether it is running / finished / failed, and how long it has been going — live, in the Web GUI sidebar.
The Skill Monitor panel: two sessions, the first one running a skill the user invoked
Captured from the running plugin: the sidebar entry opens a panel listing every Agent, and each row is one
skill call with its status, elapsed time and start clock.
What changes after you install it
Skill invocation in DSH is invisible. The model loads a skill, and you see the answer — not the call, not
the timing, not the one that never came back. After installing this plugin:
- the sidebar gains a 技能监控 / Skill Monitor entry, with a live badge showing how many calls are running;
- the panel lists every live Agent, and under each one every skill call with its status and elapsed time;
- both ways a skill can be reached are covered and told apart — the model calling the
skill tool, and
you triggering a skill yourself, which carries a 用户触发 badge on its row;
- nothing is written anywhere: no tool registered, no session state touched, no call intercepted or delayed.
Install
# from npm (recommended — prebuilt, so no build-script permission is needed)
dsh plugin --profile web add @yyc-jnu/dsh-skill-invocation-monitor
# or straight from GitHub (plain ESM JavaScript: no build step, so pnpm never asks for allowBuilds)
dsh plugin --profile web add github:YYC-JNU/dsh-skill-invocation-monitor
Then restart DSH and refresh the page. Both halves — the host observers plus the read-only route, and the
browser bundle — are scanned during host start-up, so no sidebar entry appears before that restart.
Verify the layer before booting:
dsh --profile web --dump-config # look for: # == @yyc-jnu/dsh-skill-invocation-monitor
If that layer is missing, the package installed as a plain dependency and the plugin never activated.
Uninstall:
dsh plugin --profile web remove @yyc-jnu/dsh-skill-invocation-monitor
Features
| |
|---|
| Live Agent list | Every live Agent, its session title, and whether it is running or idle |
| Per-call rows | Skill name, status (运行中 / 已完成 / 失败 / 未观测到结果), elapsed time, start clock |
| Both invocation paths | The model's skill tool call, and a human triggering a skill — marked 用户触发 |
| Ticking durations | The elapsed time updates every second while a call is running |
| Sidebar badge | The running-call count sits on the sidebar entry, so you can see activity without opening the panel |
| Honest failures | A failed call shows a truncated reason, e.g. skill "…" is unknown or no longer available |
| Self-check diagnostics | Every response carries counters describing what the plugin observed, so a wrong assumption is visible rather than silent |
| Zero dependencies | Neither half imports a third-party package (the browser half uses only react from the host module table) |
Permissions and data
This plugin exists to observe, and its whole design is built around not participating.
| |
|---|
| Observes (read-only) | Five Cordis events: tools/pre-execute, tools/result, session/event, agent/status, agent/disposed |
| Reads | The live Agent list, session titles, and the durable session-event feed as it is appended |
| Does not write | No session state, no session log, nothing on disk, no config, no tool, no command |
| Does not interfere | The tools/pre-execute listener returns next() unchanged and never denies, cancels, rewrites or delays a call; every observer is wrapped so a bug in this plugin cannot break a dispatch |
| HTTP surface | Exactly one route, GET /api/plugins/skill-monitor/state — no body, no mutation, cache-control: no-store |
| Network exposure | Loopback only: a non-loopback peer, a cross-site fetch marker or a foreign Origin gets 403; any non-GET method gets 405 |
| Where the data lives | One Map in host memory. Records are capped (200 by default) and expire (10 minutes after they settle); restarting DSH clears everything |
| Not collected | Prompt text, model output, file contents, credentials, or anything else the plugin never reads |
Compatibility
| |
|---|
| DSH | Verified against 0.2.0-rc.2 (the build this plugin was developed and tested on). The seams it uses are documented core surfaces, so other 0.2.x builds are expected to work |
| Node.js | Declared >=20. The plugin runs inside the DSH host, so the host's own Node requirement is what matters |
| Surface | Web GUI (dsh.client.platform: "web"). Uses only official Slots — sidebar.panellist for the entry and main for the panel — never DOM injection |
| Skill paths | Uses the durable contracts for both paths: tool calls on tools/pre-execute / tools/result, and human invocations through the skill-invocation message source declared by the skills seam |
How it works
The two invocation paths — and why the human one is read from a message source
A skill can be reached two structurally different ways in DSH, and the plugin observes both.
| Model path | User path |
|---|
| Trigger | The model calls the skill tool | You invoke a skill (e.g. /grill-me) |
| What DSH does | Runs the tool pipeline and produces a tool result | Appends the skill's instructions to the conversation as a user/message — no tool call and no command event |
| Pairing key | callId | the message id |
| Duration measured | tools/result − pre-execute | turn/end − injection (see the note below) |
| Row marker | none | 用户触发 |
The human path is read from the structured source the skills seam itself attaches
(SkillInvocationSource, in the persistence catalog):
{ "type": "user/message", "data": {
"source": { "kind": "skill-invocation", "name": "grill-me", "form": "instructions" },
"role": "user", "id": "7c2e0f5a-3b9d-4e18-9a6c-1d2f3a4b5c6d" } }
The name comes straight from source.name, so no skill-catalog lookup and no text parsing is involved. That
is a measured result, not a guess: an earlier implementation assumed the human path went through
command/run / command/done, and a real session log disproved it — a human-invoked skill produces no
command event at all, which removed an entire catalog cache and its 60-second TTL.
One deliberate choice: a human skill has no result event of its own, so its duration is the turn window
(injection until turn/end), not the skill's own execution time. That is a stated convention, not a
measurement — if you would rather see an instantaneous event with a duration of 0, only the settling point
changes.
Status semantics
| Status | Meaning |
|---|
运行中 | A start was observed (a pre-execute or a skill message) and no ending event has arrived; the duration grows live |
已完成 | The tool reported isError === false, or the turn that carried the skill ended normally |
失败 | The tool reported isError === true; the row shows a truncated reason |
未观测到结果 | No ending event arrived within 15 minutes (the call was denied before dispatch, its parent was cancelled, the host crashed). This is a derived status and is never written back into the record |
The read-only contract
This is the plugin's core constraint, and there are tests asserting each row.
| Constraint | How it is implemented |
|---|
| Registers no writable tool | Never touches ctx.tools — no register, restrict or guard; apply() adds no member to ctx (asserted by a test) |
| Modifies no session state | Never appends a session event, never writes a session log, never calls a service method; only reads agents.list() / sessions.get() / sessionTitle.get() |
| Intercepts nothing | The tools/pre-execute listener returns next() untouched and never denies, cancels, rewrites or measures; a hostile arguments Proxy cannot make it block a call (asserted by a test) |
| Writes nothing to disk | All state is one in-memory Map, capped and expiring; no history, no persistence, no alerting |
| One read-only route | GET /api/plugins/skill-monitor/state only — no POST, no request body, no write path |
| Zero runtime dependencies | Neither half imports a third-party package |
Diagnostics — how a wrong assumption stays visible instead of silent
Every state response carries a diagnostics object describing what the plugin actually saw:
| Field | What it tells you |
|---|
sessionEvents | The durable event feed did reach the plugin |
skillInjections / trackedInjections | Human skill messages were seen and recorded |
unmatchedInjections | A skill-invocation message arrived whose name could not be read |
turnEnds / settledByTurnEnd | Turn endings arrived, and how many human records they closed |
This is not decoration: it is what caught the disproved command-path assumption in the first place.
Configuration (optional)
- id: skill-invocation-monitor
name: '@yyc-jnu/dsh-skill-invocation-monitor'
config:
maxRecords: 200 # in-memory record cap
retentionMs: 600000 # keep settled records for 10 minutes
staleMs: 900000 # no outcome within 15 minutes → 未观测到结果
maxCallsPerAgent: 20 # rows shown per Agent in the panel
Development
Tests, scripts and repository layout — for contributors
cd dsh-skill-invocation-monitor
npm run verify # node --check on all three entry files
npm test # 72 offline tests: node --test
72 offline tests cover three layers: the pure aggregation model (test/tracker.test.mjs, 36 — duration
settling for both paths, pairing keys, staleness, eviction and caps, Agent merging, field-name parsing of the
durable payload), the read-only contract itself (test/host-plugin.test.mjs, 25 — exactly five events, exactly
one GET route, a pure passthrough pre-execute listener, loopback fencing, disposal), and the browser bundle
(test/client-bundle.test.mjs, 11 — loaded the way the shell loads it, asserting the slot registrations and
the rendered output).
The plugin is plain ESM JavaScript: lib/ is the source, there is no build step and no src/.
dsh-skill-invocation-monitor/
├── package.json # name / exports / dsh.bundle.patch / dsh.client
├── cordis.patch.yml # the one profile row both halves hang off
├── lib/
│ ├── host/
│ │ ├── index.js # host half: 5 read-only observers + 1 GET route
│ │ └── tracker.js # pure aggregation model (no Cordis, no I/O, explicit timestamps)
│ └── client.js # browser half: ModuleLoader bundle, 2 slot registrations
├── docs/
│ ├── screenshot.png # the screenshot above
│ └── skill-monitor-flow.json # archify sequence-diagram source
└── test/ # 72 offline tests
License
MIT
Acknowledgements
The code in this plugin was written with AI assistance: the author defined the requirements, reviewed the
results and accepted them, while the AI produced the implementation and the documentation. Every behaviour was
verified against a running DSH instance; session titles, message ids and timestamps in tests and examples are
synthetic and contain no real user data.