fix(agent-presets): bound the mount registry on a host that never reads it
Records are pruned by observation rather than by a disposal hook, for the reason the module already states: three different owners can tear a subtree down, and a cleared `uid` is what they share. That leaves the pruning to whoever reads — and the only production reader is the invariant companion, whose package is a development composition a shipped host never loads. So a live host pruned nothing: every session ever composed left a record retaining its whole disposed subtree, since the fiber holds its config and that config is the key its EntryTree is stored under. Prune on the mount path too. Every session takes it, which bounds the set at one generation of dead records instead of one per session.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/README.md
|
||||
README.md: 365659617c97c44dd0f30fbcd3347b6438024eb3
|
||||
README.zh.md: 9edabd67ea728e77e2863a32c250675a5b9359f8
|
||||
README.md: b736aa5dc9d0e9313d652d40c3f4834456dccbb4
|
||||
README.zh.md: 53081b5e8c2d465dc866644eae78bebf0c4fc3a1
|
||||
|
||||
@@ -86,15 +86,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
methods: [
|
||||
{
|
||||
signature: 'async list(): Promise<AgentPreset[]>',
|
||||
jsDoc: '/**\n * Every profile the configured roots currently supply.\n * @returns the profiles, first-root-wins per id.\n */',
|
||||
jsDoc: '/**\n * Every preset the configured roots currently supply.\n * @returns the presets, first-root-wins per id.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resolve(id?: string): Promise<AgentPreset>',
|
||||
jsDoc: '/**\n * Resolve one profile by id.\n * @param id - the profile id, or `undefined` for {@link defaultId}.\n * @returns the resolved profile.\n * @throws when no configured root supplies that id.\n */',
|
||||
jsDoc: '/**\n * Resolve one preset by id.\n * @param id - the preset id, or `undefined` for {@link defaultId}.\n * @returns the resolved preset.\n * @throws when no configured root supplies that id.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async mount(agentCtx: Context, id?: string): Promise<AgentPreset>',
|
||||
jsDoc: '/**\n * Compose one agent from a profile, installing it under that agent alone.\n *\n * Call from the agent factory\'s `setup(agentCtx)`; a rejection there rolls\n * the agent creation back, so a broken profile never yields a half-composed\n * session.\n * @param agentCtx - the agent\'s scope context.\n * @param id - the profile id, or `undefined` for {@link defaultId}.\n * @returns the profile that was mounted, for the caller to record.\n * @throws when the profile is unknown or its composition is unusable.\n */',
|
||||
jsDoc: '/**\n * Compose one agent from a preset, installing it under that agent alone.\n *\n * Call from the agent factory\'s `setup(agentCtx)`; a rejection there rolls\n * the agent creation back, so a broken preset never yields a half-composed\n * session.\n * @param agentCtx - the agent\'s scope context.\n * @param id - the preset id, or `undefined` for {@link defaultId}.\n * @returns the preset that was mounted, for the caller to record.\n * @throws when the preset is unknown or its composition is unusable.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/preset/README.md
|
||||
README.md: e7940642166f81e370e3a328f3097d15fd367151
|
||||
README.zh.md: 0767ca5074071e9ef2fa38769d27d8ef2344188e
|
||||
README.md: 7baac391c224f717b60edeb0de828cb004ab460a
|
||||
README.zh.md: 4d8c350b2831ae9c506ad7756ae157bf313a1c3e
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md
|
||||
README.md: 6068a68d3c81081074165077a8afa6b42af48d1f
|
||||
README.zh.md: 9f951f566a51a7b7acb666c7d9ea80061aa45d73
|
||||
README.md: 5d66c23f24717d1c30a9e729c7b0528715692d41
|
||||
README.zh.md: 9cdcd8b11a8a37d6d789c393e9a1500e4fbfd503
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -60,16 +60,34 @@ export interface PresetMount {
|
||||
const mounts = new Set<PresetMount>()
|
||||
|
||||
/**
|
||||
* Every preset composition still installed, pruning fibers disposed since the
|
||||
* last read. Records are dropped lazily rather than through a disposal hook
|
||||
* Drop every record whose subtree is gone.
|
||||
*
|
||||
* Records are pruned by observation rather than through a disposal hook
|
||||
* because a subtree can be torn down by its owning agent, by a failed mount, or
|
||||
* by the whole tree unloading, and a cleared `uid` is what all three share.
|
||||
* @returns the live mounts.
|
||||
*
|
||||
* Pruning therefore has to happen on a path this module owns. Reading is one
|
||||
* such path, but not a reliable one: the only production reader is the
|
||||
* invariant companion's service listener, and `dsh-invariants` is a
|
||||
* development composition — a shipped host never loads it. Mounting is the
|
||||
* other, and it is the one every session takes, which bounds the set at one
|
||||
* generation of dead records rather than one per session ever composed. Each
|
||||
* record would otherwise retain its whole disposed subtree: the fiber holds
|
||||
* its config, and that config is the key its `EntryTree` is stored under.
|
||||
*/
|
||||
export function livePresetMounts(): PresetMount[] {
|
||||
function pruneDisposedMounts(): void {
|
||||
for (const mount of mounts) {
|
||||
if (mount.fiber.uid === null) mounts.delete(mount)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every preset composition still installed, pruning fibers disposed since the
|
||||
* last read.
|
||||
* @returns the live mounts.
|
||||
*/
|
||||
export function livePresetMounts(): PresetMount[] {
|
||||
pruneDisposedMounts()
|
||||
return [...mounts]
|
||||
}
|
||||
|
||||
@@ -167,6 +185,9 @@ export async function mountPreset(agentCtx: Context, preset: AgentPreset): Promi
|
||||
)
|
||||
}
|
||||
const config: Include.Config = { path: pathToFileURL(preset.path).href }
|
||||
// Before the record this mount is about to add: every session takes this
|
||||
// path, so it is what keeps the set bounded on a host that never reads it.
|
||||
pruneDisposedMounts()
|
||||
const handle = agentCtx.plugin(PresetTree, config)
|
||||
try {
|
||||
await handle.await()
|
||||
|
||||
Reference in New Issue
Block a user