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:
Yichen Jiang
2026-08-06 11:05:01 +08:00
parent 18fe174897
commit 065257addb
11 changed files with 63 additions and 37 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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()