Merge branch 'worktree-llm-dynamic-config' into worktree-llm-web-config

# Conflicts:
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/event-producer-consumer.md
#	examples/headless-agent/tests/headless.snapshot.ts
#	examples/headless-agent/tests/snapshots/missing-credential/stream-json.expected.jsonl
#	packages/llm/llm-deepseek/README.i18n.yaml
#	packages/llm/llm-deepseek/src/index.ts
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/src/index.ts
#	packages/llm/llm/README.i18n.yaml
#	packages/llm/llm/src/index.ts
This commit is contained in:
Yichen Jiang
2026-07-30 17:22:44 +08:00
60 changed files with 1320 additions and 320 deletions

View File

@@ -590,6 +590,20 @@ export abstract class Settings extends Service {
}
}
/**
* Value mirror of the `FiberState` members {@link isUnloading} compares
* against: a const enum has no runtime object to import, and the value is
* needed at runtime (same rationale as the CLI boot driver's mirror).
*/
const FIBER_DISPOSED = 4
const FIBER_UNLOADING = 5
/** Whether the consumer's own fiber is tearing down (not just losing the settings service). */
function isUnloading(ctx: Context): boolean {
const state: number = ctx.fiber.state
return state === FIBER_UNLOADING || state === FIBER_DISPOSED
}
/** Hooks a consumer hands to {@link installSettingsSection}. */
export interface SettingsSectionHooks<T> {
/**
@@ -630,6 +644,13 @@ export function installSettingsSection<T>(
const scope = sctx.settings.register(ns, schema, { base: entry })
hooks.setSource(() => scope.get())
sctx.effect(() => () => {
// This disposer runs for two different reasons. A settings provider
// detaching leaves the consumer running, so it must fall back to its
// composition entry and re-judge what it derived. The consumer's own
// unload runs it too — and there `onChange` would re-register routes
// and touch resources the teardown is releasing, so the fallback is
// pointless and the notification actively harmful.
if (isUnloading(ctx)) return
hooks.setSource(() => entry)
hooks.onChange()
})

View File

@@ -694,4 +694,34 @@ describe('installSettingsSection', () => {
})
expect(current()).toEqual({ theme: 'entry' })
})
it('stays silent when the consumer itself unloads', async () => {
const { ctx } = await boot({ doc: { 'helper-ns': { theme: 'user' } } })
const entry = { theme: 'entry' }
let current: () => { theme: string } = () => entry
const changes: string[] = []
const consumer = ctx.plugin({
inject: ['settings'],
apply: (child: Context) => {
installSettingsSection(child, settingsNamespace('helper-ns'), HelperSchema, entry, {
setSource: (source) => {
current = source
},
onChange: () => {
changes.push(current().theme)
},
})
},
})
await consumer
await vi.waitFor(() => {
expect(changes).toEqual(['user'])
})
// The consumer's own teardown must not re-derive anything: an onChange
// here would re-register routes and touch resources being released.
await consumer.dispose()
await new Promise(resolve => setTimeout(resolve, 20))
expect(changes).toEqual(['user'])
})
})