Zum Hauptinhalt springen
Y

dsh-sessions-diagnosis

youhui1/dsh-sessions-diagnosis

Diagnose and repair DSH sessions that stopped opening after a harness upgrade — from the CLI, from an agent tool, or from a dashboard in the DSH web UI.

Installation

dsh plugin --profile web add github:youhui1/dsh-sessions-diagnosis

README

dsh-sessions-diagnosis

Diagnose and repair DSH sessions that stopped opening after a harness upgrade — from the CLI, from an agent tool, or from a dashboard in the DSH web UI.

中文说明 · Root cause write-up


The problem

DSH stores each session as a directory holding one immutable file per format generation, and migrates old files lazily when you open them. Since 0.1.5 the final migration step is a closed-vocabulary audit: every message must carry a source.kind from a fixed set of fifteen values, and anything else aborts the whole migration.

One retired value is enough to make a conversation unopenable. The culprit in the wild is at-file-mention — the provenance tag older builds wrote for the synthetic user message created by an @path mention. So the breakage looks arbitrary: it hits exactly the sessions where somebody once typed @.

The session still lists (listing only reads the header), but opening it fails with a generic gateway/internal error that says nothing about the cause. And a failed migration writes nothing, so there is no evidence on disk to inspect.

Full root-cause analysis

What this does

DiagnoseRuns DSH's own released migration chain over your stored logs, so the reported reason is exactly what the harness hit — not a reimplementation or a guess. Classifies every session as ok, unmigrated, repairable, unrepairable, corrupt, too-new, or empty, down to the offending field — what each verdict means.
RepairProduces a valid current-format successor and lets DSH select it. Verified before anything is written. The original file is never modified.
VisualiseA dashboard in the DSH web UI: every session with its verdict and reason, plus one-click repair with a dry-run confirmation step.

Screenshots

The dashboard — Settings → Session diagnosis. Every stored session with its verdict and the reason behind it, one Inspect per row, and a repair that always shows its planned edits before it writes anything.

The Session diagnosis dashboard in the DSH web UI

One session's detail, expanded beneath its own row: the artifact DSH opens, the findings behind the verdict, and what a repair guarantees.

An expanded session detail in the same panel

The panel is localized — both screenshots happen to be running in Chinese.

Install

From a checkout. The . needs no absolute path: dsh rewrites a relative spec against the directory you run it in (pnpm itself runs inside the profile, where . would otherwise mean the profile).

git clone https://github.com/YouHui1/dsh-sessions-diagnosis.git
cd dsh-sessions-diagnosis
dsh plugin --profile web add .

dsh detects dsh.bundle.patch, links the package into the profile, and appends dsh-sessions-diagnosis to dsh.profile.bundles. The dependency is saved as link:<your checkout>, so the profile reads the code where you cloned it — pull and restart to update, no re-install. Then restart DSH and hard-refresh the browser. The dashboard appears in Settings → Session diagnosis.

To install without keeping a checkout, take the same package straight from GitHub:

dsh plugin --profile web add github:YouHui1/dsh-sessions-diagnosis

That fetches a copy rather than linking to your directory, so a newer version arrives with dsh plugin --profile web update dsh-sessions-diagnosis.

Either way there is no build step and no dependency install: the plugin ships prebuilt JavaScript and resolves DSH's own format codec at runtime.

Repairing a session does not require a restart — DSH resolves generations at open time, so a repaired session opens on the next click.

Usage

Command line

The CLI is a script in this repo, so it works straight from a checkout with no install step:

node bin/dsh-session-doctor.mjs scan

The examples below drop the node bin/ prefix for readability — read dsh-session-doctor scan as node bin/dsh-session-doctor.mjs scan. To get the bare command instead, run npm link once from the repo root (it writes to your global npm prefix); after that dsh-session-doctor is on PATH like dsh.

dsh-session-doctor scan                    # what is in the store
dsh-session-doctor diagnose                # classify every session
dsh-session-doctor diagnose <id>           # one session, in detail
dsh-session-doctor repair <id>             # dry run — writes nothing
dsh-session-doctor repair <id> --apply     # perform the repair
dsh-session-doctor repair --all --apply    # repair everything repairable
dsh-session-doctor rollback <id>           # undo a repair this tool made
dsh-session-doctor verify                  # restore every session with DSH's codec
dsh-session-doctor demo                    # create a synthetic broken session
dsh-session-doctor demo --remove --apply   # delete it again
dsh-session-doctor rules                   # list the repair rules

--json on any command for machine-readable output; --dsh-home <path> to point at a non-default store.

