Environment
- dsh version:
0.1.0-rc.7 (dsh --version; installed at C:\Users\30816\AppData\Roaming\npm, running @deepseek-ai/dsh\lib\bin.js web)
- dsh-notification version:
0.1.4 (installed via dsh plugin --profile web add <v0.1.4 tarball>)
- OS: Windows (paths below are from this machine)
- Profile: default
web profile (C:\Users\30816\.dsh\profiles\web)
Summary
After installing dsh-notification, every session in the GUI became unloadable — opening any conversation shows no history — and in the session list all session names collapsed to the workspace directory basename (e.g. every session under D:\desktop\workspace\dsh showed up as dsh). The breakage is global, not limited to sessions the plugin ever touched, and it is not a settings/rules issue: it happens with default settings too.
Root cause: the projection definition contract drifted again (0.1.1 schema/view vs 0.1.2 stateSchema/wire)
This is the same class of API drift as #15 and #17, but on dsh 0.1.0-rc.7 the mismatch is now inverted and has a much worse blast radius.
dsh-notification@0.1.4 registers its unit with the 0.1.2 shape (confirmed in src/projection.ts in this repo):
return {
key: 'notification',
stateSchema: z.object({ openTurn: ..., last: ... }).strict(), // <-- stateSchema
init: () => ({ openTurn: null, last: null }),
apply: (state, event) => applyProjectionEvent(state, event, config.maxBodyChars),
wire: { viewSchema, view: state => state.last ?? EMPTY_PROJECTION }, // <-- wire.{viewSchema,view}
stateVersion: 1,
}
But on dsh 0.1.0-rc.7 the registry only ever reads the 0.1.1 shape — top-level schema and view. From the installed runtime, @deepseek-ai/dsh-session-projection/lib/index.js:
// snapshot()
values[registration.def.key] = registration.def.schema.parse(registration.def.view(cell.state))
// viewCheckpoint()
if (row === void 0 || row.ver !== def.stateVersion) continue;
values[def.key] = def.schema.parse(def.view(row.val));
// restore()
values[def.key] = def.schema.parse(def.view(state));
There is no stateSchema / wire fallback anywhere in dsh-session-projection, dsh-session-projection-cache, or dsh-host-apiproxy in rc.7. Every built-in unit on this build uses the old shape, e.g. @deepseek-ai/dsh-session-title/lib/index.js:178:
ctx.inject(["sessionProjections"], (projectionCtx) => {
projectionCtx.sessionProjections.register({
key: "title",
schema: z$1.union([z$1.string().min(1), z$1.null()]), // <-- schema, not stateSchema
init: () => null,
apply: (state, event) => event.type === "session/title" ? event.data.title : state,
view: (state) => state, // <-- view, not wire.view
stateVersion: 1,
});
});
So on rc.7 the plugin's registration is accepted (nothing validates the definition shape at register(), see below), and then every later read of that unit dereferences undefined: def.schema.parse(...) throws TypeError: Cannot read properties of undefined (reading 'parse'), or def.view is not a function.
Why this breaks all sessions, not just the ones the plugin touched
This is the part that makes the symptom look much bigger than a notification plugin. The registry is a single shared table, and the failure escapes per-key containment in two ways:
snapshot() / restore() iterate every registered unit and have no per-unit try/catch. @deepseek-ai/dsh-session-projection/lib/index.js:105-235 loops for (const registration of this.registrations.values()) and calls def.schema.parse(def.view(...)) inline. One malformed unit throws out of the whole loop, so the other keys — including title — never make it into the returned values, and the caller sees a failed/short read for sessions that have nothing to do with notifications.
restore() throwing triggers the pathological path in the cold-read ladder. In @deepseek-ai/dsh-session-projection-cache/lib/index.js:186-195, coldSnapshot calls restore(cached, tail.events, floor) inside a try; on any throw it retries restore({}, whole.events, 0) — a full log re-read from seq 0 that throws again for the same reason, so opening any session ends with no projection values and no history rendered.
Why every title becomes the workspace basename
The title projection never reaches the client, so the client's display-title projection falls through to its own second rung. @deepseek-ai/dsh-client-runtime/lib/client.js:8821-8835:
function workspaceTitleOf(cwd) {
return cwd.replace(/[/\\]+$/, "").split(/[/\\]/).pop() ?? "";
}
function displayTitleOf(title, cwd, id) {
if (title !== void 0) return title; // durable title (missing -> falls through)
if (cwd !== void 0 && cwd !== "") {
const base = workspaceTitleOf(cwd);
if (base !== "") return base; // <-- workspace basename
}
return id;
}
That is exactly the reported symptom: with title absent, every row renders its cwd's last path segment. On this machine all sessions under D:\desktop\workspace\dsh render as dsh, and the workspace's own stored title in C:\Users\30816\.dsh\storages\workspace.json is 通用 — matching what the reporter saw.
Evidence from the affected machine
The durable projection cache C:\Users\30816\.dsh\storages\session_projcache.json (session_projcache domain, version 3) holds 50 sessions, and the corruption is visible in its row keys:
rowKeys: sessionStats, title, goal, tokenUsage, contextPressure, contextBreakdown,
subagentTiming, subagent, permissions, sessionListMetadata, imageLimits, todos, plan
sessions with notification key: 0
titleVerHistogram: {"v1":50}
Two things worth flagging:
- No
notification row was ever persisted for any of the 50 sessions. The unit registered but never produced a foldable/checkpointable value, consistent with the definition never being readable on rc.7.
- The
title rows are intact (ver: 1, matching dsh-session-title's stateVersion: 1 on this build) and their val is the correct, previously-generated title, e.g. {"ver":1,"seq":1175,"val":"安装这个到deepseek harness"}. The user's titles are still on disk and unharmed. They simply stop being served because the shared read path throws before reaching them. That is good news for recovery: nothing needs to be regenerated, and removing the plugin should restore the titles (see below).
Also confirmed the served bundle after uninstall no longer contains the plugin — GET http://127.0.0.1:3080/ returns a __DSH_BOOT__ payload whose 44 client entries contain no notification entry (hasNotificationEntry=False), and C:\Users\30816\.dsh\plugins has no notification directory or tarball left. So this report is about the window in which the plugin was installed; the durable side effects above are the residue.
Secondary issue: register() does not validate the definition shape
SessionProjectionRegistry.register() (@deepseek-ai/dsh-session-projection/lib/index.js:58-81) validates only stateVersion:
if (!Number.isSafeInteger(definition.stateVersion) || definition.stateVersion < 0) throw new Error(...)
It never checks that schema/stateSchema/view/wire are present, and it does not reject an unknown-key definition. A plugin built against the other generation of this contract is therefore silently admitted and only fails much later, deep inside every read path, where the blast radius is the entire projection table instead of one key. This is what turned a version mismatch into "the whole GUI can't open any conversation".
Suggested fixes
- Ship a definition that satisfies both generations. Since 0.1.4 already claims to "support both the 0.1.1 and 0.1.2 Harness client package layouts", extend that to the host projection definition: emit both
schema/view and stateSchema/wire.{viewSchema,view} (with schema validating the view, i.e. viewSchema, since rc.7 parses def.view(state) with def.schema). Keep stateVersion: 1.
- Feature-detect at registration time as a belt-and-braces measure: register the shape matching the running runtime by probing for a marker of the 0.1.2 layout before calling
register.
- Upstream (worth a separate issue on the harness): validate the definition shape in
register(), and wrap the per-unit loop bodies in snapshot()/viewCheckpoint()/restore() so one malformed unit degrades to an absent key for itself instead of failing every session's read. Also, coldSnapshot's catch-all "re-read from seq 0" retry should not fire when the failure is a shape error the retry cannot fix.
Workaround / what I did
Uninstalled the plugin and restarted the web server; the served bundle no longer includes it. Titles and history should come back on their own because the title rows on disk are still valid at ver: 1. If any session still shows the workspace basename afterwards, deleting C:\Users\30816\.dsh\storages\session_projcache.json forces a clean refold from the session logs — safe here because the cache is an explicitly non-authoritative fold shortcut (its own docs: "a stale or unreadable cache costs a longer tail replay, never a wrong value").
Related
This is the same drift, now hitting users who are on dsh 0.1.0-rc.7, with the added symptom that a single bad unit takes down history loading for all sessions.
Environment
0.1.0-rc.7(dsh --version; installed atC:\Users\30816\AppData\Roaming\npm, running@deepseek-ai/dsh\lib\bin.js web)0.1.4(installed viadsh plugin --profile web add <v0.1.4 tarball>)webprofile (C:\Users\30816\.dsh\profiles\web)Summary
After installing
dsh-notification, every session in the GUI became unloadable — opening any conversation shows no history — and in the session list all session names collapsed to the workspace directory basename (e.g. every session underD:\desktop\workspace\dshshowed up asdsh). The breakage is global, not limited to sessions the plugin ever touched, and it is not a settings/rules issue: it happens with default settings too.Root cause: the projection definition contract drifted again (0.1.1
schema/viewvs 0.1.2stateSchema/wire)This is the same class of API drift as #15 and #17, but on dsh
0.1.0-rc.7the mismatch is now inverted and has a much worse blast radius.dsh-notification@0.1.4registers its unit with the 0.1.2 shape (confirmed insrc/projection.tsin this repo):But on dsh
0.1.0-rc.7the registry only ever reads the 0.1.1 shape — top-levelschemaandview. From the installed runtime,@deepseek-ai/dsh-session-projection/lib/index.js:There is no
stateSchema/wirefallback anywhere indsh-session-projection,dsh-session-projection-cache, ordsh-host-apiproxyin rc.7. Every built-in unit on this build uses the old shape, e.g.@deepseek-ai/dsh-session-title/lib/index.js:178:So on rc.7 the plugin's registration is accepted (nothing validates the definition shape at
register(), see below), and then every later read of that unit dereferencesundefined:def.schema.parse(...)throwsTypeError: Cannot read properties of undefined (reading 'parse'), ordef.view is not a function.Why this breaks all sessions, not just the ones the plugin touched
This is the part that makes the symptom look much bigger than a notification plugin. The registry is a single shared table, and the failure escapes per-key containment in two ways:
snapshot()/restore()iterate every registered unit and have no per-unit try/catch.@deepseek-ai/dsh-session-projection/lib/index.js:105-235loopsfor (const registration of this.registrations.values())and callsdef.schema.parse(def.view(...))inline. One malformed unit throws out of the whole loop, so the other keys — includingtitle— never make it into the returnedvalues, and the caller sees a failed/short read for sessions that have nothing to do with notifications.restore()throwing triggers the pathological path in the cold-read ladder. In@deepseek-ai/dsh-session-projection-cache/lib/index.js:186-195,coldSnapshotcallsrestore(cached, tail.events, floor)inside atry; on any throw it retriesrestore({}, whole.events, 0)— a full log re-read from seq 0 that throws again for the same reason, so opening any session ends with no projection values and no history rendered.Why every title becomes the workspace basename
The
titleprojection never reaches the client, so the client's display-title projection falls through to its own second rung.@deepseek-ai/dsh-client-runtime/lib/client.js:8821-8835:That is exactly the reported symptom: with
titleabsent, every row renders its cwd's last path segment. On this machine all sessions underD:\desktop\workspace\dshrender asdsh, and the workspace's own stored title inC:\Users\30816\.dsh\storages\workspace.jsonis通用— matching what the reporter saw.Evidence from the affected machine
The durable projection cache
C:\Users\30816\.dsh\storages\session_projcache.json(session_projcachedomain, version 3) holds 50 sessions, and the corruption is visible in its row keys:Two things worth flagging:
notificationrow was ever persisted for any of the 50 sessions. The unit registered but never produced a foldable/checkpointable value, consistent with the definition never being readable on rc.7.titlerows are intact (ver: 1, matchingdsh-session-title'sstateVersion: 1on this build) and theirvalis the correct, previously-generated title, e.g.{"ver":1,"seq":1175,"val":"安装这个到deepseek harness"}. The user's titles are still on disk and unharmed. They simply stop being served because the shared read path throws before reaching them. That is good news for recovery: nothing needs to be regenerated, and removing the plugin should restore the titles (see below).Also confirmed the served bundle after uninstall no longer contains the plugin —
GET http://127.0.0.1:3080/returns a__DSH_BOOT__payload whose 44 client entries contain no notification entry (hasNotificationEntry=False), andC:\Users\30816\.dsh\pluginshas no notification directory or tarball left. So this report is about the window in which the plugin was installed; the durable side effects above are the residue.Secondary issue:
register()does not validate the definition shapeSessionProjectionRegistry.register()(@deepseek-ai/dsh-session-projection/lib/index.js:58-81) validates onlystateVersion:It never checks that
schema/stateSchema/view/wireare present, and it does not reject an unknown-key definition. A plugin built against the other generation of this contract is therefore silently admitted and only fails much later, deep inside every read path, where the blast radius is the entire projection table instead of one key. This is what turned a version mismatch into "the whole GUI can't open any conversation".Suggested fixes
schema/viewandstateSchema/wire.{viewSchema,view}(withschemavalidating the view, i.e.viewSchema, since rc.7 parsesdef.view(state)withdef.schema). KeepstateVersion: 1.register.register(), and wrap the per-unit loop bodies insnapshot()/viewCheckpoint()/restore()so one malformed unit degrades to an absent key for itself instead of failing every session's read. Also,coldSnapshot's catch-all "re-read from seq 0" retry should not fire when the failure is a shape error the retry cannot fix.Workaround / what I did
Uninstalled the plugin and restarted the web server; the served bundle no longer includes it. Titles and history should come back on their own because the
titlerows on disk are still valid atver: 1. If any session still shows the workspace basename afterwards, deletingC:\Users\30816\.dsh\storages\session_projcache.jsonforces a clean refold from the session logs — safe here because the cache is an explicitly non-authoritative fold shortcut (its own docs: "a stale or unreadable cache costs a longer tail replay, never a wrong value").Related
@deepseek-ai/dsh-client-runtimewas removed upstream (createSnapshotStore→@deepseek-ai/dsh-client-store) #17 (dsh 0.1.2:createSnapshotStoremoved out ofdsh-client-runtime)schema→stateSchema, top-levelview→wire.{viewSchema,view}) #15 (dsh 0.1.1:schema→stateSchema, top-levelview→wire.{viewSchema,view})This is the same drift, now hitting users who are on dsh
0.1.0-rc.7, with the added symptom that a single bad unit takes down history loading for all sessions.