refactor(scope,agent-presets): per-preset standing mounts over a scope parent chain
A preset is now ONE composition per process, not one per session. The roster mounts it once under a synthetic standing scope; each agent joins by having its scope key parented to the mount's. Two mechanisms in dsh-scope carry the whole change: registration views walk the parent chain (global → preset → agent, nearest shadowing farthest — ScopedLayers.chainLayers), and scoped event dispatch admits a listener tagged with an ancestor of the carrier key, which is what lets a standing composition's plan/compaction/token listeners observe each agent composed under it while a sibling preset's stay deaf. The preset plugins already key their state by Session/Agent — they predate presets and were written for the shared world — so sharing one instance is a return to their design, not a rewrite. Preset ymls are unchanged: one mount per preset means one Entry per preset, whose entry-local realms keep two presets' services apart exactly as they kept two sessions' apart before. The standing scope hangs off the service's UNTRACED context (selfCtx): a method invoked through the traceable proxy sees this.ctx rebound to the caller and carrying its shadow, and a subtree minted from that resolves every service through the shadow's fiber instead of each entry's own inject store — preset rows then fail on the very services they declare. A standing mount survives its agents deliberately. The composition a running session joined must outlive the file changing or disappearing underneath it; reclamation happens at whole-tree teardown, and file edits reach only future generations (the authoring layer swaps the pointer, never disposes a joined generation).
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { scopeOf } from './index.ts'
|
||||
import { scopeChainOf, scopeOf } from './index.ts'
|
||||
import type { ScopeKey } from './index.ts'
|
||||
|
||||
/** One scope's aggregate contribution to a registry. */
|
||||
@@ -170,7 +170,10 @@ export class ScopedLayers<L extends ScopeLayer> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an existing exact-scope overlay.
|
||||
* Read an existing exact-scope overlay. Deliberately chain-blind: callers
|
||||
* addressing one scope's OWN contributions (its restrictions, its guards)
|
||||
* must not silently pick up an ancestor's — use {@link chainLayers} where
|
||||
* inheritance is the point.
|
||||
* @param scope - exact scope key; `undefined` denotes no overlay.
|
||||
* @returns the existing scoped layer, or `undefined` without creating one.
|
||||
*/
|
||||
@@ -180,8 +183,26 @@ export class ScopedLayers<L extends ScopeLayer> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize global named entries followed by exact-scope shadows.
|
||||
* @param scope - exact viewing scope, or `undefined` for the global view.
|
||||
* Existing overlays along the scope's parent chain ({@link scopeChainOf}),
|
||||
* farthest ancestor first and the exact scope last, so a caller layering
|
||||
* them in order gives the nearest scope the final word.
|
||||
* @param scope - viewing scope, or `undefined` for no overlays.
|
||||
* @returns the existing layers, nearest last; absent overlays are skipped.
|
||||
*/
|
||||
chainLayers(scope: ScopeKey | undefined): L[] {
|
||||
const chain = scopeChainOf(scope)
|
||||
const layers: L[] = []
|
||||
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
||||
const layer = this.scoped.get(chain[index]!)
|
||||
if (layer !== undefined) layers.push(layer)
|
||||
}
|
||||
return layers
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize global named entries followed by scope-chain shadows,
|
||||
* farthest ancestor first, so the nearest scope's entry wins a name.
|
||||
* @param scope - viewing scope, or `undefined` for the global view.
|
||||
* @param pick - select the named table from a layer.
|
||||
* @returns an insertion-ordered effective map.
|
||||
*/
|
||||
@@ -190,9 +211,9 @@ export class ScopedLayers<L extends ScopeLayer> {
|
||||
pick: (layer: L) => NamedEntries<V>,
|
||||
): Map<string, V> {
|
||||
const merged = new Map(pick(this.global).entries())
|
||||
const layer = this.peek(scope)
|
||||
if (layer === undefined) return merged
|
||||
for (const [name, value] of pick(layer).entries()) merged.set(name, value)
|
||||
for (const layer of this.chainLayers(scope)) {
|
||||
for (const [name, value] of pick(layer).entries()) merged.set(name, value)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user