Try it without risking your data

dsh-session-doctor demo                          # a retired source.kind (low-risk rule)
dsh-session-doctor demo --scenario descriptor    # a stale descriptor version (medium-risk rule)
dsh-session-doctor demo --cwd D:\my\project      # record a specific working directory
dsh-session-doctor demo --id my-demo-session     # pin the session id

This writes a synthetic broken session — no real conversation is involved. It is a genuine released-v0 log, so DSH refuses it for exactly the reason it refuses a real one, and the same rules repair it.

A demo never goes into your session store. It is a deliberately unreadable session: putting it where DSH actually reads would place a broken conversation beside yours, add a row to a sidebar the demo has no business being in, and make the demo's id part of your harness's durable state. So demo writes to a throwaway store of its own — by default <tmp>/dsh-session-doctor-demo, relocatable with DSH_DEMO_HOME — and refuses --root pointed at the real store, whatever spelling reaches it:

refusing to create a demo in the real session store (C:\Users\you\.dsh\sessions).
A demo is a deliberately broken session; it must not join your conversations.

The demo store is a complete DSH home, so the whole flow runs against it exactly as it would against yours:

dsh-session-doctor demo                                  # create it
dsh-session-doctor diagnose <id> --root <demo-store>      # it will not open
dsh-session-doctor repair   <id> --root <demo-store> --apply
dsh-session-doctor rollback <id> --root <demo-store>
dsh-session-doctor demo --remove --apply                  # delete it again

To watch the same flow in the dashboard, point a throwaway harness at that home — never the one you are using:

$env:DSH_HOME = "<tmp>/dsh-session-doctor-demo"; dsh web

Cleanup is guarded twice, because a demo is an ordinary session once it exists:

  • Ownership is proved, not assumed. A demo carries a session.diag-demo.json marker that only this tool writes, and it must name the directory it sits in. The session id is never trusted — an id is just a string, and a real conversation could end up with a demo-looking one. Such a session is invisible to cleanup.
  • A demo you have talked in is kept. The marker records the exact bytes that were generated, so cleanup can tell an untouched demo from one that has been resumed. Anything containing content this tool did not write is reported and left alone unless you pass --force.

Earlier versions of this tool did write demos into the real store, and archiving one in the sidebar leaves its id in the workspace registry afterwards. Such a leftover is harmless — a stale id matches nothing, and this tool does not edit that file — and demo --remove still cleans those stores up. New demos are never created there.

Agent tools

Three tools are registered for the model:

  • session_store_overview — cheap census (header frames only)
  • session_diagnose — full classification, store-wide or one session
  • session_repairdry run by default; writes only with apply: true, and action: "rollback" undoes a repair it made

Dashboard

Settings → Session diagnosis. Scan, inspect a verdict with its exact planned edits, and repair — the button always runs a dry run first and shows you what it will change before it touches anything. What the verdicts mean explains all seven verdicts in the panel, and every badge carries the same sentence as a tooltip, so a label like "unmigrated" never needs a trip to this file to be understood. A session repaired by this plugin shows its repair record and an Undo this repair button. Inspect expands that session's detail directly beneath its own row (click again to collapse it), so opening a session near the bottom of a long list never scrolls you back to the top.

Safety model

Every guarantee below is asserted by the test suite, not merely intended.

Your original data is never modified

The default strategy publishes a new generation beside the original and leaves the original byte-for-byte untouched — the same additive publication DSH itself does. Verified by comparing the SHA-256 of the source before and after, on real sessions and in the tests.

The conversation content is provably preserved

Structural validity is not content preservation: a migration could emit a perfectly readable log that quietly dropped messages. So every repair extracts each message payload from the source and requires all of them to appear in the result. Additions are allowed — v2→v3 is specified to insert an empty system head and to promote the recorded prompt out of the request header — but a loss or a rewrite aborts the repair before anything is written. The result is reported in every repair outcome.

Nothing is written until it has been proven twice

  1. The patched log is migrated with DSH's real v0→v3 chain.
  2. The result is encoded with DSH's own current-format encoder.
  3. The bytes are re-read and must pass the storage layer's structural admission and a full restore under both validation policies.
  4. Every source message payload must survive.

Any failure means no file appears at all.

Failures are atomic

Publication stages a complete file and then hard-links it into place, so a reader sees either a complete generation or none. Where hard links are unavailable the exclusive-copy fallback is verified and removed again on mismatch — a partially written generation would be worse than none, because DSH selects it in preference to the readable older one.

Repair is reversible

