refactor(agent-core-v2): decouple kosong from config persistence#2068
Conversation
- keep the kosong provider/model registries in memory and sync them with config.toml through a new app/kosongConfig two-way bridge: hydrate on startup, push config section changes into the registries, and persist runtime mutations (discovery refresh, OAuth provisioning, default-model pointer changes) back to disk - declare the providers/models/thinking section constants, zod schemas, env bindings, and TOML transforms in a single app/kosongConfig configSection.ts; kosong keeps hand-written types only - pin every schema to its kosong type at compile time via AssertExact<Equal<...>> (_base/utils/typeEquality) - register a transitional auth>kosongConfig exception in the domain-layer checker for the OAuth provisioning flows
🦋 Changeset detectedLatest commit: b568629 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e90056a220
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // reference, conflicting credentials, ...) must not become the default. | ||
| const model = this.get(modelId); | ||
| await this.config.set(DEFAULT_MODEL_SECTION, modelId); | ||
| await this.models.setDefaultModel(modelId); |
There was a problem hiding this comment.
Preserve env-pinned defaults when setting the model
When KIMI_MODEL_NAME is set, app/kosongConfig/envOverlay.ts forces the effective defaultModel to __kimi_env_model__, and the discovery path explicitly writes default changes through config because only the effective config view knows about that pin. This direct registry update bypasses the overlay: the bridge later persists the raw value, but the effective defaultModel does not change, so no config event resets IModelService back to the env model. After calling setDefaultModel in that environment, catalog/auth APIs that read models.getDefaultModel() report the user-selected model while agent profile resolution still reads the env-pinned config default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 9a205f0. The write path stays registry-side (kosong cannot see the config service by design), and the bridge now re-asserts the pinned effective value into the registry after persisting, so registry reads can no longer diverge from the effective config view. Added a pinned-overlay test to the bridge suite.
A registry-originated default-model/provider write lands only in the config user layer when an effective overlay pins the section (KIMI_MODEL_NAME pins defaultModel to the reserved env model): the effective value does not move and no change event fires, so the registry diverged from the effective config view — catalog/auth reads reported the user pick while profile resolution kept the pinned model. After persisting, the bridge now re-asserts the effective value into the registry, restoring the pre-refactor behavior where every default-pointer write was arbitrated by the effective view.
…y mutations
ProviderService/ModelService mutations resolved as soon as the in-memory
registry updated, while the kosongConfig bridge persisted the change on an
unawaited private chain — callers (klient kosong.*, set_default route,
OAuth provision, discovery refresh) could observe success before the write
reached config.toml, and a restart right after could lose it.
- fire registry change events through AsyncEmitter and await delivery in
set/delete/replaceAll/setDefaultX, so a mutation resolves only after
listeners' waitUntil work completes; loadAll keeps synchronous timing
- the bridge hooks its persists into waitUntil, hoists the equality guards
into the listeners so config-originated echoes stay fully synchronous,
and serializes persists on the chain as before
- retry a failed persist with bounded backoff (3 attempts); failures are
logged, never rejected to callers, and the in-memory change stands
- default-pointer change events now carry an { id } payload (fireAsync
requires object events)
- add the klient kosong-config stress example covering read-after-write,
concurrent bursts, and restart durability
Related Issue
No tracking issue — this is a planned internal refactor of the experimental agent-core-v2 engine; the problem is explained below.
Problem
kosong, the engine's pure provider/model abstraction layer, was coupled to config persistence in both directions:
providers/models/thinkingconfig-section zod schemas, TOML transforms, and env bindings, and hosted discovery / env-overlay / OAuth-token concerns, so a persistence-free layer knew about storage.config.toml.What changed
1. Two-way persistence bridge (
app/kosongConfig)Problem: kosong's in-memory provider/model registries and
config.tomlcould drift apart.What was done:
config.replace. Both directions are idempotent by deep comparison, so the sync loop terminates without reentrancy flags.KIMI_MODEL_*env overlay, and the OAuth token adapter into the same wrapper domain, so kosong never reaches up to persistence concerns.2. Section declarations consolidated; kosong is types-only
Problem: kosong still owned the config-section zod schemas and section constants — knowledge of the persisted shape.
What was done:
app/kosongConfig/configSection.ts.ProviderConfig,ModelRecord,ThinkingConfig, …); each schema is re-derived from its type and pinned at compile time viaAssertExact<Equal<z.infer<typeof Schema>, Type>>(_base/utils/typeEquality), so type/schema drift in either direction fails typecheck.profile,llmRequester,auth, discovery) read/write sections throughIConfigServicewith the constants from their new home;authkeeps a local OAuthRef schema (an L2 domain must not import the L3 wrapper) and a transitionalauth>kosongConfigexception is registered in the domain-layer checker.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.