Agent trajectory governance & anomaly diagnosis for DeepSeek Harness: rebuild structured multi-branch trajectory trees from the session/event feed, keep observation-layer snapshots, and run three temporal anomaly strategies (loop deadlock / invalid retry / goal drift).
Agent trajectory governance & anomaly diagnosis for DeepSeek Harness (dsh).
Rebuilds the flat session/event log into a structured, multi-branch trajectory
tree, keeps observation-layer snapshots, and runs three temporal anomaly
strategies (loop deadlock / invalid retry / goal drift) with results mounted on
tree nodes and surfaced in an independent GUI tab.
Zero kernel modification · consume-only event subscription · observation-layer
snapshots (no control-plane rollback) · independent SQLite storage · never
overwrites the official Trajectory view.
Why
The official Trajectory page is a flat raw-event log. For long-running agent
tasks (dozens of turns, subagents, forks) you cannot see:
which tool call belongs to which sub-task / subagent / fork branch,
where the agent started drifting from the original goal,
whether it is looping in place burning tokens (deadlock / invalid retry).
This plugin is an upper observation & reasoning layer: it consumes committed
events and derives structure, snapshots, and diagnoses — it never steers,
intercepts, or rolls back the agent. Both views coexist.
Data flow: session/event (durable, post-commit) → normalized + persisted →
tree/snapshot/diagnosis services → JSON API → the GUI tab. The diagnosis engine
runs asynchronously on an interval (incremental watermark + tail overlap) and
never blocks the agent loop.
Install
Requires dsh >= 0.1.0-rc.6. Two ways:
Bundle (published / git):
dsh plugin --profile web add github:dfycaly98931680/dsh-trajectory-governance#
A git install fetches sources, so the package ships a prepare build; pnpm
requires an explicit allow-list for it (add the printed key to the profile's
pnpm-workspace.yaml, then re-run):
allowBuilds:
dsh-trajectory-governance: true
Alternatively install a built tarball (pnpm pack) — no build permission needed.
Dev overlay (no pnpm):
dsh web --patch C:\path\to\dsh-plugin\overlay.dev.yml
Then restart the web app. You should see [trajectory-governance] loaded; storage=... sessions=N events=M anomalies=K in the logs.
Use
Run any agent task in the GUI (ideally a long one).
Open the 轨迹治理 / Trajectory Governance tab (a separate conversation.view
entry — the official Trajectory tab is untouched).
Pick a session: the tree renders with branch badges (subagent/fork),
snapshot marks (★), and red-highlighted anomaly ranges (⚠).
Click a node for the raw event + mounted diagnosis report (type, confidence,
severity, description, suggestion).
Use the snapshot bar to capture checkpoints at any node, list them, and jump
back; delete stale branches via the API/CLI.
CLI inspection utilities (against the plugin's own SQLite):
Full schema: DIAGNOSIS_CONFIG_SCHEMA in src/diagnose/config.ts.
Alerts & stop-loss (v0.2)
When a NEW anomaly is detected (confidence ≥ alerts.minConfidence), the plugin:
emits trajectory/anomaly on the Cordis bus (ecosystem notification plugins
can subscribe) and POSTs a text webhook if configured — the message is in
stop-loss language: type, confidence, and how much tokens/money/time the
range already consumed (from official assistant/message.usage);
the GUI tab polls /api/alerts, shows a desktop notification (browser
Notification API, permission button in the bar), and pins abnormal sessions
on top of the dropdown sorted by wasted tokens;
offers a one-click interrupt (POST /api/actions/interrupt) which calls
the official agent.cancel capability — never a monkey-patch. Default tier
is notify; auto-stop (loop deadlock only, confidence ≥ 0.9) can be
configured for unattended setups.
Breakpoints & waste reports (v0.3)
Breakpoint exit — the legitimate lightweight "rollback". Instead of a
control-plane rewind (already covered by dsh-turn-rewind),
this plugin offers a diagnosis-driven restart: at any snapshot / anomaly
start / selected node, "branch a new session here" resolves the target down
to the nearest stable turn/end boundary and calls the official
ctx.sessions.fork(source, boundary) — a fresh child session seeded at that
point. It never touches workspace files, keeps the observation-layer stance,
and turns "you wasted money here" into a one-click action.
Waste attribution report — not another usage-stats dashboard (that niche is
saturated): it answers the attribution question nobody else does, "which
sessions wasted tokens/money, on which anomaly types":
curl http://127.0.0.1:3080/trajectory-governance/api/report/waste # all sessions (Markdown + JSON)
curl "http://127.0.0.1:3080/trajectory-governance/api/report/waste?sessionId=" # one session detail
Both are also reachable from the tab ("浪费报告" / "本会话报告" buttons).
Quick demo (reproduce loop deadlock & goal drift)
Instant (no waiting for a real loop): seed a synthetic session into the
live store, refresh the tab, pick demo-session:
node scripts/demo-seed.mjs # writes into the default live DB
You should see a tree with red-highlighted loop_deadlock (5 identical tool
calls) and a severe goal_drift range, plus a snapshot mark (★).
Real agent:
Give the agent a long, open-ended refactor prompt (e.g. "重构这个项目为
TypeScript 并补单元测试,先读代码再动手").
Let it run until it starts repeating the same tool call (same file, same
content) with no visible progress — Strategy A flags loop_deadlock on the
trigger node.
Ask a follow-up in a different domain ("顺便帮我看看怎么做番茄意面") — after
N sampled turns Strategy C flags goal_drift (mild/moderate/severe) against
the original baseline.
Verify: the tree tab highlights the ranges in red; scripts/inspect.mjs shows the
anomaly counts; scripts/tree.mjs dumps the mounted anomalyInfo.
Screenshots
TODO — add after the first live run: tree tab overview / anomaly card / snapshot
list. (Placeholders reserved here.)
Testing
npm install
npm run build && npm test # 46 tests: normalize/store/subscriber/tree/snapshot/diagnose/api
Boundaries (important)
Zero kernel modification — no fork, no monkey-patch, no host storage access.
Consume-only events — session/event etc. are emit-mode, post-commit,
fire-and-forget listeners; payloads are never mutated.
Observation-layer snapshots — a snapshot is an index (nodeId + context hash
branchId); context is rebuilt from the event store. This plugin never forks,
resumes, or rewinds the agent.
Independent Tab — registered with its own slot id; the official Trajectory
page (id trajectory) is never overwritten.
Preview API — SESSION_FORMAT_VERSION = 0, no compatibility implied. The
event vocabulary is centralized in src/core/event-catalog.ts; unknown types
honor the envelope's ignorable marker.