Each repair records what it did in a sidecar (session.diag-repair.json, which DSH ignores) holding the SHA-256 of both files. rollback then:

  • moves the published generation aside — it is never deleted — and
  • refuses if that file has changed since the repair, because DSH appends to the current generation once you use the session again, and undoing then would discard everything written since.

patch-source is the careful fallback

Only for environments where DSH's codec cannot be reached. It verifies the backup copy is byte-identical before touching the original, confirms the original has not changed since it was read, and replaces it via a staged file and a rename rather than truncating it in place.

The one thing no tool can undo is a session you delete yourself. Nothing here ever removes a session log.

See also trap #2.

Repair rules

RuleRiskWhat it fixes
unclassified-message-sourcelowRewrites a retired source.kind (e.g. at-file-mention) to user. The @-mention path is not lost — it is already in the message content.
subagent-descriptor-versionmediumCorrects a stale subagent/descriptor.data.version to the 3 the released codec requires.

A repair runs every rule that matches, so a session the diagnosis called repairable is always actionable, and the result reports which risk tiers ran. The risk label is a disclosure, not a hidden gate: the dry run and the explicit apply are the consent step.

The one exception is a bulk sweep: repair --all stays on the low-risk rules unless you pass --include-medium-risk, because there is no per-session dry run to look at. It names the sessions it left alone rather than skipping them silently.

Adding a rule

A rule is data, not code paths: one entry in lib/core/rules.js.

{
  id: 'my-rule',
  title: 'One line describing the fix',
  risk: 'low',                       // 'low' runs by default, 'medium' is opt-in
  rationale: 'Why this edit is correct, in prose.',
  detect (rows) {                    // -> Finding[]
    return rows.filter(...).map((row) => ({
      ruleId: 'my-rule', seq: row.seq, type: row.type,
      path: 'data.someField', before: ..., after: ...,
      detail: 'shown to the user'
    }))
  },
  apply (rows, findings) {           // mutate rows in place, return edit count
    ...
  }
}

The framework does the rest: findings are shown as before/after pairs, the repair is simulated before it is offered, and every write is verified. Add a test to test/run.mjs.

How it works

lib/core/
  frames.js     concatenated-Zstandard-frame container (scan / decode / encode)
  store.js      locate DSH_HOME, enumerate generations, the source-kind vocabulary
  format.js     find DSH's format catalog at runtime; run its real migration
  rules.js      the repair-rule catalogue
  diagnose.js   static audit + live migration probe + repair simulation
  repair.js     plan, verify, publish
lib/index.js    host half: agent tools + dashboard JSON API
lib/client.js   browser half: the dashboard panel
bin/            standalone CLI

The core is plain Node.js with no DSH imports — DSH's codec is located and imported at runtime — so the identical code backs the plugin, the CLI, and the tests.

Nested frames are decoded by walking frame and block headers structurally rather than by decompressing, because zstdDecompressSync() stops after the first frame of a concatenated container.

Gotcha: DSH's JSON Schema subset is enforced at boot

If you extend this plugin with a tool, note that ctx.tools.register() validates your output.schema immediately — while the profile is booting. A schema outside DSH's supported subset does not degrade gracefully; it throws, the loader entry fails, and dsh web refuses to start:

Error: dsh: plugin tree failed to load: failed to apply loader entry <id> ...
unsupported JSON schema: schema.properties.<x>.additionalProperties is not supported on type "array"

The subset allows one scalar type, properties / required / boolean additionalProperties (objects only), items (arrays only), scalar enum / const, and exact-one oneOf. Misplaced keywords reject rather than being ignored.

test/run.mjs guards against this by running DSH's own assertSupportedJsonSchema and validateJsonSchemaValue over every definition and over every value the tools return, and by mounting the plugin against the real cordis Context and ToolRuntime — the same call that fails at boot.

Requirements

  • Node.js ≥ 22.15 (for built-in node:zlib Zstandard support)
  • DSH 0.1.5-rc.2 for the plugin halves; the CLI needs a session store and, for the strongest diagnosis, a DSH install to borrow the format codec from

Tests

node test/fetch-fixtures.mjs   # copy a broken generation from your own store (optional)
node test/run.mjs

The suite covers the frame container, generation naming, the rule engine, the diagnosis verdicts, and the full repair path end-to-end. Real-session tests read a fixture from .scratch/ and skip cleanly when it is absent, so the suite runs in a bare checkout.

The fixture is a verbatim copy of one of your conversations. .scratch/ is git-ignored — do not commit it or attach it to an issue.

License

MIT

Ähnliche Plugins