refactor(scope): bind the parent link once and gate re-linking behind the binding

setScopeParent could re-link any key from anywhere, leaving the
blank-session-only recompose rule entirely to caller discipline. The
relation now binds once — a second bind throws — and re-linking exists
only on the ScopeParentBinding returned to the original binder, the
private-capability shape the package conventions prescribe for a
single-caller operation. The preset roster keeps each composed agent's
binding in a WeakMap keyed by the agent, making it the sole authority
that can move an agent between standing compositions; the blank-session
contract itself stays with the gateway, which alone can see what a
session logged.
This commit is contained in:
Yichen Jiang
2026-08-10 11:17:04 +08:00
parent 51ca900d4e
commit 1eb9acaba9
9 changed files with 83 additions and 39 deletions

View File

@@ -8,7 +8,7 @@
* projection units exist exactly once, keyed per session inside the plugins
* themselves (they predate presets and were written for a shared world). An
* agent joins by having its scope key parented to the mount's
* ({@link setScopeParent}), which makes the mount's registrations visible to
* ({@link bindScopeParent}), which makes the mount's registrations visible to
* that agent's views and the mount's listeners receive that agent's events —
* and a host reader with no agent at all (a cold transcript read) resolves
* the same standing registrations by preset id.
@@ -23,7 +23,7 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { createScope, scopeOf, setScopeParent, type Scope, type ScopeKey } from '@deepseek-ai/dsh-scope'
import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope'
import { discoverPresets } from './discovery.ts'
import { mountPreset } from './mount.ts'
import type { AgentPreset, Config } from './types.ts'
@@ -113,6 +113,14 @@ export class AgentPresets extends Service {
*/
private readonly standing = new Map<string, Promise<StandingMount>>()
/**
* Parent bindings of the agents this roster composed, keyed by the agent's
* scope key. The binding is dsh-scope's only re-link capability; holding it
* here makes this service the sole authority that can move an agent between
* standing compositions. WeakMap: entries die with their agents.
*/
private readonly bindings = new WeakMap<ScopeKey, ScopeParentBinding>()
/**
* Compose one agent from a preset: ensure the preset's standing mount, then
* parent the agent's scope key to it so the mount's registrations and
@@ -133,7 +141,11 @@ export class AgentPresets extends Service {
}
const preset = await this.resolve(id)
const standing = await this.ensureStanding(preset)
setScopeParent(agentKey, standing.key)
// The one bind of this agent's ancestry. The binding is the only re-link
// authority, held privately so nothing outside this roster can move a
// composed agent to another preset; a later recompose layer re-links
// through it under the caller-owned blank-session contract.
this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key))
return preset
}