12 KiB
Agent Note: Regroup packages/ by measured clustering
Status: implemented
English | 中文
Problem
The two-level packages/<group>/<pkg> hierarchy (original decision) had drifted since June: 167 packages sat in 42 groups, and several group boundaries no longer matched how the packages actually cluster.
ui/mixed four unrelated planes: the human terminal channel (tui), the SDK's JSON-RPC server half (jsonrpc, whose peer dependency ondsh-sdk-protocolbinds it to the SDK wire stack), the human-interaction seams (user-interaction,user-approval,permission,tool-ask-user,commands), and channel-neutral boot glue (app-boot). Its own README narrated the mixture instead of stating a role.- The session family was fragmented across five groups —
session-persistence/,session-projection/,session-query/,session-title/, andtelemetry/— although the measured dependency edges tie them together (query → persistence, title → projection, projection → persistence; see docs/module-graph.md). - The
timeout/group for a tool-call guard collided withutil/timeout, the generic promise utility. cordis/named its group after the framework every package is built on, so the name discriminated nothing; its single packagetool-cordisis the runtime self-modification toolset.
The north star for the regrouping: closely clustered packages share a group. A cluster is measured — peer-dependency edges and co-change — not thematic. An isolated seam family may stand alone as a small group; the failure mode to avoid is the grab-bag whose name describes no single role.
Decision
Five regrouping decisions remain current; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — bash/, pty/, code-runtime/, sandbox/, subprocess/, fs/, lsp/, web/, skill/, and the rest — were already drawn correctly). The original sixth decision collected the SDK project initializer, launcher tooling, and runtime JSON-RPC packages under scaffold/; removing that unreleased toolchain superseded it by deleting the project tooling and moving the surviving runtime trio to sdk/.
| Group | Members (folder names) | From |
|---|---|---|
session/ |
session-persistence, session-persistence-jsonl, session-persistence-sqlite, session-checkpoint-policy, session-projection, session-projection-cache, session-title, session-title-llm, session-title-first-message-llm, session-title-all-messages-llm, session-telemetry, session-telemetry-otel | session-persistence/ + session-projection/ + session-title/ + telemetry/ |
interaction/ |
user-interaction, user-approval, permission, tool-ask-user, commands, tui | ui/ |
boot/ |
app-boot | ui/ |
guard/ |
repeat-tool-guard, timeout-policy | guard/ + timeout/ |
self-modification/ |
tool-cordis | cordis/ |
session/is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (session-querypeer-depends ondsh-session-title), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearbycore/sessionpackage remains the live in-memory service, while this group is the durable family around it.session-query/stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals.interaction/is the human-collaboration plane plus the terminal channel that answers it: the question/approval seams, the permission preset, the model-facingask_user_questiontool, the human-command registry (plan-modeandcommand-goalalready consumecommandstogether with the interaction seams), andtui— the interactive channel is the plane's richest provider and consumer (peer edges tocommandsanduser-interaction), and a one-packagetui/group would spend a top-level name on one plugin.boot/is a role-complete single-package group: the shared bin boot glue that belongs to no channel and no assembly (consumed byapps/cliand theexamples/demo bins).guard/keeps its documented role, loop-hygiene guards, and gains the tool-call timeout enforcer, dissolving the one-packagetimeout/group whose name collided withutil/timeout.self-modification/names the rolecordis/obscured: the toolset with which the agent inspects and mounts plugins in its own live runtime, and the landing zone for future self-modification packages.
42 groups became 39; the win is clustering correctness and truthful names, not count.
Deferred renames (FIXME markers)
Two npm names should eventually change, but renaming inside the reorganization would have turned a pure-move PR into an import-churn PR. Each affected package's module JSDoc carries a FIXME naming the intended new name. FIXME blocks a tagged release (marker semantics), which is the wanted forcing function: these renames are only free while nothing external consumes the packages. Removing the unreleased SDK project toolchain deleted the other three packages and their rename markers instead of preserving names for code that no longer exists.
| Current npm name | Intended name | Why |
|---|---|---|
@deepseek-ai/dsh-jsonrpc |
@deepseek-ai/dsh-sdk-server |
Names the wire encoding, not the role; it is the server half of the SDK protocol |
@deepseek-ai/dsh-timeout-policy |
@deepseek-ai/dsh-timeout-guard |
Suggestion, not settled: aligns the name with its guard/ home; decide at resolution time |
What the move touched
The moves landed as pure git mv moves, so rename detection carries the history. A group move touched: the moved package's tsconfig.json relative references and every dependent's entry (including the apps/cli project references), the tsconfig aggregate and path maps, group READMEs, the packages/README.md hierarchy table, the root AGENTS.md layout map, regenerated artifacts (docs/module-graph.md, path-embedding catalogs, and the lockfile's importer keys), and root-relative packages/... citations in prose and gate scripts. Remaining group-path referents (workspace configs, test globs, lint keys) were found mechanically by the acceptance gates failing loud — the repository's own misconfiguration rule.
A group move did not touch: npm names, imports, cordis.yml configs, snapshot fixtures, the pnpm-workspace.yaml/tsdown globs (both packages/*/*), or the Python runtime manifest — all reference packages by npm name.
client/ and host/ were out of scope and are unchanged.
Alternatives considered
Coarse domain buckets (exec/ = subprocess+sandbox+bash+pty+code-runtime, workspace/ = fs+lsp+workspace, orchestration/ = subagent+workflow+tasks, knowledge/ = web+skill, collab/ = plan+todo+goal; ~16 groups). Rejected: the measured graph contradicts the merges. sandbox and subprocess are shared infrastructure consumed across families (bash ×5, fs ×5, pty, lsp, mcp, and subagent edges), web ↔ skill have zero edges, and a large bucket reproduces the ui/ grab-bag at a larger scale.
Abstract layer names (capability/, policy/, extension/, provider/). Rejected: they describe every plugin equally badly, and a capability/ bucket would hold ~50 packages.
A full npm rename sweep (dsh-<group>-<pkg> for every package). Rejected: npm names are flat, so group-prefixing adds churn across imports, configs, and fixtures with no disambiguation gain; targeted FIXME-tracked renames cover the actual collisions.
Performing the deferred renames inside the reorganization. Rejected: renames multiply open-PR conflicts and destroy the pure-move review property. The remaining FIXME markers keep them visible release blockers to resolve as small follow-up PRs.
A two-way session split (session-core/ + session-utils/). Rejected: query belongs to neither side cleanly, and session-core invites confusion with core/session (dsh-session, the live in-memory service, which stays in core/).
A three-way session split (session-store/ + session-query/ + session-utils/). Rejected: session-utils/ was a negatively-defined annex ("derived, nothing load-bearing depends on it") — the grab-bag shape the north star forbids, and factually wrong besides (session-query peer-depends on dsh-session-title). The invented compound names also read machine-generated; one plain session/ group says what a human would say. Query stays standalone regardless: it is an independently consumed read surface with its own tool package and backend.
Recomposing ui/ as a single channels/ group (tui + jsonrpc + acp + interaction seams + boot). Rejected: the same grab-bag under a new name — those packages serve four planes, jsonrpc's measured cluster is the SDK wire stack, and acp/ is an automation transport, not a human channel.
A standalone one-package tui/ group. Rejected: tui is the interaction plane's primary provider/consumer (peer edges to commands, user-interaction), and a top-level name spent on one plugin adds a group without adding information; it folds into interaction/.
Moving app-boot to apps/. Rejected: apps/ is the assembly tier over the package tier, and dsh-app-boot is a package-tier library — placing it in apps/ would invert the tiers and put a workspace library outside the packages/*/* build globs. It stays a package; boot/ is its role-complete home.
Moving tool-cordis into core/. Rejected: self-modification is its own product seam, expected to grow; the spine stays minimal. The group was first named self-evolve/; the name settled on self-modification/ as the plainer term.
Renaming context/ to request-context/. Rejected: within this tree the group is unambiguous in situ; the churn is unjustified.
Consequences
- The five still-current regrouped families hold the listed members; the groups
ui/,telemetry/,timeout/,cordis/,session-persistence/,session-projection/, andsession-title/no longer exist. The regrouping itself changed no npm names. The later SDK toolchain removal intentionally changed the package set and restoredsdk/as the precise home of the runtime SDK trio. Two FIXME markers pin the remaining deferred renames; a FIXME that later proves wrong must be removed explicitly with rationale, never silently dropped. - What pins the result:
pnpm run typecheck, the unit suites of every moved group,verify-package-paths,verify-md-links, and the corpus-wide translation pairing all pass on the moved tree; the group-scoped test globs invitest.snapshot.config.tswere rewritten with the moves so the suites collect the same test files as before (a fail-open glob would silently drop coverage). - Every open PR touching a moved file rebases across the move once; rename detection resolves most hunks mechanically.
- Single-package groups remain (
boot/,self-modification/, and existing ones such asacp/). Accepted deliberately: each is role-complete rather than a fragment of a family, and a truthful small group beats a nominal merge. - The
sdk/role folders map explicitly to their npm names intsconfig.base.json; theserver/mapping remains transitional untildsh-jsonrpcis renamed. - What this gave up: nothing functional — the change is navigational. Muscle memory and external links to old GitHub paths break, which is acceptable pre-release with no external consumers.