* feat(i18n): key translation pairing records by heading section Replace the whole-file blob hashes in *.i18n.yaml with one entry per heading section that still holds language-specific content, keyed by the English heading-slug path. Blocks identical across the pair (generated table rows, code blocks, shared regions) stay out of the hash, so edits to different sections and identical regenerations merge with Git's default text merge, including on GitHub. Remove the dsh-translation-pairing merge driver, its resolver command and installer wiring; gen-translation-brief recovers last-confirmed text from commit history. Migrate all active records, archive the superseded Agent Note, and record the replayed conflict reduction in the new one. * test(i18n): refresh translation prompt snapshot for the terminology note * feat(i18n): exclude only code blocks and generated regions from pairing records Replace byte-equality masking with an explicit rule: a record hash skips fenced code blocks and BEGIN/END GENERATED regions, the two kinds of content the pairing gate already requires to be identical on both sides. Move generated data into regions written into both pages: gen-config-catalog renders both languages and the record with per-package regions labelled by code identifiers; gen-module-graph keeps its table in a region; gen-doc-graphs splices the event matrix regions into the Chinese page. Add a generic region splice beside the region grammar.
description, kind
| description | kind |
|---|---|
| The scoped-registration library for plugin authors and maintainers building registries or event surfaces that isolate contributions per agent or per group. | package-library |
@deepseek-ai/dsh-scope
English | 中文
Summary
dsh-scope lets plugin authors give each agent or group an isolated contribution set with a shared lifetime. Child scopes inherit ancestor contributions, with the nearest definition taking precedence, while ancestor scopes can observe descendant activity; neither relationship works in reverse. Disposing a scope removes everything owned by it. Use this dependency-free library when per-agent or per-group isolation must work without depending on the agent loop or presets.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Known Limitations and Deferred Work
- Dev Note
Use this package
Plugin authors use dsh-scope to give one agent (or one group) its own registration world. The registries in the core group build on it — a tool registered through agent.ctx is visible only to that agent — and the same primitive serves any custom registry or scope-filtered event.
Mint a scope
createScope(ctx, key) creates a scope under ctx's fiber: its ctx carries the scope tag, and everything registered through it is both scope-visible and scope-lifetime. dispose() unwinds every registration through the scope; rawDispose is the exact Cordis disposer for nesting the teardown in an ordered composite effect.
const scope = createScope(ctx, agent)
scope.ctx.on('agent/status', ({ agent, status }) => track(agent, status))
// later:
await scope.dispose() // unwinds every registration made through scope.ctx
Route scoped events
scopeTarget(base, key) builds the opaque carrier a scope-filtered event dispatches with. Untagged listeners stay global; a listener tagged with key receives events for that key and its descendants. The carrier carries routing state only — the real subject travels in the event arguments.
Build a scoped registry layer
Registry authors use ScopedLayers, NamedEntries, and AnonymousEntries to hold one eager global layer plus lazily created exact-scope layers: reads never create layers, merge() materializes insertion-ordered named shadows along the scope chain, and effect() derives visibility and ownership from the same context. A scoped layer is reclaimed only when its whole aggregate is empty.
Understand the implementation
Implementation internals — click to expand
This section explains how the package realizes the behavior above; the observable contract is covered in Use this package.
Design concept
The registration context determines both visibility and ownership: a registration made through a scoped context is visible in that scope and disposed with it, preventing a contribution from being visible in one scope but torn down with another. The primitive routes trusted same-process plugins; it is not a sandbox or an authority boundary. Handing out a scoped context also hands out the minting plugin's service-resolution API (resolution walks the minting fiber's dependency chain), so a scope is minted from the plugin whose dependencies the scoped registrations need.
Source map
| File | Role |
|---|---|
src/index.ts |
createScope, scopeOf, scopeTarget, bindScopeParent/scopeParentOf/scopeChainOf, carrier marks |
src/store.ts |
ScopedLayers, NamedEntries, AnonymousEntries, ScopeLayer |
src/invariant.ts |
Invariant companion over the generated scoped-event map |
src/scoped-events.generated.ts |
Generated resolver map of declared scoped events |
The parent chain
One relation powers both directions: registration views inherit DOWN the chain (a child scope sees its ancestors' layers), while event admission extends UP it (a listener tagged with an ancestor receives events dispatched to a descendant key). Binding is once — a key that already has a parent throws, and only the returned binding may re-link it — and every link rejects a cycle. scopeChainOf returns [key, parent, …] nearest-first.
Event filtering
scopeTarget composes the base's existing Context.filter with the scope predicate: an untagged listener is admitted; a tagged listener is admitted iff its tag is the dispatch key or an ancestor of it; key === undefined admits untagged listeners only. { global: true } listeners bypass filtering. The Scoped<T> brand demands the carrier as the this type of a scope-filtered event, so dispatching with a bare subject is a compile error.
Further Exploration
The package-level contract is enough for most consumers; read these when you need the surrounding domain and the design rationale.
- Scoped registration subsystem — the identity, carrier, and layer types.
- Agent-scope contexts Agent Note — the security non-goals and context design.
- Agent-scope runtime design Agent Note — how the loop builds per-agent scopes.
- Core group map — how the core packages compose.
Known Limitations and Deferred Work
These limits define when the primitive needs special care. They are current package constraints, not a task backlog.
- Only scope-aware APIs isolate state — registries must file by
scopeOf()and events must dispatch throughscopeTarget(); an arbitrary Cordis service remains context-global merely because it is called through a scoped context. - A context carries one nearest scope key — the hierarchy lives in the key-level parent relation, not in context tags; nested scope contexts still shadow to a single tag, and multi-membership policy sets remain unsupported.
- Service reachability comes from the scope minter — handing out
Scope.ctxalso hands out the minting plugin's injected services, so a broader minter cannot later be narrowed by the holder.
Dev Note
Working context for maintainers — click to expand
None.