package.json dual declaration: dsh.bundle (host) + dsh.client (browser)
src/org.ts pure projection: session events → the agency + its timeline (no imports, no IO)
src/index.ts Cordis host: event subscription, policy observation, the /abyss endpoints
client/client.js the embedded panel (hand-authored, no build step)
lib/index.js the host half, transpiled
tests/ 113 cases
Why the host half is built: dsh runs plugins under plain Node, and Node refuses to strip TypeScript inside node_modules. A plugin shipping a .ts entry works on its author's machine (where pnpm dsh runs under tsx) and fails the moment a user installs it. So the host half is transpiled to lib/index.js; the browser half is already plain JavaScript.
Zero runtime dependencies: @deepseek-ai/cordis is a type-only import erased at build time; only node: builtins are used at runtime.
src/org.ts is a set of pure functions reducing a session event stream into an agency. It knows nothing of the network, the filesystem, or cordis.
Events consumed (dsh declares 47; this reads 20 of them):
| Event | Projected into |
|---|---|
subagent/descriptor |
a hire: job title, firm, staff vs temp |
session/title |
the case's name (the same one the sidebar shows) |
request/header / request/context |
model route, context window |
turn/start / turn/end |
a shift on the attendance record |
assistant/chunk |
thinking / speaking |
assistant/message |
the spoken line, tokens, wage, context load |
tool/call |
activity; subagent → assign, send_message → message, report → report |
tool/result |
failures (error or the result block's isError) |
tool-workflow/agent-start|end |
members a workflow fanned out |
approval/asked / approval/decided |
waiting on a human, and the measured wait |
llm/retry |
a dropped call redialled |
compaction/start / compaction/summary |
archiving, and how many tokens went away |
todo/write |
the member's own task list |
Principles:
- Every spoken line is text from the log. With no text to read, the projection changes state rather than inventing a sentence.
- Returning the same reference means nothing changed, which is how the host decides whether to broadcast.
- No guessing: no context gauge without an advertised window; a non-zero shell exit is not a failure (dsh reports it as ordinary output, and sniffing that text would make every
grepwith no match an incident).
process (a dsh instance) the agency
└─ workspace (cwd) a project
└─ session tree a CASE: the root session plus every subagent under it
└─ agent session a member
Several sessions in one project are several parallel cases, not one flat roster — hence the three scopes (this case / this project / all), which every tab and the footer follow.
Past maxTeams, the host evicts only cases where no member is still on staff.
All served on the product's own origin under /abyss, with no CORS header — a permissive one would let any site you visit read your session titles, tool names and spend.
| Endpoint | Purpose |
|---|---|
GET /abyss/state |
snapshot (members, scenes, org chart, theme) |
GET /abyss/events |
SSE stream: snapshot / staff / scene / gone |
GET /abyss/replay?team=<id> |
rebuild a case from the durable logs (used both to replay and to rehydrate an office) |
GET /abyss/report?team=<id> |
the same rebuild rendered as a Markdown write-up |
GET /abyss/cases |
the archive: cases on disk (headers only, no logs opened) |
A composition with no web server (headless, ACP) can opt into a standalone data port with port; the office view itself is a Web UI feature.
- Live:
ctx.on('session/event'). - History:
ctx.sessionPersistencefirst, falling back toctx.sessions(only sessions open in this process). This is the entire reason an office survives a restart — reading only the live store 404s every case from yesterday. - A read that fails is reported: a corrupt log (a seq gap, say) comes back as
unreadable, and both the write-up and the replay reel say so. A report whose totals are quietly too small is worse than no report.
- A hand-authored lazy-CJS bundle registered through
window.__ModuleLoader__and served by the product under/plugins. - Everything interpolated is escaped: session titles, tool names and scene lines are model output, so one unescaped quote is an injection into the product's own page.
- Rendering is throttled to one animation frame and bounded: at most 12 cards per case on screen, at most 300 scenes in the browser.
- Transient state lives in the model, never only in the DOM — every render replaces the floor's markup.
- The org chart is derived in the panel, not taken from the wire; otherwise an office rebuilt from disk shows seven people on the floor and an empty chart.
- Theme follows
body[data-ds-dark-theme], language followshtml[lang], motion respectsprefers-reduced-motion.
npm test # 113: projection 46 + host wiring 15 + browser 52- The browser tests drive the shipped bundle, not a copy of its logic. Every client defect this project has hit — a shadowed global, an unescaped quote, DOM-only state, an inverted fold toggle, a motion class landing on text — is invisible to a test that reimplements the code.
- Host tests drive
apply()with real event shapes: endpoints, SSE, config validation, the persistence fallback, corrupt logs. - Live runs against the real product: watching the panel during real tasks, three concurrent sessions, a real approval escalation and real tool failures — with the plugin's numbers reconciled line by line against the raw logs.
- The live floor is in memory and rebuilt from disk after a restart;
compactionandllm/retryhave unit coverage but no live evidence (they occur 0 and 1 times in the real logs here). - A flying note needs both desks on screen; otherwise the sender still speaks.
- Replay paces by compressed real gaps, so long idle stretches are skipped rather than waited out.