Compare commits

..

No commits in common. "main" and "@moonshot-ai/kimi-code@0.40.1" have entirely different histories.

2882 changed files with 270218 additions and 173952 deletions

View file

@ -0,0 +1,70 @@
---
name: agent-core-dev
description: Use when developing in packages/agent-core-v2 (the DI × Scope agent engine) — adding or modifying a domain Service, choosing a LifecycleScope, wiring DI dependencies, splitting a domain across scopes, owning or migrating a config section, gating behavior behind an experimental flag, raising coded errors, working on the permission system, writing DI/Scope tests, porting business logic from agent-core (v1) to v2, triaging a main-branch commit against v2, or exposing a v2 domain over server-v2 while keeping the /api/v1 wire contract compatible with released clients. Self-contained guide organized by development stage (orient → design → implement → test → verify) plus align workflows for v1→v2 migration, main-branch commit triage, and server-v2 wire exposure; each file carries the rules, examples, and red lines for its step.
---
# agent-core-dev
> Develop `packages/agent-core-v2` by lifecycle stage. This skill is **self-contained**: every rule, recipe, and red line lives in the stage files below — it does not delegate to `packages/agent-core-v2/docs/`.
`agent-core-v2` is the new agent engine built on the **DI × Scope** architecture (a port of `packages/agent-core`). Everything resolves through the container: a service declares an **identity**, its **dependencies**, and a **lifetime**; the container decides construction, singleton-per-scope, ordering, and disposal. The stage files restate the rules in imperative form so you can work without reading the source docs.
## Lifecycle at a glance
```text
Orient → Design → Implement → Test → Verify
│ │ │ │ │
│ │ │ │ └─ lint:imports · typecheck · test · dep graph · red lines
│ │ │ └─ test.md
│ │ └─ implement.md (+ errors.md · flags.md · permission.md)
│ └─ design.md
└─ orient.md
```
Stages are ordered but not strictly linear: a test failure (stage 4) that reveals a wrong scope sends you back to design (stage 2); a `CyclicDependencyError` sends you to `design.md` §dependency-direction and `implement.md` §cycles.
## Workflows
End-to-end procedures that span the stages. Reach for these before reading the stage files individually.
- [Align (port `agent-core` → `agent-core-v2`)](align.md): split a v1 class into semantic units, fix each unit's domain / scope / Service / dependencies, then migrate the logic and tests. Use when the task is "move feature X from v1 to v2" or "port `IXxxService` to v2".
- [Commit align (triage a `main` commit against v2)](commit-align.md): given one `main` commit hash + a short note, find the v1 logic it changed, check whether v2 already has the corresponding implementation, bucket it (aligned / partial / missing / not-applicable), and recommend a minimal fix. Use in the `kimi-code-v2`-catching-up-to-`main` phase, for one commit at a time; escalate to [align.md](align.md) if the gap is a whole domain.
- [Server align (expose `agent-core-v2` over `server-v2`)](server-align.md): wire a v2 domain into `packages/kap-server` over `/api/v2` (native) and `/api/v1` (v1-compatible mirror), keep the wire schema byte-compatible with the established v1 contract by sharing the `@moonshot-ai/protocol` schema, and isolate v1-only behavior in a `<domain>Legacy` edge adapter instead of distorting the native v2 Service. Use when the task is "expose the new v2 Service on the server", "add a route to the `/api/v1` surface", or "keep server-v2 wire-compatible with released v1 clients".
## Stages
- [Stage 1 — Orient](orient.md): the DI black box (identity / dependencies / lifetime), the four `LifecycleScope` tiers and visibility, and the no-comment convention. Read before touching business code.
- [Stage 2 — Design a service](design.md): pick a scope, split a domain across scopes, choose a calling style (direct call vs event vs hook), and direct dependencies. Decide *where things live and who knows whom* before coding.
- Topic: [Domain boundaries vs Scope](domain-boundaries.md) — keep `session` / `agent` / `turn` from becoming god objects; data-ownership test and their split conclusions.
- Topic: [Persistence layering](persistence.md) — the three-layer `Store → Storage → backend` model, naming Stores by access pattern, and which layer business code should depend on.
- Topic: [Edge exposure — `resource:action` + WS events](edge-exposure.md) — which Services are exposed over `/api/v2` (per-scope action map) and which events stream over WS; what to wrap in a facade.
- [Stage 3 — Implement](implement.md): the standard Service recipe and the DI building blocks — interface + identity, constructor injection, scoped registration, `Disposable`, eager vs delayed, `invokeFunction`, `createInstance`, child scopes, and the cycle-refactor playbook.
- Topic: [Service authoring](service-authoring.md) — file layout, naming, contract vs impl contents, interface style, constructor/field conventions, events, multi-Service domains, comment rules.
- Topic: [Config](config.md) — the section-registry model, App vs Session split, owning a config section, the TOML format, and the env overlay.
- Topic: [Errors](errors.md) — co-located `XxxError`, the central code registry, wire serialization, boundary translation.
- Topic: [Flags](flags.md) — `registerFlagDefinition`, `IFlagService.enabled(id)`, the `[experimental]` config section, resolution precedence.
- Topic: [Permission](permission.md) — risk-only chain-of-responsibility kernel, harness constraints and product reviews as domain `onBeforeExecuteTool` veto listeners (`veto` / `allow` / `pass` / cold `waitUntil` factories), shared `toolApproval` round-trip, policy registry + composer, `modes`/`agentTypes` metadata, `resolveExecution`/`accesses`.
- Topic: [Telemetry](telemetry.md) — emitting events via `ITelemetryService`, context propagation, and appender destinations (`ConsoleAppender` / `CloudAppender`).
- [Stage 4 — Test](test.md): resolve the system under test by interface, pick `TestInstantiationService` vs `createScopedTestHost`, shared stubs, service groups, teardown.
- [Stage 5 — Verify & submit](verify.md): `lint:imports`, `typecheck`, `test`, and the pre-submit checklist.
## How to use this skill
Jump to the stage you are in and read that one file; each is self-contained and ends with its own red lines. Skim the global red lines below before submitting — they catch most mistakes across every stage. The repo's source of truth remains the code in `packages/agent-core-v2/src/`; this skill codifies the same rules so you do not have to re-derive them.
## Global red lines
Invariants that hold across every stage. Each is expanded in the stage file noted.
1. No `new` on a class whose constructor carries `@IService` deps — inject with `@IX` or `accessor.get(IX)`. (implement.md)
2. `@IX` decorates constructor parameters only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services). (service-authoring.md)
3. Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. (implement.md)
4. Parent scope never depends on child scope — short-lived may inject long-lived, never the reverse. (orient.md)
5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); activation timing does not break dependency cycles. (design.md, implement.md)
6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md)
7. Scope follows state identity — no `Map<sessionId, …>` at `App` to fake per-session state. (design.md)
8. Foundational layers never know upstream ones; business code never depends on the edge layer (`gateway`/`rpc`). (design.md)
9. Throw coded errors; register codes centrally; branch on `code` across the wire, never `instanceof`. (errors.md)
10. Gate unreleased behavior behind a flag contributed via `registerFlagDefinition` and resolved through `IFlagService.enabled(id)`; no ad-hoc env toggles. (flags.md)
11. Tests resolve the SUT by interface; shared stubs live under `test/`, never `src/`. (test.md)
12. Config is the preference registry: only preferences that are persistable, schema'd, and user/operator-facing go in `IConfigService`. Domain-specific config (including env-only operational toggles) goes through `registerConfigSection` + `envOverlay`. Facts → `IBootstrapService`, and host invocation arguments (CLI flags, host identity headers, prompt identity) → `BootstrapInput.args` / `IBootstrapService.args` — never new per-domain runtime-options services; domain runtime state (cron/flags/model) never goes onto `IBootstrapService`; session state → Session scope; constants → code. Business domains never call `IBootstrapService.getEnv()` directly. (config.md)

View file

@ -0,0 +1,235 @@
# Subskill — Align (port `agent-core` → `agent-core-v2`)
Port business logic from `packages/agent-core` (v1) into `packages/agent-core-v2` (v2) by **splitting semantics, then fixing the domain, scope, Service, and dependency relationships**, and finally migrating the logic and tests.
Use this when the task is "move feature X from v1 to v2", "port `IXxxService` to v2", or "align a v1 domain with the v2 architecture". It complements the stage files: orient / design / implement / test explain the *target* architecture; this file explains how to get there *from v1*.
## The one-paragraph mental model
v1 is a **VSCode-style singleton container**: services self-register with `registerSingleton`, resolve as singleton-per-container, and have no explicit lifetime tier — so a single `ISessionService` / `IToolService` tends to accumulate global, per-session, and per-agent state in one class. v2 is a **DI × Scope tree**: every service binds to one of `App` / `Session` / `Agent`, and a domain with state at several lifetimes is split into several Services. Porting is therefore **not** a file copy — it is "find each lifetime of state hiding in the v1 class, give each its own v2 Service at the right scope, then re-wire the dependencies".
## v1 → v2 at a glance
| Concern | v1 (`agent-core`) | v2 (`agent-core-v2`) |
|---|---|---|
| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, ScopeActivation.OnDemand, 'domain')` |
| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/lifecycle'` |
| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Workspace/Session/Agent) — see orient.md |
| Domain granularity | coarse (`session`, `tool`, `loop`) | fine, split by scope + responsibility |
| Test import | `from '@moonshot-ai/agent-core/di/test'` | `from '#/_base/di/test'` |
| Resolve SUT in tests | `ix.createInstance(Impl)` (common) | `ix.get(IX)` by interface — see test.md |
| Scope tests | none | `createScopedTestHost` — see test.md |
| Errors | `from '../../errors'` (central `KimiError`, `ErrorCodes`) | `from '#/_base/errors'` + domain co-located `XxxError` — see errors.md |
| Flags | `flags/` (process-global `FlagResolver`) | `flag/` (App-scope `IFlagService`) — see flags.md |
| Permission | `agent/permission/` (hardcoded chain) | `permission*` (registry + composer) — see permission.md |
## The align workflow
```text
Read v1 → Semantic split → Map domain → Assign scope → Shape Services
→ Direct dependencies → Port logic → Port tests → Verify
```
Each step below states the goal and the concrete action, then points to the stage file that goes deeper. Do them in order; a later step often sends you back to an earlier one (a scope that does not fit means the semantic split was wrong).
### 1. Read v1
**Goal:** build an accurate inventory of what the v1 code actually owns. Read the v1 *source*, not v1 docs.
Actions:
- Locate the v1 entry: contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), plus any helpers under the same folder.
- Inventory three things from the impl:
- **State** — every field / `Map` / cache the class holds. For each, note its *identity* (global? keyed by `sessionId`? by `agentId`?).
- **Behavior** — every public method; group them by which state they touch.
- **Dependencies** — every `@IFoo` constructor injection and every cross-domain relative import (`from '../<other>/...'`).
- Note the v1 registration line (`registerSingleton(...)`) and any `services.set(IX, ...)` overrides at bootstrap (these reveal runtime static args or prebuilt instances the port must preserve).
Do not start splitting yet — an accurate inventory prevents the common mistake of porting the class shape instead of the semantics.
### 2. Semantic split
**Goal:** break one v1 class into independent semantic units, each owning state at exactly one lifetime. This is the heart of the port.
Method — for each piece of state from the inventory, ask:
1. **What is it keyed by?** nothing → a global unit; `sessionId` → a per-session unit; `agentId` → a per-agent unit.
2. **When should it die?** with the process / the session / the agent. State that must outlive its neighbors is a different unit.
3. **Which methods touch only this state?** they travel with the unit.
Worked example — v1 `ISessionService` (one class, ~600 lines) holds:
- a global index of all sessions → **global** unit → v2 `sessionStore` (`ISessionStore`, App);
- this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session);
- this session's activity / status → **per-session** unit → v2 `sessionActivity`;
- this session's context projection → **per-session** unit → v2 `sessionContext`;
- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **per-workspace** unit → v2 `sessionLifecycle` (Workspace, one per live workspace handler).
A v1 class that maps cleanly to one v1 decorator often becomes **three to five** v2 Services. That is expected and correct — do not try to keep the v1 class shape.
Red lines:
- If two pieces of state have different identities, they belong in different units — do not keep them together "because v1 did".
- Do not split by method count or file aesthetics; split by state identity (design.md §3).
- If a unit has no mutable state (pure behavior), defer its scope decision to step 4 (it is pulled down by its shortest-lived dependency).
### 3. Map to v2 domain
**Goal:** assign each semantic unit to a v2 domain — an existing one if it fits, a new one only if none does.
Actions:
- Search v2 `src/` for an existing domain that owns the same responsibility. Prefer joining an existing domain over creating a new one.
- If creating a domain, name it after the responsibility (camelCase folder, e.g. `sessionActivity`), not after the v1 file.
- Keep a domain's public surface to one contract file (`<domain>.ts`) plus its impl(s).
Reference mapping (a **starting point**, not gospel — verify against the current v2 `src/`, which is the source of truth):
| v1 location | v2 domain(s) |
|---|---|
| `services/session/`, `session/` | `session`, `sessionStore`, `sessionMetaStore`, `sessionActivity`, `sessionContext`, `agentLifecycle` |
| `services/tool/`, `tools/`, `agent/tool/` | `toolRegistry`, `toolStore`, `toolExecutor`, `tooldedup`, `userTool` |
| `loop/`, `agent/` (turn loop) | `loop`, `llmRequester`, `llmRequestLog`, `turn` |
| `agent/context/`, `agent/compaction/` | `contextMemory`, `contextProjector`, `contextSize`, `fullCompaction`, `dynamicInjector` |
| `agent/permission/` | `permission`, `permissionMode`, `permissionPolicy`, `permissionRules`, `approval`, `externalHooks` |
| `agent/goal/`, `agent/plan/`, `agent/swarm/`, `agent/cron/`, `agent/background/` | `goal`, `plan`, `swarm`, `cron`, `background`, `subagentHost` |
| `services/config/`, `agent/config/` | `config` |
| `services/event/`, `base/common/event` | `event`, `eventBus` |
| `services/logger/`, `logging/` | `log` |
| `services/fileStore/` | `filestore`, `blobStore` |
| `services/fs/`, `services/workspace/` | `fs`, `workspace` |
| `services/auth/`, `services/oauth/` | `auth` |
| `services/environment/` | `environment` |
| `services/terminal/` | `terminal` |
| `services/question/`, `services/approval/` | `question`, `approval` |
| `services/prompt/`, `agent/injection/` | `prompt`, `dynamicInjector` |
| `services/mcp/`, `mcp/` | `mcp` |
| `plugin/`, `profile/`, `skill/` | `plugin`, `profile`, `skill` |
| `rpc/`, `services/coreProcess/` | `rpc`, `gateway` |
| `di/` | `_base/di` |
| `errors/`, `errors.ts` | `_base/errors` + co-located domain errors |
| `flags/` | `flag` |
| `telemetry.ts` | `telemetry` |
| `agent/records/` | (records split) — verify in v2 `src/` |
When the table says "verify", or when v1 and v2 have diverged, **read the v2 `src/` tree and decide from the code** — do not invent a mapping.
### 4. Assign scope
For each semantic unit, fix its `LifecycleScope` from the identity you found in step 2. Follow design.md §2 verbatim:
- global → `App`; per `sessionId` → `Session`; per `agentId` → `Agent`.
- Stateless unit → default to `App`, pulled down only by a shorter-lived dependency.
- Self-check: "when this scope is disposed, should this state disappear with it?"
This is the decision v1 never had to make — get it right before writing any v2 code, because the scope is fixed at registration and changing it later ripples through every consumer.
### 5. Shape Services
Decide the Service shape per unit, following design.md §3:
- A unit that owns **one instance's** state → a single per-instance Service (`ISessionXxx` / `IAgentXxx`).
- A unit that owns a **global view plus per-instance** state → split into an `App` registry/factory (`XxxStore` / `XxxRegistry` / `XxxCatalog`) **and** a per-instance Service. The `App` half creates or locates the per-instance half.
- Do not pre-split a unit that has state at only one lifetime.
Most consumers inject the per-instance Service; inject the `App` factory only for genuine cross-instance management.
### 6. Direct dependencies
Re-wire the dependencies you inventoried in step 1, now across the new v2 Services. Follow design.md §4–§5:
- **Calling style** — need a result / I orchestrate → direct call (`@IX` injection); stating a fact → event; ordered participation that may veto → hook.
- **Scope direction** — a Service may inject only its own scope or an ancestor. If an `App` Service needs something from a `Session` Service, the dependency is backwards: re-scope or invert into an event.
- **Domain direction** — foundational layers must not know upstream ones. A cycle means a v1 relative import is now pointing the wrong way; extract a third Service or invert the notification into an event.
- **Durable facts** — state changes that must be recorded / replayed / projected across agents go on the wire (`wireRecord`), not a direct call alone.
Run `lint:imports` (verify.md) as soon as the dependencies compile — it catches v1 imports and kosong boundary violations early.
### 7. Port the business logic
Move the behavior into the shaped v2 Services, applying the mechanical conversions below. Follow implement.md for the recipe.
**Registration:**
```ts
// v1
import { InstantiationType, registerSingleton } from '../../di';
registerSingleton(IXxxService, XxxService, InstantiationType.Delayed);
// v2
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
registerScopedService(LifecycleScope.Session, IXxxService, XxxService, ScopeActivation.OnDemand, 'xxx');
```
**Imports:**
```ts
// v1
import { createDecorator, Disposable, IInstantiationService } from '../../di';
import { KimiError, ErrorCodes } from '../../errors';
// v2
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { Disposable } from '#/_base/di/lifecycle';
import { IInstantiationService } from '#/_base/di/instantiation';
import { KimiError, type ErrorCode } from '#/_base/errors';
```
**Constructor injection** — unchanged in shape (`@IX` on constructor params, service params after static params). Verify each dependency is resolvable from the new scope (step 6).
**Errors** — move any shared error into a co-located `XxxError extends KimiError` with a registered `code` (errors.md). Do not keep throwing v1's central error codes from a v2 domain.
**Flags** — replace any `FlagResolver` / env check with `IFlagService.enabled(id)`; contribute new flags from the owning domain's `flag.ts` via `registerFlagDefinition` (flags.md).
**Events** — v1's `Emitter` / `Event` from `base/common/event` maps to v2's `event` / `eventBus` domains. Read existing v2 usage in neighboring domains and match it; do not import v1's `Emitter`.
**Runtime static args / prebuilt instances** — if v1 bootstrap did `services.set(IX, new SyncDescriptor(C, [bag]))` or set a prebuilt instance, preserve that behavior at the v2 composition root (the scope that owns the Service). Do not silently drop it.
Red lines:
- Do not copy a v1 file and "fix imports". Re-split first (steps 2–6); a straight copy carries v1's implicit-singleton assumptions into v2 and creates the `Map<sessionId, …>`-at-`App` anti-pattern.
- Do not leave v1 relative imports (`from '../x/...'`) in v2 — use the `#/...` alias.
- Do not preserve a v1 behavior just because it exists; if the split reveals it was a workaround for the missing scope tree, drop it.
### 8. Port the tests
Convert v1 tests to the v2 harness, following test.md:
```ts
// v1
import { TestInstantiationService } from '@moonshot-ai/agent-core/di/test';
const svc = ix.createInstance(XxxService, 'static-arg');
// v2
import { createServices } from '#/_base/di/test';
// in additionalServices:
reg.define(IXxxService, XxxService);
// in the test body:
const svc = ix.get(IXxxService);
```
- Resolve the SUT by interface (`ix.get(IX)`), never `new` a `@IService`-carrying impl, and prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Move shared stubs into `test/<domain>/stubs.ts`; import by relative path, never `#/...`.
- If the port introduced scope-layer behavior, add a `createScopedTestHost` test that asserts resolution from the correct scope (with `_clearScopedRegistryForTests()` + explicit re-registration in `beforeEach`).
- Keep v1's behavioral assertions where they still describe observable behavior; delete assertions that only checked v1's internal class shape.
## Migration checklist
Before submitting a port:
- [ ] Every piece of v1 state landed in a v2 Service whose scope matches its identity (no `Map<sessionId, …>` at `App`).
- [ ] Each v1 dependency now points in the right scope direction; `lint:imports` passes.
- [ ] Registrations use `registerScopedService` with an explicit scope and domain name; no `registerSingleton` remains.
- [ ] Imports use the `#/...` alias; no v1 relative (`../../di`, `../../errors`) imports remain.
- [ ] Errors are co-located coded errors; flags go through `IFlagService`.
- [ ] Tests resolve the SUT by interface; scope behavior is asserted via `createScopedTestHost`; teardown goes through one `DisposableStore`.
- [ ] v1 bootstrap overrides (`services.set(...)`) are preserved at the v2 composition root.
## Red lines (this subskill)
- Porting is semantic splitting, not file copying — never preserve a v1 class shape in v2.
- Decide scope from state identity before writing v2 code; the scope is fixed at registration.
- Verify the domain mapping against current v2 `src/`; the table here is a starting point, not authority.
- One Service owns state at exactly one lifetime; split global-view + per-instance into registry + per-instance.
- A dependency cycle introduced by the port means a v1 import is now backwards — refactor it; activation timing cannot break the cycle.

View file

@ -0,0 +1,155 @@
# Topic — Close vs Dispose
How to shut down a scoped service in `agent-core-v2`: when `dispose()` is enough, when to add an async `close()`, and where cancellation / abort belongs. Read this before putting business shutdown logic into a `Disposable`.
## The one-sentence rule
> **`close()` is async business shutdown; `dispose()` is synchronous resource cleanup.**
`close()` finishes a domain's work: stop in-flight operations, apply shutdown policy, flush persistence, release async resources. `dispose()` releases object resources: event subscriptions, timers, hook registrations, and child disposables.
## Why they must stay separate
`IDisposable.dispose()` is synchronous:
```ts
export interface IDisposable {
dispose(): void;
}
```
The container calls it during scope teardown. Disposal order is deterministic (orient.md): child scopes first, then reverse construction order within a scope. Nothing awaits a Promise returned from `dispose()`.
Business shutdown is usually async. It may need to:
- stop in-flight tasks and wait for settlement;
- decide policy (`kill` vs `keepAliveOnExit` vs `markLost`);
- flush write queues and persistence;
- emit final records / events / telemetry;
- close sockets, child processes, or external clients.
If that logic lives in `dispose()`, it becomes fire-and-forget: the scope keeps tearing down, dependencies may be disposed immediately afterward, and the async continuation can run against a half-dead object graph.
## What `close()` owns
Add `close(): Promise<void>` when a service owns async shutdown work:
```ts
export interface IXxxService {
readonly _serviceBrand: undefined;
close(reason?: string): Promise<void>;
}
```
A good `close()`:
- is idempotent — repeated calls return the same Promise or no-op;
- is called by lifecycle code **before** `scope.dispose()`;
- rejects new work after it starts;
- applies shutdown policy explicitly;
- awaits the work it starts;
- leaves `dispose()` with only synchronous cleanup.
Sketch:
```ts
class XxxService extends Disposable implements IXxxService {
declare readonly _serviceBrand: undefined;
private closed = false;
async close(reason = 'scope closed'): Promise<void> {
if (this.closed) return;
this.closed = true;
await this.stopInFlightWork(reason);
await this.flushPersistence();
}
override dispose(): void {
this.closed = true;
// synchronous cleanup only: clear timers, remove listeners, release handles.
super.dispose();
}
}
```
`flush()` is different from `close()`: `flush()` persists buffered state while the service stays open; `close()` is terminal.
## What `dispose()` owns
`dispose()` releases resources owned by the object instance:
```ts
class WSBroadcastService extends Disposable implements IWSBroadcastService {
declare readonly _serviceBrand: undefined;
constructor(@IEventService event: IEventService) {
super();
this._register(event.subscribe(() => { /* … */ }));
}
}
```
Use `dispose()` to:
- `_register(...)` event subscriptions and hook registrations;
- clear timers;
- remove signal listeners;
- dispose child `IDisposable`s;
- detach from synchronous handles.
`dispose()` must be idempotent and should avoid throwing. If `close()` was already called, `dispose()` should be a no-op for business work and only clean resources.
## Where abort / cancellation belongs
Cancellation is not the same thing as graceful shutdown.
For an operation-scoped object, a cancellation trigger can be disposed:
```ts
const tokenSource = new CancellationTokenSource();
store.add(toDisposable(() => tokenSource.cancel()));
```
This is fine when the contract is **fire-and-forget cancel**: the operation observes the token and settles asynchronously; disposal does not wait for completion.
For a manager/service that owns many tasks and their state, do not use `dispose()` as the graceful abort path. Expose `stop()` / `stopAll()` / `close()` and let lifecycle code await the one it needs.
Background-specific rule: a `background`-style service may use `AbortController` internally to propagate cancellation to process / agent / question tasks, but manager shutdown belongs in `close()` or explicit `stopAll()`. `dispose()` may best-effort abort controllers only as a safety net; it must not be the mechanism that decides terminal status, persistence, or notifications.
## Decision tree
```text
What does the service own?
│
├─ only event subscriptions / timers / disposable handles?
│ └─ extend Disposable; no close() needed.
│
├─ async work, in-flight tasks, persistence buffers, sockets, child processes?
│ └─ add close(): Promise<void>; call it before scope.dispose().
│
├─ a single operation that callers may cancel?
│ └─ expose an AbortSignal / CancellationToken or a fire-and-forget cancel handle.
│
└─ both async shutdown and disposable resources?
└─ close() for business shutdown; dispose() for resource cleanup.
```
## VSCode parallel
VSCode uses the same split:
- `src/vs/base/common/lifecycle.ts` — `IDisposable.dispose(): void` for synchronous cleanup.
- `src/vs/base/parts/storage/common/storage.ts` — `close(): Promise<void>` flushes and closes the database.
- `src/vs/base/common/cancellation.ts` — `CancellationTokenSource.dispose(true)` / `cancelOnDispose()` cancels operation-scoped work without awaiting it.
The lesson is not "never cancel in dispose". It is: **disposal may trigger cancellation for a scoped operation, but service shutdown policy stays in an explicit async close path.**
## Red lines (this topic)
- Do not put business shutdown in `dispose()` — `dispose()` is synchronous and is not awaited.
- Do not `await` inside `dispose()`.
- Do not rely on `dispose()` to flush persistence, emit final events, wait for tasks, or send notifications.
- Add `close(): Promise<void>` for async shutdown and call it before `scope.dispose()`.
- Keep `close()` and `dispose()` idempotent; `dispose()` after `close()` must be safe.
- Use disposal as a cancellation trigger only for operation-scoped work, not as a manager/service shutdown policy.

View file

@ -0,0 +1,78 @@
# Subskill — Commit align (triage a `main` commit against v2)
Context: you are on the `kimi-code-v2` branch, in the phase of catching it up to **new commits that landed on `main`**. Those commits change `packages/agent-core` (v1); the job is to decide, for one commit at a time, whether v2 (`packages/agent-core-v2`) already has the corresponding logic — and if not, what the minimal fix is.
Use this when the user hands you **one commit hash plus a short description** ("look at `<commit>` — it fixed the steering race"). It is the small, per-commit sibling of [align.md](align.md): `align.md` ports a whole v1 domain into v2; this file triages a single `main` commit and says *port / adapt / skip*. If the triage reveals a whole missing domain, stop and switch to [align.md](align.md).
## The one-paragraph mental model
A `main` commit edits v1's singleton-container code. The same behavior in v2 lives behind a scoped Service, so a commit lands in one of four buckets: **already-aligned** (v2 has it, possibly by construction), **partial** (v2 has a nearby version whose semantics drift), **missing** (v2 has nothing), or **not-applicable** (the v2 architecture removed the very problem the commit fixes). Your output is a bucket assignment plus evidence, then a fix sized to that bucket — never a blind port of the diff.
## The workflow
```text
Read the commit + the user's note → Locate the v1 logic → Map to a v2 domain
→ Check v2 for a corresponding implementation → Bucket it → Recommend a fix → Verify
```
### 1. Read the commit and the note
**Goal:** know exactly what changed in v1 and *why*. The user's one-liner gives the intent; the diff gives the facts.
Actions:
- Inspect the change scoped to v1: `git show <commit> -- packages/agent-core` (and `--stat` first to see the blast radius).
- From the diff, list: touched files, changed functions/methods, and the observable behavior delta (before → after).
- Reconcile with the user's note: is this a bugfix, a semantic correction, new behavior, or a refactor? The *why* decides whether v2 even needs the change.
Do not skim the user's sentence and guess — the diff is the spec for what "aligned" means here.
### 2. Locate the v1 logic
Pin the change to a v1 place: the contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), or the helper/handler the commit touched. Note which state it reads/writes and which other v1 services it calls — this is the same inventory as [align.md](align.md) §1, scoped to the commit's footprint.
### 3. Map to a v2 domain
Use the v1 → v2 domain table in [align.md](align.md) §3 as a starting point, then **verify against the current `packages/agent-core-v2/src/` tree** — it is the source of truth. Identify the candidate v2 Service(s) that would own this behavior, and their `LifecycleScope`.
### 4. Check v2 and assign a bucket
Search the candidate domain in v2 (Grep the method name, the state field, the error code). For each piece of the commit's behavior delta, decide:
- **Already-aligned** — v2 produces the same observable result (sometimes for free, because the v2 design never had the bug). Cite the v2 file:line.
- **Partial** — v2 has a near miss: same method, different guard/ordering/error; or the state lives at a different scope. Name the exact drift.
- **Missing** — no v2 Service owns this behavior. Confirm it is a single-Service gap, not a whole-domain gap (latter → [align.md](align.md)).
- **Not-applicable** — the v2 architecture removed the condition the commit fixes (e.g. the scope tree already serializes what v1 patched with a lock). Explain why, so a reviewer trusts the skip.
Every claim needs a citation (`path:line`) on both sides; "I couldn't find it" is a finding only after you name where you looked.
### 5. Recommend a fix (sized to the bucket)
- **Already-aligned** — say so and stop; reference the v2 location. No code change.
- **Partial** — propose the smallest edit that closes the drift: which Service, which method, which guard. Stay inside v2 rules — scope/domain direction, no `Map<sessionId, …>` at `App` (see [align.md](align.md) §6–§7 red lines).
- **Missing** — sketch the port at commit granularity: target domain + scope, the Service/method to add or extend, the dependency direction, and which [align.md](align.md) §7 conversions apply (registration, `#/…` imports, co-located coded error, `IFlagService` for any gate). If it needs a new scope or a wire change, flag it.
- **Not-applicable** — recommend no v2 change, but call out any test worth adding so the gap stays closed.
Keep the recommendation to the commit's footprint. If it keeps growing, that is the signal to hand off to [align.md](align.md) for a full domain port.
### 6. Verify
Point at the checks that cover the fix, per [verify.md](verify.md): `lint:imports`, `typecheck`, and the relevant `test`. Note the expected outcome rather than asserting you ran it if you did not.
## Output shape
When triaging, answer in this order so the user can act on it directly:
1. **Commit + intent** — one line restating what the commit changed and why (from the note + diff).
2. **v1 location** — file(s) and the behavior delta.
3. **v2 status** — one of the four buckets, with `path:line` evidence on both sides.
4. **Recommendation** — the concrete fix (or the justified skip), scoped to the commit; name the target Service / scope / dependency direction.
5. **Verify** — which checks should pass, and whether to escalate to [align.md](align.md).
## Red lines (this subskill)
- Read the diff and the note before judging v2; never infer "aligned" from the description alone.
- Do not copy a v1 diff into v2. Decide the bucket first; a bugfix commit often maps to **not-applicable** because the v2 design already removed the defect.
- Cite `path:line` on both sides. A recommendation without evidence is a guess.
- Stay in the commit's footprint. Growing scope means "switch to [align.md](align.md)", not "keep porting here".
- Do not break v2 invariants to chase v1 parity — scope direction, domain direction, and no `Map<sessionId, …>` at `App` still hold ([align.md](align.md) red lines).

View file

@ -0,0 +1,312 @@
# Topic — Config
How the `config` domain works and how a domain owns its configuration section. Covers the section-registry model, the App vs Session split, the TOML on-disk format, and the recipe for adding or migrating a config section.
The `config` domain is a thin registry + loader: it does **not** know the shape of any individual section. Each domain owns the schema (and, where needed, the TOML transform) for the config it consumes, contributes the section (statically at module load via `registerConfigSection`, or at runtime as a `ConfigSectionContribution` collection record), and reads it through `IConfigService`. There is no whole-config object passed around.
## What belongs in Config
`IConfigService` is the **preference registry**: it holds values a user or
operator *chooses*, each with a schema and a default, that *can* be persisted to
`config.toml`. It is not a grab-bag for every value a domain needs. Before
registering a section, classify the value along three axes — **decision-maker**,
**preference vs fact**, **mutability / persistence**:
| Type | Decision-maker | Preference/Fact | Persisted? | Examples | Home |
|---|---|---|---|---|---|
| User preference | user | preference | ✅ config.toml | model, theme, log level | **Config** |
| Operational override | operator/deployer | preference | ❌ env / flag | `KIMI_MODEL_*`, `KIMI_LOG_*` | **Config** (env overlay) |
| Per-run intent | invoker | preference | ❌ ephemeral | CLI `--model`, `--config` | **Config** (Memory layer) |
| Host fact | host | fact | ❌ | platform, CI, proxy, home dir | **Bootstrap** |
| Derived convention | code | fact (derived) | ❌ | `configPath`, `logsDir` | **Bootstrap / code** |
| Session runtime state | session/agent | state | ✅ session meta | active model, plan mode | **Session scope** |
| Tuning constant | developer | preference | ❌ compile-time | retry backoffs, buffer sizes | **code** |
A value belongs in Config **iff** it satisfies all of:
1. **Preference** — a choice among valid values, not an observed fact.
2. **Persistable** — it *can* be written to `config.toml`, even when a given
value arrives via env or CLI.
3. **Schema + default** — registerable as a section with validation.
4. **User- or operator-facing** — meaningful to set as a preference.
If it fails any rule, it is not Config:
- **Fact** (CI, platform, proxy, `HOME`) → a structured fact on
`IBootstrapService` (the startup snapshot), not Config.
- **Derived convention** (`configPath`, `logsDir`) → `IBootstrapService` / code.
- **Session runtime state** (active model, plan mode) → a Session-scoped
service in the owning domain (e.g. `IProfileService`), not `config`.
- **Tuning constant** (retry config, buffer sizes) → domain code; promote to
Config only when it becomes user-tunable.
**`IBootstrapService` is domain-agnostic.** It holds only generic facts shared by
all domains — the env bag, resolved paths, and host facts (`platform`, `arch`,
`cwd`, `osHomeDir`, `isCI`, …) — plus the host's process-level invocation
arguments in `args` (explicit `agentFiles` / `skillDirs`, `requestHeaders`,
prompt identity). `args` mirrors VS Code's `NativeParsedArgs` on the
environment service: the host states them once via `BootstrapInput.args` at
the composition root, and downstream services read them from
`IBootstrapService.args` instead of through per-domain runtime-options
services (do not add new `IXxxRuntimeOptions` services or seed functions for
host parameters). What must **never** land on `IBootstrapService` is state
tied to a specific upper domain (no `cron`, no `flags`, no feature-specific
fields): that couples the foundational layer to an upstream one.
Any value that belongs to a specific domain — including env-only operational
toggles (`KIMI_CRON_*`, `KIMI_CODE_EXPERIMENTAL_*`), model parameters, or feature
flags — goes through **Config registration**: the owning domain registers a
section with a declarative `envBindings` map (and a `stripEnv` when the value must
not be persisted) and reads it via `config.get(...)`. Each config value declares
an optional env binding (`{ field: 'ENV_VAR' }`, with optional `parse`/`default`);
IConfig resolves each field by `env > config.toml > default` automatically. This
keeps every domain's config in one registry and keeps Bootstrap free of upstream
knowledge.
Operational env overrides and per-run intent live *inside* Config as layers over
the same persistable key: `model` can be set in `config.toml`, via `KIMI_MODEL_*`,
or via CLI `--model`. They are not separate abstractions — see "Reads vs writes"
and "Layered resolution" below.
Env access is encapsulated: business domains read `config.get(...)` or structured
`IBootstrapService` facts; only the `config` domain reads the raw env bag (from
`IBootstrapService`) to build its overlays. Business domains must not call
`IBootstrapService.getEnv()` directly.
## Layered resolution
`IConfigService` resolves a key by precedence across layers, lowest to highest:
```text
Default registered defaultValue (and code constants promoted to a section)
↓
User config.toml (persisted user preferences)
↓
Operational env overlay (e.g. KIMI_MODEL_*, KIMI_CODE_EXPERIMENTAL_*)
↓
Memory per-run intent (CLI flags); never persisted; highest
```
`set(domain, patch, target?)` writes the `User` layer (persisted) by default;
pass `ConfigTarget.Memory` for a per-run override that is never written to disk.
`inspect(domain)` reports the value at each layer.
## Layout
- `src/app/config/config.ts` — `IConfigRegistry` / `IConfigService` tokens, `ConfigSection`, `ConfigEffectiveOverlay`, event types.
- `src/app/config/configService.ts` — `ConfigRegistry` + `ConfigService` impl; self-registers at App scope. The registry is also the fold of the `ConfigSectionContribution` collection: it drains the module-level contributions at construction, then refolds incrementally (`added` → `registerSection`, `removed` → `unregisterSection`).
- `src/app/config/configSectionContributions.ts` — the `ConfigSectionContribution` collection token (the runtime channel: a unit contributes with `this.provide(ConfigSectionContribution, …)`) plus the module-level `registerConfigSection` collector (the static channel, import = register).
- `src/app/config/configOverlayContributions.ts` — the module-level `registerConfigOverlay` collector for `ConfigEffectiveOverlay`s (drained at construction like the sections).
- `src/app/config/toml.ts` — generic snake_case ↔ camelCase machinery plus the registry-aware `transformTomlData` / `applySectionToToml` entry points. Per-domain normalization lives in the section owner's `configSection.ts` (registered as `fromToml` / `toToml`); this module stays free of any other domain's semantics.
- `src/kosong/model/thinking.ts` (owner domain, not `config`) — the `resolveThinkingEffort` helper and the authoritative `ThinkingConfig` type (the `thinking` section itself registers from `src/app/kosongConfig/configSection.ts`).
- `src/app/config/configPure.ts` — `isPlainObject`, `deepMerge`, `omitUndefined`, `describeUnknownError`.
A domain that owns a section keeps the schema in its own `configSection.ts` (e.g. `src/app/flag/flag.ts` for `experimental`, `src/agent/loop/configSection.ts` for `loopControl`). Exception: kosong-owned sections (`providers`, `models`, `thinking`) — kosong is a pure, persistence-free abstraction layer that defines only the types (`src/kosong/{provider,model}`); the section constants, the zod schemas (re-derived from those types and compile-time pinned via `AssertExact<Equal<z.infer<typeof Schema>, Type>>`, see `_base/utils/typeEquality.ts`), the registrations, env bindings, and TOML transforms all live in the persistence wrapper `src/app/kosongConfig/configSection.ts`. (`modelCatalog` has no kosong-side type at all — its section is fully self-contained in `app/kosongConfig`, types derived from the schema.) A cross-section env overlay (e.g. the `KIMI_MODEL_*` synthesis in `src/app/kosongConfig/envOverlay.ts`) lives in the wrapper too and is registered via module-level `registerConfigOverlay`. The session subagent domain owns two sections in `src/session/subagent/configSection.ts`: `[subagent]` (`timeout_ms` on disk) and `[secondary_model]` (`default_model` plus the `[secondary_model.models]` pool, with a lone legacy v1 `model` key honored as a fallback default below `default_model`); neither carries a cross-section overlay. Cross-field pool validation (default present / in-pool / every key resolvable) runs at session creation in `subagentModelsValidationService.ts`, not in the schema. The two-way sync between config sections and kosong's in-memory registries is owned by `IKosongConfigService` (`src/app/kosongConfig/kosongConfigService.ts`).
## Scope
- `IConfigRegistry` / `IConfigService` — **App** scope, process-global. One registry of sections; one loader reading `~/.kimi-code/config.toml` (path from `IBootstrapService.configPath`).
All config reads go through `IConfigService` (global config). Per-session runtime state (active model, thinking level, etc.) lives in the owning Session-scoped service (e.g. `IProfileService`), not in `config`.
## The section-registry model
A config section is identified by a camelCase domain key (`'providers'`, `'thinking'`, `'loopControl'`). Each section has:
- `schema?: ConfigSchema<T>` — zod schema used to validate the value (absent ⇒ passthrough).
- `defaultValue?: T` — filled when the file has no value for the domain.
- `merge?: ConfigMerge<T>` — how `set(domain, patch)` combines base + patch (default `deepMerge`).
- `fromToml?: ConfigFromToml` — read-path transform (snake_case file value → in-memory shape). Defaults to a plain key-casing pass; owners register one when the on-disk shape needs custom normalization (record key preservation, nested object conversion, array entries, key renames, reshapes).
- `toToml?: ConfigToToml` — write-path transform (in-memory value → snake_case file value). Defaults to a plain camelCase→snake_case key mapping.
Two contribution channels:
- **Static (import = register)** — the owning domain calls `registerConfigSection(domain, schema, options)` at the top level of its `configSection.ts`; `ConfigRegistry` drains the collected contributions when it is constructed. Every in-repo section uses this channel.
- **Runtime (collection record)** — a unit contributes `this.provide(ConfigSectionContribution, { domain, schema, options })` (e.g. a feature assembled through `IFeatureManager`); the `ConfigRegistry` fold registers the section when the record lands and unregisters it when the record is withdrawn (provider disposed). User TOML values survive a withdrawal — they just stop being validated and effective.
Ownership rules:
- **One owner per section.** `registerSection` throws if a domain is registered twice — the static channel fails fast when `ConfigRegistry` drains it; a conflicting runtime record is reported through `onUnexpectedError` and the first registration wins (the fold is an event path and never throws).
- **The domain that consumes a config owns its schema.** This is what keeps `config` from depending on its consumers: `config` must not import `externalHooks` / `permissionRules` / `provider` / `kosong` / etc. for a section's schema. If a schema needs a domain's types, the schema lives in that domain.
- **Demand-driven.** Do not register sections for config that no domain reads yet; a section appears (with its schema in the owning domain) only when a consumer appears.
## Env bindings
A section can declare how its fields are read from environment variables, so the
value resolves through `config.get(...)` rather than ad-hoc `process.env` reads.
Declare the bindings with `envBindings(schema, { … })` — the field names are
type-checked against the schema (no magic strings), and nested schemas recurse:
```ts
registerConfigSection('thinking', ThinkingConfigSchema, {
env: envBindings(ThinkingConfigSchema, {
effort: 'KIMI_MODEL_THINKING_EFFORT',
}),
});
// nested / record section — outer key is a runtime constant, inner fields are
// checked against the value schema:
registerConfigSection('providers', ProvidersSectionSchema, {
env: envBindings(ProvidersSectionSchema, {
[ENV_MODEL_PROVIDER_KEY]: envBindings(ProviderConfigSchema, {
apiKey: 'KIMI_MODEL_API_KEY',
type: 'KIMI_MODEL_PROVIDER_TYPE',
baseUrl:'KIMI_MODEL_BASE_URL',
}),
}),
stripEnv: stripProvidersEnv,
});
```
Each field is an `EnvBinding` — a string (env var name) or
`{ env, deprecatedEnv?, parse?, default? }`. IConfig resolves every field by
`env > config.toml > default`, sets it on the effective value, and validates the
section. Empty nested entries (no field resolved) are omitted, so a synthetic
entry like `__kimi_env__` only appears when at least one of its env vars is set.
When `deprecatedEnv` is set and `env` itself is absent or fails `parse`, the
deprecated var still supplies the value and a warning diagnostic is reported —
use it to rename an env var without breaking existing setups.
`stripEnv(value, raw?, getEnv?)` removes env-derived fields before `set`/`replace`
persists, so env overrides never leak into `config.toml`. `raw` is the section's
env-free camelCase base (already `fromToml`-normalized), and `getEnv` reads the
live env bag. For fields that are **both
user-persistable and env-overridable**, register
`stripEnv: stripEnvBoundFields(sectionEnvBindings)` (from `#/app/config/config`)
— it derives the guard from the same bindings the read path uses: while a
field's env var resolves to a value, writes restore the field's raw-base value
(or drop it) instead of persisting an echoed env value; an env value that
fails the binding's `parse` owns nothing, so writes pass through. Env-only
fields/sections need no env check — strip them unconditionally (e.g. thinking's
`forcedEffort`, cron's whole-section `() => undefined`).
Business domains read `config.get('section')`; they never read env directly, and
never write their own env-merge logic.
## Add a config section (recipe)
1. Define the schema in the owning domain, e.g. `src/<domain>/configSection.ts`:
```ts
export const MY_SECTION = 'mySection';
export const MySectionSchema = z.object({ /* ... */ });
export type MySection = z.infer<typeof MySectionSchema>;
```
2. Register it at the top level of the same module (import = register):
```ts
// src/<domain>/configSection.ts
import { registerConfigSection } from '#/app/config/configSectionContributions';
registerConfigSection(MY_SECTION, MySectionSchema, { defaultValue: {} });
```
`ConfigRegistry` drains module-level contributions when it is constructed, so the section exists before any consumer resolves `IConfigService` — no owning Service needs to be constructed first. Make sure `src/index.ts` imports the leaf so the top-level call runs.
3. (Runtime variant) a dynamically loaded unit (e.g. one assembled through `IFeatureManager`) contributes the section as a collection record instead:
```ts
this.provide(ConfigSectionContribution, { domain: MY_SECTION, schema: MySectionSchema, options: { defaultValue: {} } });
```
The `ConfigRegistry` fold registers it incrementally and unregisters it when the unit is retracted (user TOML values survive) — see "Late registration".
4. Read it anywhere via `IConfigService`:
```ts
constructor(@IConfigService private readonly config: IConfigService) {}
// ...
const value = this.config.get<MySection>(MY_SECTION);
```
5. React to edits by subscribing `IConfigService.onDidChange` and filtering on `e.domain === MY_SECTION` (see `FlagService`).
6. Write it only through `IConfigService.set(domain, patch)` (merge) or `.replace(domain, value)` (wholesale). Never write `config.toml` directly.
## Reads vs writes
Data flow is one-way by default — reading config never touches the file:
```text
config.toml ──load──▶ IConfigService.effective ──get──▶ services read
▲ │
└──────── IConfigService.set/replace ◀──── only on explicit writes
```
- **Read path** (startup, every service): `config.toml` is loaded into `IConfigService` once; services read via `get()`. This path **never writes the file**.
- **Write path** (rare): `config.toml` is rewritten only when something explicitly calls `IConfigService.set/replace`. The only production writers today are provider CRUD (`ProviderService.set/delete`, e.g. provisioning a provider after OAuth login).
**Runtime service state is not config.** Mutating a service at runtime does **not** rewrite `config.toml`:
- `ProfileService.configure(...)` / `update(...)` / `setModel(...)` / `setThinking(...)` only change **in-memory** fields and append to the session **wireRecord** (for replay). They never call `IConfigService.set`.
- Switching model or thinking level mid-session is session runtime state, not a config edit — the user's `config.toml` is left untouched.
So `configure(...)` never overwrites the local file. Treat `config.toml` as the user's static config; runtime overrides live in memory and the session record.
## Late registration
`ConfigService` loads in its constructor (first `get(IConfigService)`). Static sections are drained before that, but a runtime-contributed section (a `ConfigSectionContribution` record) can register at any later moment. To keep validation and defaults correct:
- `IConfigRegistry` emits `onDidRegisterSection` whenever a section is registered (and `onDidUnregisterSection` when a runtime record is withdrawn).
- `ConfigService` subscribes and, on registration, re-validates the already-loaded raw value for that domain, applies the default if the raw value is absent, re-runs the env overlay, and fires `onDidChange` if the effective value changed. On unregistration it devalidates the domain — `get(domain)` falls back to the raw value.
- Before a section is registered, `get(domain)` returns the raw (transformed, unvalidated) value; consumers that need validated values should read after the section lands, or react to `onDidChange`.
This means registration order is never a correctness concern — you do not need an eager bootstrap.
## TOML on-disk format
`config.toml` stores keys in **snake_case**; in-memory values are **camelCase**. `ConfigService` converts both ways by dispatching to each section's registered transform:
- **Read**: `transformTomlData(fileData, registry)` maps each top-level key to a domain and applies that domain's `fromToml` hook (or a plain key-casing pass when none is registered). Owner domains register their own normalization — e.g. provider `oauth`/`env`/`customHeaders`, permission `deny/allow/ask` → `rules`, `experimental` keys preserved verbatim. When a section registers after the initial load, `ConfigService` re-applies its `fromToml` against the preserved snake_case raw value (see "Late registration"), so registration order is never a correctness concern.
- **Write**: `applySectionToToml(rawSnake, domain, value, registry)` applies the domain's `toToml` hook (or a plain camelCase→snake_case mapping) into a raw clone of the file, preserving unknown top-level keys and unknown sub-fields (lossless round-trip).
`ConfigService` keeps four views:
- `rawSnake` — snake_case clone of the file; the write base, never carries the env overlay.
- `raw` — camelCase, env-free; the read/set/replace base.
- `validated` — validated `raw`, env-free; the base every live env re-application starts from, so a degraded or removed env value falls back to the file instead of a stale overlay.
- `effective` — `validated` plus the env overlay, recomputed on load/set; `get()`/`getAll()` re-apply the overlay on a fresh `validated` copy per read rather than caching it.
### Renaming config keys and env vars (deprecations)
Renames are declared once on the section, never hand-rolled in `fromToml`:
```ts
registerSection(MY_SECTION, MySectionSchema, {
deprecations: [{ key: 'old_key', replacement: 'new_key' }], // snake_case, on-disk
env: envBindings(MySectionSchema, {
newKey: { env: 'KIMI_NEW_KEY', deprecatedEnv: 'KIMI_OLD_KEY', parse },
}),
});
```
- A deprecated TOML key is **ignored** (its value no longer applies — the schema only knows the new key) and reports a warning `ConfigDiagnostic` while present; the file is never rewritten, so the warning is the migration guide. Diagnostics are recomputed on every load/reload and surface to clients via `IConfigService.diagnostics()` and `onDidChangeDiagnostics` (kap-server republishes them as the global `event.config.warning` WS event).
- A deprecated env var still **resolves** as a fallback (new var first), with the same warning treatment, and `stripEnvBoundFields` treats it as env-owned for writes.
- See `src/agent/loop/configSection.ts` for a worked example (`max_retries_per_step` → `max_attempts_per_step`).
### `KIMI_MODEL_*` env overlay
When `KIMI_MODEL_NAME` is set, the `kosongConfig` wrapper's `kimiModelEnvOverlay` (`src/app/kosongConfig/envOverlay.ts`) injects a reserved model alias (`__kimi_env_model__`) into `effective`, points `defaultModel` at it, and merges the request `modelOverrides`; the reserved provider (`__kimi_env__`) comes from the `providers` section env bindings. The overlay is registered via module-level `registerConfigOverlay` and applied **only to `effective`**, never to `rawSnake`, so it is never persisted. Its `strip` (plus the providers section `stripEnv`) is the final guard so a caller that read `effective` (with the overlay) cannot write the reserved entries or the shell API key back to disk. `config` itself only runs registered overlays — it does not know the `KIMI_MODEL_*` semantics.
## Owner-owned sections
`config` holds no monolithic config schema and no whole-config object. Every section is owned by the domain that consumes it: the schema (and any `fromToml` / `toToml` normalization and `stripEnv`) lives in that domain's `configSection.ts`, and the domain contributes it via module-level `registerConfigSection` (or a runtime `ConfigSectionContribution` record). Cross-section env behavior (e.g. `KIMI_MODEL_*`) lives in an owner-registered `ConfigEffectiveOverlay` (module-level `registerConfigOverlay`). To add a section, follow "Add a config section" above in the owning domain — never add schema or normalization to `config` itself.
## Ownership map (generated)
The authoritative, always-current list of registered sections — rendered in the on-disk `config.toml` shape, with owner file, scope, defaults, env bindings, and schema fields — is generated from the live registry:
- `packages/agent-core-v2/docs/config-manifest.toml` (checked in; do not edit by hand).
- Regenerate with `pnpm --filter @moonshot-ai/agent-core-v2 gen:config-manifest` (add `--check` for a freshness check; `test/app/config/configManifest.test.ts` enforces it in CI).
`config` must not import from any of these owner domains; that is the whole reason the schemas, TOML normalization, and env overlays live with their owners.
## Scope & dependencies
- `config` is a low-level capability: domains that own sections import `config` (for `IConfigRegistry` / `IConfigService`), never the reverse — section schemas live in the owning domain.
- Cross-domain type sharing for a config type: prefer importing the type from the owning domain over re-declaring it (e.g. `plugin` imports `McpServerConfig` from the MCP config schema).
- `IConfigRegistry` / `IConfigService` are **App**. Agent scope services may inject App services via ancestor lookup.
- `config` never imports a higher domain and holds no section schemas of its own; if a section needs a type from another domain, that schema lives in that domain.
## Red lines (this topic)
- One owner per section: a duplicate static registration throws when `ConfigRegistry` drains it; a conflicting runtime record is logged (`onUnexpectedError`) and the first registration wins.
- `config` never imports the domains that consume it — keep section schemas in the owning domain.
- Config is the **preference registry**: register only values that are preferences, persistable, schema'd, and user/operator-facing. Facts → `IBootstrapService`; session state → Session scope; constants → code.
- Business domains read `config.get(...)` or structured `IBootstrapService` facts; never call `IBootstrapService.getEnv()` directly — only `config` reads the raw env bag to build overlays.
- Keep `IBootstrapService` domain-agnostic: host invocation arguments (CLI flags, host identity headers, prompt identity) go into `BootstrapInput.args` / `IBootstrapService.args` — never into new per-domain runtime-options services; domain runtime state (cron, flags, model params, …) never goes onto `IBootstrapService` at all. Domain-specific config goes through `registerConfigSection` + `envBindings`, read via `config.get(...)`.
- Do not pass a whole config bag via options; read each section through `IConfigService`. There is no `KimiConfig` object — config is a registry of owner-owned sections.
- `config.toml` is snake_case on disk, camelCase in memory — never write camelCase keys to disk, and never write to `config.toml` except through `IConfigService.set/replace`.
- Reading config / calling `configure(...)` / switching model at runtime must not rewrite `config.toml`; runtime state lives in memory and the session wireRecord, not the file.
- Never persist env overlays (`__kimi_env__` / `__kimi_env_model__` / shell API key / experimental env); overlays live only in `effective` / `Memory`.
- Runtime contribution (a `ConfigSectionContribution` record from a unit at any scope) is fine — the late-registration mechanism keeps validation correct; the static channel needs no eager bootstrap (import = register, drained at `ConfigRegistry` construction).

View file

@ -0,0 +1,289 @@
# Stage 2 — Design a service
Decide *where things live and who knows whom* before writing code. Every rule here derives from two questions:
1. **What is the identity of the state it owns?** → decides the **Scope**.
2. **Who owns the decision, and who needs the result?** → decides the **calling style** and **dependency direction**.
## 1. What a Service is
A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetime**.
- **Behavior** is almost free — the same logic runs anywhere, so it does not by itself decide a scope.
- **State** pins a Service to a scope. State has an **identity** (what it is keyed by) and a **lifetime** (when it is born, when it dies).
- **Dependencies / calling style** answer a different question: who controls whom, and who knows whom.
## 2. Choosing a scope
> Scope = the identity + lifetime of the owned state.
| Scope | State identity (keyed by) | Lifetime |
|---|---|---|
| `App` | none (single global instance) | the process |
| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) |
| `Session` | `sessionId` | one session |
| `Agent` | `agentId` | one agent |
### Decision tree
**Q1. Does it own mutable state?**
- No (pure behavior) → jump to Q3.
- Yes → Q2.
**Q2. What is the identity of that state?**
- one global instance → **`App`**
- one per workspace (shared by every session of that workspace) → **`Workspace`**
- one per session → **`Session`**
- one per agent → **`Agent`**
- a mix (a global registry *and* per-instance state) → **split it** (see §3).
**Q3 (stateless). What is the shortest-lived dependency it must inject?**
A stateless Service is pulled *down* by its shortest-lived dependency: if it injects an `Agent`-scoped Service, it cannot be `App`. Among the scopes that still satisfy every dependency, **default to the longest-lived one** (usually `App`) to maximize reuse. Push it down only when it must inject a shorter-lived Service, or when you want to limit its visibility.
### The core anti-pattern (a litmus test)
> **Do not store per-session state in a `Map<sessionId, …>` inside an `App` Service.**
This is the tell-tale sign of "should have been `Session`-scoped but was parked at `App`". Consequences: nobody cleans the entry up when the session ends (leak); every consumer threads `sessionId` around (loss of type safety); it cannot inject `Session`/`Agent`-scoped collaborators.
### One-sentence self-check
> "When this scope is disposed, should this state disappear with it?"
>
> - Yes → the scope is right.
> - It must outlive the scope → too short; move up one tier.
> - It should be one-per-unit but is shared → too long; move down one tier.
## Scope is not a domain
Scope answers **lifetime and visibility**. Domain answers **responsibility and data ownership**. A Service registered at `Session` or `Agent` scope is not automatically part of the `session` or `agent` domain, and an entity Service must not be named `I{Scope}EntityService` just because its data is scoped that way.
Use the data-ownership test and the `session` / `agent` / `turn` split conclusions in [domain-boundaries.md](domain-boundaries.md) before naming a Service or adding `I{Domain}EntityService`.
## 3. Multi-Scope splitting
> One Service owns state at exactly one identity / lifetime. If a domain owns state at several lifetimes, split it along those boundaries — one Service per lifetime.
The standard split is "global registry / factory" + "per-instance":
| Tier | Role | Naming tends to |
|---|---|---|
| `App` | global registry / catalog / factory — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
| `Workspace` / `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` |
Canonical splits in the codebase:
- **`records`** — `ISessionStore` (`App`) + `ISessionMetaStore` (`Session`) + `IAgentRecords` (`Agent`).
- **`config`** — `IConfigRegistry` / `IConfigService` (`App`).
- **`kosong`** — `IProtocolHandlerRegistry` (`App`) + `IProviderManager` (`Session`). Generation is driven by `ILLMRequester` (`Agent`) in the `llmRequester` domain.
- **`tool`** — `IToolDefinitionRegistry` (`App`) + `IToolService` (`Agent`).
Split when the domain genuinely has both a global view and per-instance state. Do **not** split when state lives at only one lifetime (e.g. purely `App` like `log`; purely `Agent` like `prompt`). Do not pre-split for symmetry.
After the split, the `App` Service usually plays the **factory**; most consumers inject the **per-instance** Service. Inject the `App` factory only when you genuinely need cross-instance management.
## 4. Choosing a calling style
Three mechanisms answer three different questions:
| Mechanism | Nature | Coupling | Returns a value? | Consumers |
|---|---|---|---|---|
| **Direct call** | command: A tells B to do | A → B | yes | one (known) |
| **Event** | fact: A announces "X happened" | both depend only on the bus | no | zero / one / many (unknown) |
| **Hook** (`onWill` / `onDid`, `OrderedHookSlot`) | participation: observers step into an operation, in order | both depend only on the bus | can observe / veto | many, but ordered |
### Decision tree
**Q1. Does A need a return value from B?** → Yes: **direct call**. Events cannot return a value (request/reply over events is an anti-pattern).
**Q2. Is B's reaction part of A's responsibility, or B's own concern?**
- A's responsibility *includes* B's behavior (A orchestrates B) → **direct call**. E.g. `session` drives `agentLifecycle`; `loop` drives `llmRequester` / `toolExecutor`.
- B's reaction is B's own concern, A merely states a fact → **event**. E.g. `flag` reacts to `config.onDidChange`.
**Q3. How many consumers?**
- exactly one, known → **direct call**.
- zero / one / many, producer should not know → **event**.
**Q4. Would a direct A→B call create a cycle or violate scope direction?** → A *consequence check*, not a primary reason. Decide by Q1–Q3 first; do not turn a genuine direct call into an event just to break a cycle.
**Q5. Is this fact part of the durable record / replay / cross-agent projection?** → Yes: **emit it on the wire** (`wireRecord`). State changes that must be recorded, replayed, or synchronized across agents are projected onto the wire, not handled by a direct call alone (`permission.set_mode`, `goal.create/update/clear`, `plan_mode.enter/exit`). The wire is the *durable record*, not the live notification channel.
### One-sentence rule
> "I am telling you to do this, and I may need the result" → **direct call.**
> "I am announcing that something happened; react if you care" → **event.**
> "I am announcing something, and you may step in, in order, possibly to veto" → **hook.**
### As extension points (open-closed)
The three mechanisms above are also where a domain accepts new behavior without being edited. When adding a scenario would otherwise require changing this domain's `if/else`, expose the right extension point instead:
| Need | Extension point | Typical scope |
|---|---|---|
| Register a new implementation / definition | a **registry / catalog** the domain queries | `App` |
| React to a fact the domain announces | an **event** on the bus | the announcing scope |
| Step into an operation in order / veto | a **hook** (`onWill`/`onDid`, `OrderedHookSlot`) | the owning scope |
| Swap a backend (File ↔ DB ↔ S3) | a **Store / Storage token** at the byte layer (see persistence.md) | `App` (composition root) |
The standard shape of a "registry / catalog the domain queries" row is an L3 contribution point: the target domain owns a `collection<T>` token, contributors call `this.provide(token, record)` from a unit, and a fold service in the target domain injects the `CollectionView` (incremental `onDidChange`; provider death withdraws the record). The four in-repo seams are `ConfigSectionContribution` → `ConfigRegistry`, `AgentToolContribution` → `AgentToolActivationService`, `AgentProfileContribution` → `IAgentProfileRegistry`, and `WireModelContribution` → `WireService` (file-level pointers: `packages/agent-core-v2/AGENTS.md` §Units and contribution points).
Closed-for-modification means: the domain's own file is not where new scenarios branch. If a new scenario forces an edit here, an extension point is missing or misplaced.
## 5. Dependency direction
Two layers are involved:
- **Scope direction**: short-lived → long-lived, **enforced by the container** (see orient.md).
- **Domain direction**: which domain may depend on which — **a matter of judgment**, not enforced by the container.
> **A depends on B iff A needs B's data or behavior to do its own job.**
Add one anti-rot heuristic to keep the graph from collapsing into a clique:
> **Do not let a more foundational / more-reused Service come to know a more specific / more-upstream one.**
Once a foundational component knows about an upstream scenario, it can no longer be reused by other scenarios and will almost always create a cycle.
### The boundaries of this repo
`agent-core-v2` has no mechanical domain-layer numbering — dependency direction is the judgment rule above, applied per domain. What remains enforceable is a small set of specific boundaries (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core`).
- The kosong subtree keeps its strict internal order (`contract ← protocol ← provider/model`, purity bans, the `provider/bases` registration boundary).
Two standing red lines on top of that:
- The **base substrate** (`_base`, errors, wire types) never depends on any business domain.
- Business logic never depends on the **edge** (`gateway`, `rpc`, the `*Legacy` v1 adapters) — business code should not know REST / WebSocket exist.
- A cycle means knowledge was placed backwards: extract a third, more foundational Service, or invert the "notification" half into an event.
> Capability → orchestrator (e.g. `prompt → turn`) is allowed and present in this repo; the real red line is *inverted reuse* — a foundational / lower Service depending on a specific / upper one.
> When a Service is meant to be reached over the wire (`/api/v2`, WS), see [edge-exposure.md](edge-exposure.md) for the per-scope `resource:action` map, which Services may be exposed directly vs wrapped in a facade, and how events stream.
## 6. New-Service checklist
1. **What does it remember, and what is the state's identity?** → pick the scope (§2).
2. **What is the shortest-lived dependency it must inject?** → the scope cannot be longer than that.
3. **Does it own state at both a global and a per-instance lifetime?** → if yes, split Multi-Scope (§3).
4. **For each collaborator: am I commanding it, notifying it, or letting it participate?** → pick the calling style (§4).
5. **Does each dependency arrow make a more foundational thing know a more specific thing?** → if yes, invert it (§5).
## 7. Render the placement tree
After the checklist, render the result as a plaintext tree — the deliverable reviewers read. Keep it in the design doc or PR description.
```text
domain: `<name>` (owning scope: <Scope>)
├─ serves (who uses me) tag = HOW they reach me
│ ├─ (inject) <ConsumerDomain> @<Scope> — <what they use me for>
│ └─ (accessor) <ConsumerDomain> @<Scope> — <what they use me for>
├─ exposes (interfaces I provide, by scope)
│ ├─ App : <IXxxRegistry> — <role>
│ ├─ Workspace : <IWorkspaceXxx> — <role>
│ ├─ Session : <ISessionXxx> — <role>
│ └─ Agent : <IAgentXxx> — <role>
└─ depends (what I inject) tag = calling style
└─ <DepDomain> @<Scope> direct/event/hook — <what for>
```
Conventions:
- List **only real interfaces**; write `—` for a scope with no exposed interface. Most domains are single-scope — do not invent symmetry.
- On `depends`, tag each arrow with its calling style: `direct`, `event`, or `hook`.
- On `serves`, tag each consumer with its **access mechanism**, grouped `inject` first then `accessor`:
- `inject` — a descendant or peer scope DI-injects me. Resolved by the container; lifetime-safe.
- `accessor` — an ancestor or edge scope borrows me through `IScopeHandle.accessor.get(...)`. Valid only while this scope lives; never cache the result; must run before the child scope is disposed. See the cross-scope borrow diagram below.
- An empty `(inject)` group with a non-empty `(accessor)` group is a signal: the interface is currently an edge / lifecycle command surface — check it is not leaking internals.
- A consumer is upstream of you. If you cannot name one business consumer, the domain may be dead or mis-scoped.
### Cross-scope borrow diagram
When a domain has `accessor` consumers, draw the reverse-direction borrow next to the tree so it is never mistaken for injection:
```text
App scope
<AncestorService> ──holds──► IScopeHandle(<id>)
│
│ accessor.get(<IMyService>)
│ └── resolve runs inside the child scope
▼
<Child> scope (<id>)
<MyService> ← the interface lives here
```
Read it as:
- `──holds──►` = the ancestor owns a handle to the child scope (it stores the key, not the service). DI allows this.
- `accessor.get(...)` = a **runtime borrow**, not a dependency edge. It must cross an `IScopeHandle`, run on demand, never be cached, and finish before the child scope is disposed.
Worked example — `sessionLifecycle`:
```text
domain: `sessionLifecycle` (owning scope: Workspace)
├─ serves (who uses me)
│ ├─ (inject) — (none)
│ └─ (accessor)
│ ├─ sessionLegacy @App(edge) — v1-compatible create/fork/archive/…
│ └─ gateway / rpc @App(edge) — native v2 session lifecycle actions
├─ exposes (interfaces I provide, by scope)
│ ├─ Workspace : ISessionLifecycleService — owns this workspace's live session scope tree
│ ├─ Session : — — (per-session state lives in sessionMetadata / agentLifecycle / …)
│ └─ Agent : — — (per-agent state lives in agentLifecycle)
└─ depends (what I inject)
├─ workspaceContext @Workspace seed — handler identity + persistence scope
├─ bootstrap @App direct — addresses session storage
├─ hostEnvironment @App direct — gates scope creation on the probe
├─ sessionIndex @App direct — persisted read model for cold resumes
├─ storage @App direct — atomic docs + append logs
├─ workspaceDirs / workspaceSkillCatalog / workspaceMcp / …
│ @Workspace direct — the handler's shared resource services
└─ event @App direct — broadcasts session-level facts (e.g. archived)
```
Cross-scope borrow for `sessionLifecycle`:
```text
App scope
WorkspaceLifecycleService ──holds──► IScopeHandle(workspaceId) (one per live handler)
│
│ accessor.get(ISessionLifecycleService)
│ └── resolve runs inside the Workspace scope
▼
Workspace scope (workspaceId)
SessionLifecycleService ──holds──► IScopeHandle(sessionId)
│
│ accessor.get(ISessionMetadata) …
│ └── resolve runs inside the Session scope
▼
Session scope (sessionId)
sessionMetadata / agentLifecycle / … ← per-session services live here
```
How the three lenses shaped it:
- **Scope (§2)** → the live registry of one workspace's session scopes is per-handler, so it is Workspace-scoped; the process-wide handler registry lives in the App-scoped `workspaceLifecycle`; per-session data stays in Session-scoped services, reached through the handle's `accessor`.
- **Dependency direction (§5)** → `sessionLifecycle` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service.
- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `sessionLifecycle`.
For a multi-scope split, the `exposes` block fills more than one scope — see the `records` pattern in §3.
## Red lines (this stage)
- Scope is not a domain; ownership follows write authority and invariants, not read consumption.
- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`) that re-merge multiple domains.
- No `Map<sessionId, …>` at `App` to fake per-session state.
- Scope follows state identity; stateless Services are pulled down by their shortest-lived dependency, otherwise default to `App`.
- Do not pre-split a domain that has state at only one lifetime.
- Need a result / I orchestrate → direct call; stating a fact → event; ordered participation / may veto → hook.
- Foundational layers never know upstream ones; business code never depends on the edge layer.
- A cycle means knowledge is placed backwards — refactor, do not route around it.
- Render the placement tree with real interfaces only — never pad an empty scope for symmetry.
- Tag `serves` consumers with `inject` / `accessor`; an empty `inject` group is a signal to check the interface is not leaking internals.
- An `accessor` consumer is a runtime borrow across a scope boundary, not DI injection — never cache the result and finish before the child scope disposes.
- A `serves` list with no business consumer (or only edge consumers) signals a dead or leaking interface.

View file

@ -0,0 +1,203 @@
# Topic — Domain boundaries vs Scope
How to keep `agent-core-v2` from recreating a god object after splitting one. Read this before naming a Service, adding an `I{Domain}EntityService`, or deciding whether data belongs to `session`, `agent`, or `turn`.
## The one-sentence rule
> **Scope is a lifetime and visibility boundary; a domain is a responsibility and data-ownership boundary.**
A Service registered at `LifecycleScope.Session` or `LifecycleScope.Agent` is **not automatically in the `session` or `agent` domain**. Scope says when an instance is born, when it dies, and who can see it. Domain says which business responsibility it owns and which data it is allowed to mutate.
## Definitions
| Term | Meaning |
|---|---|
| **Scope** | Lifetime / visibility tier. Current code registers Services at `App`, `Session`, or `Agent`. |
| **Domain** | A cohesive business responsibility with its own model, invariants, and write authority. |
| **Entity** | Data with identity and lifecycle, usually suitable for `get/list/create/update/delete` semantics. |
| **Aggregate** | A consistency boundary: the owner that enforces invariants over a cluster of data. |
| **Read model / projection** | Derived data built for queries; it may be shaped like a domain, but it is not the write authority. |
| **Runtime state** | Ephemeral data that dies with its scope; it should not be forced into an entity store. |
## The data-ownership test
Do not ask "does Session / Agent / Turn use this data?". Most data is used by several of them. Ask these instead:
1. **What is the data's identity?** `sessionId`, `agentId`, `turnId`, `taskId`, `workspaceId`, `providerName`, or something else?
2. **Who is the only writer?** The writer is usually the owner. Readers and projectors are not owners.
3. **Who enforces the invariants?** The domain that decides valid transitions owns the model.
4. **What is the authoritative source?** Atomic document, append-log / event stream, blob, query projection, config, or runtime memory?
5. **Can it be named without `Session` / `Agent` / `Turn`?** If yes, it probably deserves its own domain.
Examples:
- `PermissionRules` are Agent-scoped, but `permission` owns rule changes and evaluation.
- `BackgroundTask` is spawned by an Agent, but `background` owns task state and output.
- `ContextMessage` is consumed by the Agent loop, but `contextMemory` / `wireRecord` owns history and replay.
- `SessionMeta` is about a Session, but it is owned by `sessionMetadata`, not by a broad `session` data bag.
## Persistence models are not all entity CRUD
Before introducing `I{Domain}EntityService`, classify the persistence model:
| Persistence model | Use when | Examples |
|---|---|---|
| **Atomic document** | One typed document per key | `SessionMeta`, `config.toml` |
| **Append-log / event-sourced** | The authoritative record is "what happened" | `wireRecord`, `contextMemory`, `goal`, `plan`, `permission` transitions |
| **Blob / key-value** | Large or content-addressed bytes | media offload, blob store |
| **Indexed query / read model** | Derived, queryable view | `sessionIndex`, future `IQueryStore` projections |
| **Registry / catalog** | Global or scoped known items | `workspace`, `toolRegistry` |
| **Ephemeral runtime state** | No durable entity | active turn handle, pending interactions, terminal handles |
See [persistence.md](persistence.md) for the `Store → Storage → backend` rules. A domain EntityService is a business facade over those stores; it is not a replacement for the store layer.
## Naming consequence
Do not name Services after a scope or a god-object-shaped concept:
- ❌ `IAgentEntityService`
- ❌ `IAgentDataService`
- ❌ `ISessionEntityService`
- ❌ `ITurnEntityService` that bundles context, tools, permissions, and telemetry
Name Services after the real owning domain:
- ✅ `ISessionMetadata`
- ✅ `ISessionIndex`
- ✅ `IAgentLifecycleService`
- ✅ `ITurnService`
- ✅ `IBackgroundTaskEntityService`
- ✅ `ICronTaskEntityService`
- ✅ `IPermissionRulesService`
`Session` and `Agent` are valid scope names. They are usually **not** good data-owner names.
## Split conclusion — `session`
`session` is both a Scope and a narrow Domain. Keep the Domain small.
The `session` domain owns only Session-level identity, metadata, lifecycle commands, and Session-level read views:
| Concern | Owner | Notes |
|---|---|---|
| `sessionId`, `workspaceId`, `sessionDir`, `metaScope` | `sessionContext` | Seeded facts; no IO |
| `SessionMeta` | `sessionMetadata` | Durable atomic document; entity-like |
| Open session scope registry | `sessionLifecycle` | Workspace-scope live handles, one registry per workspace handler (the process-wide handler registry is `workspaceLifecycle`); not the persisted entity table |
| Session commands such as `archive()` | `session` | Orchestrates metadata, agent teardown, and events |
| Persisted session list / get / count | `sessionIndex` | Backend-neutral read model |
| Running / idle / awaiting status | `sessionActivity` | Derived from interactions and active turns; owns no state |
`session` must not reabsorb these:
| Data | Real owner |
|---|---|
| Agent instances / handles | `agentLifecycle` |
| Turns | `turn` |
| Context messages | `contextMemory` / `wireRecord` |
| Tool state | `toolStore` / `tool` |
| Permission rules / mode | `permission` |
| Profile / model | `profile` |
| Goal / Plan | `goal` / `plan` |
| Background tasks | `background` |
| Cron tasks | `cron` |
| Pending approvals / questions | `interaction` / `approval` / `question` |
| Workspace | `workspace` |
| Provider / config | `provider` / `config` |
Entity-service conclusion for `session`:
- ✅ `ISessionMetadata` is already an entity-document Service.
- ✅ `ISessionIndex` is a query/read-model Service.
- ❌ Do not create a broad `ISessionEntityService` that owns agents, turns, records, interactions, logs, workspace, and config.
## Split conclusion — `agent`
`agent` is primarily a Scope and composition boundary, not a large data Domain.
Strictly, the `agent` domain owns only Agent-instance concerns:
| Concern | Owner | Notes |
|---|---|---|
| Agent instance identity / handle | `agentLifecycle` | Owns live Agent scope handles |
| Agent creation / removal | `agentLifecycle` | Lifecycle, not a data bag |
| Parent / child relationship | `session` / `agentLifecycle` depending on current code | Do not duplicate it into a new Agent data service |
| Active turn reference | `turn` | Turn is its own domain even though it is Agent-scoped |
Many Agent-scoped Services are **not** in the `agent` domain:
| Data / capability | Real owner | Persistence model |
|---|---|---|
| Wire records | `wireRecord` | Append-log |
| Context messages | `contextMemory` | Event-sourced through `wireRecord` |
| Profile / model config | `profile` | Config + wire records |
| Tool definitions / registry | `toolRegistry` | Runtime registry |
| Tool mutable state | `toolStore` | Wire records |
| Permission mode / rules | `permissionMode` / `permissionRules` | Wire records + config |
| Goal | `goal` | Wire records |
| Plan | `plan` | Wire records + plan file |
| Skill activation | `skill` | Wire records |
| Background tasks | `background` | Task records / output logs, candidate for entity service |
| Cron tasks | `cron` | Task records, candidate for entity service |
Entity-service conclusion for `agent`:
- ✅ Keep `IAgentLifecycleService` for Agent instance lifecycle.
- ✅ If a persisted Agent identity registry is ever needed, name it after that narrow concern, e.g. `IAgentInstanceRegistry`.
- ❌ Do not create `IAgentEntityService` or `IAgentDataService` that bundles profile, records, tools, permission, goal, plan, background, cron, and turn.
## Split conclusion — `turn`
`turn` is a Domain, but it is **not** currently a separate `LifecycleScope` in code; `ITurnService` is registered at `Agent` scope.
`turn` owns one execution round's runtime state and turn-level facts:
| Concern | Owner | Notes |
|---|---|---|
| Active `Turn` handle | `turn` | `id`, `abortController`, `ready`, `result` |
| Turn id allocation | `turn` | Restored from `turn.prompt` records and `context.append_loop_event` turn ids |
| Turn lifecycle hooks | `turn` | `onLaunched`, `onEnded`, `beforeStep`, `afterStep` |
| `turn.started` / `turn.ended` live events | `turn` | Live event stream |
`turn` must not own these:
| Data / capability | Real owner |
|---|---|
| Prompt and context messages | `contextMemory` |
| Append-only record log mechanics | `wireRecord` |
| Step loop | `loop` |
| Tool execution | `toolExecutor` / `tool` |
| Permission decisions | `permission` |
| External hook policy | `externalHooks` |
| Telemetry pipeline | `telemetry` |
| Event transport | `eventSink` |
Entity-service conclusion for `turn`:
- ✅ Keep `ITurnService` as a runtime orchestrator.
- ✅ Add a Turn read model / projection only if history queries are needed.
- ❌ Do not create `ITurnEntityService` with `create/update/delete/list` over a turn table as the authoritative model.
## Migration recipe
When moving data out of a v1 god object or reviewing a proposed EntityService:
1. **Name the data without using `Session`, `Agent`, or `Turn`.** If you cannot, the domain is probably unclear.
2. **Find the writer.** The exclusive writer is the likely owner.
3. **Find the invariant.** The Service that rejects invalid transitions owns the model.
4. **Classify the persistence model.** Atomic document, append-log, blob, query projection, registry, or runtime-only.
5. **Pick the Service shape.**
- Entity document / record → `I{Domain}EntityService` or domain-specific CRUD Service.
- Event-sourced → behavior Service + `wireRecord` record types + optional projection.
- Derived query → read-model Service, not a write authority.
- Runtime-only → scoped Service with no entity store.
6. **Choose the Scope by state identity.** Scope follows what the state is keyed by; it does not decide the domain name.
7. **Render the placement tree** from [design.md §7](design.md#7-render-the-placement-tree).
## Red lines (this topic)
- Scope is not a domain. `Session` / `Agent` scopes do not make data `session` / `agent` owned.
- Ownership follows write authority and invariants, not read consumption.
- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`, `ITurnEntityService`) that re-merge multiple domains.
- Event-sourced domains keep behavior Services and append-log records; do not replace them with arbitrary CRUD.
- Read models may be shaped like a domain, but they are projections, not write authorities.
- A dependency is not ownership. A Service may inject another domain without owning that domain's data.

View file

@ -0,0 +1,183 @@
# Edge exposure — `resource:action` + WS events
How a domain's Services become the wire surface (`/api/v2`) and WebSocket events. This is a **design-time** decision: which Services are exposed, under what public `resource:action` name, and which events stream.
The transport (`/api/v2` over HTTP + WS) lives in the **edge** layer (`gateway`/`rpc`/`transport`). It borrows business Services by interface; business code never imports it.
## 1. The edge model
Four scopes, four URL shapes, one dispatcher:
```text
GET|POST /api/v2/:sa Core
GET|POST /api/v2/workspace/:workspace_id/:sa Workspace
GET|POST /api/v2/session/:session_id/:sa Session
GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent
```
`:sa` is a single path segment of the form `<resource>:<action>` (e.g.
`sessions:list`, `session:read`, `profile:getModel`).
- `:resource` is a **public** name (`sessions`, `session`, `profile`), never an internal domain token (`ISessionMetadata`).
- `:action` is the method. `GET` for reads, `POST` for writes.
- Body = the method's single argument (JSON), omitted for no-arg.
- Response = the project envelope `{ code, msg, data, request_id, details? }`.
- The dispatcher resolves the **scope** from the URL, the **Service** from an `actionMap`, calls the method, wraps the result.
```ts
// actionMap — the allowlist; hides internal domain names.
const actionMap = {
core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... },
workspace: { 'skills:list': { service: IWorkspaceSkillCatalog, method: 'list' }, ... },
session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... },
agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... },
};
```
The `actionMap` is the single allowlist: only mapped `resource:action` pairs are callable; unknown → `40001`.
## 2. What may be exposed directly
A Service method is directly exposable iff **all** hold:
1. Args are JSON-serializable (no live objects, `AbortSignal`, callbacks, resumer fns).
2. Return is JSON-serializable data or `void` (no `IScopeHandle`, `Turn`, `IProcess`, `AsyncIterable`, `IDisposable`, `Event`).
3. Errors are `KimiError` (coded).
4. It is a command/query, not a factory, stream, byte-store, or sink.
If any fail → add a wire-safe orchestration method to the owning domain Service (e.g. `IAgentPromptService.submit` settles `{turn_id}` instead of returning the live `PromptHandle`) or compose several domain Services at the edge — kap-server's `routes/prompts.ts` is the reference for edge-side composition.
## 3. Per-scope `resource:action` map
Read = `GET`, write = `POST`. `sid` = `session_id`, `aid` = `agent_id`.
### Core (`/api/v2/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `sessions` | `listRecent` | ISessionIndex.listRecent | GET |
| `sessions` | `get` | ISessionIndex.get | GET |
| `sessions` | `count` | ISessionIndex.count | GET |
| `workspaces` | `list` | IWorkspaceService.list | GET |
| `workspaces` | `get` | IWorkspaceService.get | GET |
| `workspaces` | `createOrTouch` | IWorkspaceService.createOrTouch | POST |
| `workspaces` | `update` | IWorkspaceService.update | POST |
| `workspaces` | `delete` | IWorkspaceService.delete | POST |
| `config` | `get` / `getAll` / `inspect` | IConfigService.* | GET |
| `config` | `set` / `replace` / `reload` | IConfigService.* | POST |
| `providers` | `list` / `get` | IProviderService.* | GET |
| `providers` | `set` / `delete` | IProviderService.* | POST |
| `oauth` | `startLogin` / `cancelLogin` / `logout` | IOAuthService.* | POST |
| `oauth` | `getFlow` / `status` | IOAuthService.* | GET |
| `auth` | `summarize` | IAuthSummaryService.summarize | GET |
| `auth` | `ensureReady` | IAuthSummaryService.ensureReady | POST |
| `flags` | `snapshot` / `enabled` / `explain` / `explainAll` | IFlagService.* | GET |
| `fs` | `browse` / `home` | IHostFolderBrowser.* | GET |
| `meta` | `getEnv` / `detect` | IBootstrapService.* | GET |
### Session (`/api/v2/session/:sid/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `session` | `read` | ISessionMetadata.read | GET |
| `session` | `update` | ISessionMetadata.update | POST |
| `session` | `setTitle` | ISessionMetadata.setTitle | POST |
| `session` | `setArchived` | ISessionMetadata.setArchived | POST |
| `session` | `status` | ISessionActivity.status | GET |
| `session` | `isIdle` | ISessionActivity.isIdle | GET |
| `session` | `archive` | ISessionLifecycleService.archive | POST |
| `approvals` | `listPending` | IApprovalService.listPending | GET |
| `approvals` | `decide` | IApprovalService.decide | POST |
| `questions` | `listPending` | IQuestionService.listPending | GET |
| `questions` | `answer` | IQuestionService.answer | POST |
| `interactions` | `listPending` | IInteractionService.listPending | GET |
| `interactions` | `respond` | IInteractionService.respond | POST |
| `workspace` | `workDir` / `additionalDirs` / `resolve` | ISessionWorkspaceContext.* | GET |
### Agent (`/api/v2/session/:sid/agent/:aid/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `goal` | `get` | IGoalService.getGoal | GET |
| `goal` | `create` / `pause` / `resume` / `cancel` | IGoalService.* | POST |
| `plan` | `status` | IPlanService.status | GET |
| `plan` | `enter` / `exit` / `cancel` / `clear` | IPlanService.* | POST |
| `tasks` | `list` / `get` / `readOutput` | IBackgroundService.* | GET |
| `tasks` | `stop` / `detach` | IBackgroundService.* | POST |
| `usage` | `status` | IUsageService.status | GET |
| `context` | `status` | IAgentTokenCountingService.get | GET |
| `swarm` | `isActive` | ISwarmService.isActive | GET |
| `swarm` | `enter` / `exit` | ISwarmService.* | POST |
| `permission` | `getMode` | IPermissionModeService.mode | GET |
| `permission` | `setMode` | IPermissionModeService.setMode | POST |
| `permissionRules` | `list` | IPermissionRulesService.rules | GET |
| `permissionRules` | `addRules` | IPermissionRulesService.addRules | POST |
| `profile` | `get` / `getModel` / `getSystemPrompt` / `getActiveToolNames` | IProfileService.* | GET |
| `profile` | `setModel` / `setThinking` | IProfileService.* | POST |
| `messages` | `list` | IContextMemory.get | GET |
| `messages` | `splice` | IContextMemory.splice | POST |
| `toolStore` | `get` / `data` | IToolStoreService.* | GET |
| `toolStore` | `set` | IToolStoreService.set | POST |
| `mcp` | `list` | IMcpService.list | GET |
| `mcp` | `reconnect` | IMcpService.reconnect | POST |
| `tools` | `list` | IToolRegistry.list | GET |
## 4. Facade-needed (wrap before exposing)
These fail §2 and must be wrapped in a facade that takes ids and returns data:
| Service | Why not direct | Facade shape |
|---|---|---|
| ISessionLifecycleService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session |
| IAgentPromptService / IAgentTurnService | returns `Turn` handle | `prompts.submit` / `steer` / `abort` / `undo` |
| ILLMRequester | `AsyncIterable` stream | stream over WS, not RPC |
| ISubagentHost | `SubagentHandle` | `subagents.spawn` / `resume` → info |
| IProcessRunner | `IProcess` streams | terminal (separate WS protocol) |
| Storage / Store (IFileSystemStorageService / IAppendLogStore / IAtomicDocumentStore / IBlobStore) | bytes / streams | not for RPC |
| IAgentFileSystem | `withCwd` handle | `fs.read` / `write` → text/bytes |
| IExternalHooksService | server-side outbound | not exposed |
| IWireRecord | write-ahead log | internal |
## 5. WS events
A single WebSocket endpoint multiplexes RPC `call`s and event `listen`s over a JSON protocol (the lean counterpart of VSCode's `IMessagePassingProtocol`, carrying the same safety features — see §6):
```text
WS /api/v2/ws
```
Client → server: `hello` (auth), `call` (scope + `resource:action` + arg), `cancel`, `listen` (scope + event), `unlisten`, `pong`.
Server → client: `ready`, `result`, `error`, `event`, `ping`.
`call` reuses the same dispatcher as the HTTP routes (scope + `actionMap`). `listen` subscribes to an `Event<T>` source and forwards each emission as an `event` message, keyed by the client-chosen `id`.
The `eventMap` binds a public event name to the scope's `Event` source (analogous to the `actionMap`):
| Scope | event | Source |
|---|---|---|
| Core | `events` | `IEventService.subscribe` (process-wide `DomainEvent` bus) |
| Agent | `events` | `IEventSink.on` (per-agent `AgentEvent` stream) |
Session-level `onDidChange` sources (metadata / interactions) carry no payload today, so they are not exposed until there is a concrete consumer.
Safety / reliability (carried over from `packages/server/src/ws/connection.ts` and VSCode's `ChannelServer`):
- request ids + active-request table — `cancel` / `unlisten` disposes them;
- heartbeat — `ping` every 30s, `pong` timeout 10s → `terminate`;
- schema validation — invalid frames are dropped, not fatal;
- graceful close — dispose listeners, cancel pending, reject in-flight calls;
- no stack traces over the wire;
- non-serializable event payloads are dropped, never fatal.
Cursor / replay / resync for events is a future addition (a separate `call` before `listen`); the raw stream is the foundation.
## 6. Red lines (edge exposure)
- Never expose an internal domain token (`ISessionMetadata`) as a URL segment — use a public `resource` name + `action`.
- Never expose a method that returns a handle / stream / bytes / disposable — wrap in a facade.
- Never expose a method that takes a live object / `AbortSignal` / callback / resumer fn — wrap in a facade.
- Session / Agent Services are reached by `accessor.get` with the id from the URL — never cache the result; finish before the scope disposes.
- The `actionMap` is the allowlist — only mapped `resource:action` pairs are callable; unknown → `40001`.
- Events stream over WS (`listen`), never RPC (`call`).
- Business code never imports the edge (`gateway` / `rpc` / `transport`) — the edge borrows business Services by interface.
- Read = `GET`, write = `POST`; do not overload `POST` for reads when caching / browser-friendliness matters.

View file

@ -0,0 +1,40 @@
# Topic — Errors
Error infrastructure for agent-core-v2: base classes, the per-domain code contract, wire serialization, and the conventions domains follow when raising errors. The package-level reference is `packages/agent-core-v2/docs/errors.md`; this topic summarizes the hot-path rules.
Base classes and serialization are **centralized** in `_base/errors`; error **codes** are **decentralized** — each domain owns an `errors.ts` that self-registers its codes and metadata, and the `src/errors.ts` facade aggregates them into the unified `ErrorCodes` const.
## Where things live
- `src/_base/errors/errors.ts`: base classes — `Error2`, `ExpectedError`, `ErrorNoTelemetry`, `BugIndicatingError`, `NotImplementedError`, plus `isError2` and `unwrapErrorCause`.
- `src/_base/errors/codes.ts`: the `ErrorDomain` contract, the registry (`registerErrorDomain` / `errorInfo` / `isErrorCode`), and `CoreErrors` (`internal`, `not_implemented`). The `ErrorCode` union type is derived by `#/errors` from the aggregated domain definitions.
- `src/_base/errors/serialize.ts`: `ErrorPayload`, `isCodedError`, `toErrorPayload`, `fromErrorPayload`. Wire-facing names (`KimiErrorPayload`, `toKimiErrorPayload`) mirror the protocol and are kept as-is.
- `src/_base/errors/unexpectedError.ts`: `onUnexpectedError` / `setUnexpectedErrorHandler` (global handler).
- `src/<domain>/errors.ts`: the domain's `XxxErrors` descriptor (codes + retryable list + per-code info overrides), self-registered on import.
- `src/errors.ts`: the **facade** — imports every domain's `errors.ts`, builds `ErrorCodes`, re-exports the primitives. Throw sites import from here.
## Conventions (hard rules)
- **Throw a coded error, not a bare string.** `throw new Error2(ErrorCodes.X, …)`. Bare `new Error` only for unreachable guards; `BugIndicatingError` for caller bugs; `NotImplementedError('feature')` for stubs.
- **Define codes in the owning domain**, in `<domain>/errors.ts` as an `XxxErrors` descriptor (`satisfies ErrorDomain` + `registerErrorDomain`), then wire it into the facade. Never add domain codes to `_base/errors`.
- **One `code` per failure mode.** Codes read `domain.reason`. The valid code strings are derived from the aggregated domain definitions (`ErrorCode` in `#/errors` is computed from the `ErrorCodes` aggregate): **add new codes to the owning domain's `errors.ts`** — registration throws on cross-domain collisions. Renaming/removing a code is a major.
- **Translate foreign errors at the boundary.** Provider/HTTP, fs, MCP errors are re-thrown as the owning domain's coded error. `_base/errors` never imports a business domain.
- **Translation is idempotent and cause-preserving.** Translators (`toHostFsError`, `toStorageIoError`) pass through an already-translated error and always keep the original as `cause`.
- **`details` is structured and JSON-serializable; `message` is a short human sentence.** Paths/errnos/scope/key go into `details`, not the message.
- **Cancellation passes through untranslated** (`UserCancellationError` from `_base/utils/abort`) — apply only at boundaries that can actually see cancellation; do not sprinkle the check everywhere.
- **Classify wrapped errors via `unwrapErrorCause`** — errno/status predicates test the unwrapped cause, not the coded wrapper.
- **Branch on `code`, never `instanceof`, across the wire.** In-process, `instanceof Error2` / `isCodedError` are fine.
## Reference tiers
- `os.fs` — `HostFsError` via `toHostFsError` (`os/interface/hostFsErrors.ts`): errno → `os.fs.*`, details `{ path, op, errno?, syscall? }`.
- `os.process` — `HostProcessError`: `spawn_failed` / `kill_failed`, raw error as `cause`.
- `storage` — `StorageError` (`persistence/interface/storage.ts`): `not_found` / `decode_failed` / `corrupted` / `io_failed` (retryable) / `locked` (retryable). ENOENT keeps absence semantics, never an error. A locked query store throws `storage.locked`; consumers catch it explicitly and fall back — no silent no-op degradation.
- `wire` — `WireError` (`wire/errors.ts`): `DuplicateOpError`, `CycleError`, and `wire.unknown_record` (replay skips unknown records, reports via `onUnexpectedError`, returns `{ unknownRecords }`).
## Red lines (this topic)
- Throw a coded error with a `code`, not a bare string (except unreachable guards / `BugIndicatingError` / `NotImplementedError`).
- Codes live in the owning domain's `errors.ts` and self-register; new codes land in the owning domain first.
- Translate foreign errors at the owning domain's boundary, idempotently, with `cause` and structured `details`; `_base/errors` never imports a business domain.
- Branch on `code` across the wire, never `instanceof`.

View file

@ -0,0 +1,108 @@
# Topic — Flags
Experimental feature-flag gating for agent-core-v2 — an App-scope `IFlagService` resolver plus a writable `IFlagRegistry` catalog that domains contribute their flags to, backed by the `[experimental]` config section.
Gate not-yet-public features behind `IFlagService.enabled(id)`, per the repository hard rule that unreleased behavior must be flag-gated. v1 was a process-global `FlagResolver` singleton over a central `FLAG_DEFINITIONS` array; v2 is a scoped DI service whose flag definitions are registered **decentrally** by each owning domain — there is no central catalog to edit.
## Layout
- `src/app/flag/flagRegistry.ts` — `IFlagRegistry` token + `FlagDefinitionInput` / `FlagId` / `FlagSurface` types + `registerFlagDefinition` / `getContributedFlags` (import-time contribution queue).
- `src/app/flag/flagRegistryService.ts` — `FlagRegistryService` impl; in-memory catalog seeded from import-time contributions; App scope.
- `src/app/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `EXPERIMENTAL_SECTION` (`experimental`) / `ExperimentalConfigSchema` (zod) + the module-level `registerConfigSection(EXPERIMENTAL_SECTION, …)` call that owns the section.
- `src/app/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`); reads definitions from `IFlagRegistry` and overrides from `IConfigService`; self-registers at App scope.
- `src/app/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './app/flag/flagService'`).
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.
## Public surface
- `IFlagService` (DI token, App scope): `enabled(id)`, `explain(id)`, `snapshot()`, `enabledIds()`, `explainAll()`, `setConfigOverrides(overrides)`, `registry`.
- `IFlagRegistry` (DI token, App scope): `register(definition)`, `get(id)`, `list()` — writable catalog. `register` is the **runtime** path (tests, dynamic registration); `IFlagService.registry` exposes the same instance for hosts/UI to enumerate flags without resolving them.
- `registerFlagDefinition(definition)` — the **import-time** path. Domains call this from their `flag.ts` top level; contributions are queued and drained by `FlagRegistryService` when it is instantiated.
- `FlagService` / `FlagRegistryService`: exported for tests and hosts that construct them directly.
## Resolution precedence
Highest wins; env is read live on every call (nothing cached):
1. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
2. `[experimental]` config section per-flag override.
3. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
4. Registry `default`.
`explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered.
## Config integration
- The flag domain owns the `[experimental]` section: `src/app/flag/flag.ts` registers it at module load via `registerConfigSection(EXPERIMENTAL_SECTION, ExperimentalConfigSchema, { fromToml, toToml })` (import = register, drained by `ConfigRegistry` at construction); `FlagService` reads overrides from `IConfigService`.
- It subscribes `IConfigService.onDidChange` and refreshes overrides whenever the `experimental` domain changes, so config edits apply live.
- `ConfigRegistry.registerSection` throws if a domain is registered twice — `experimental` is owned exclusively by the flag domain.
- `setConfigOverrides(overrides)` is an imperative escape hatch for tests and hosts without an `IConfigService`; hosts on `IConfigService` should set the `[experimental]` section instead.
Config shape:
```toml
[experimental]
my_feature = false
```
Keys are intentionally loose (`z.record(z.string(), z.boolean())`), so obsolete flags stay inert config.
## Add a flag
Declare the definition in the owning domain's `flag.ts` and call `registerFlagDefinition` at the module top level. There is no central catalog to edit.
`src/<domain>/flag.ts`:
```ts
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';
export const myFeatureFlag: FlagDefinitionInput = {
id: 'my_feature',
title: 'My feature',
description: '...',
env: 'KIMI_CODE_EXPERIMENTAL_MY_FEATURE',
default: false,
surface: 'both',
};
registerFlagDefinition(myFeatureFlag);
```
Then ensure the package entry `src/index.ts` imports the flag leaf precisely so the top-level call runs at import time — there is no `src/<domain>/index.ts` barrel:
```ts
// src/index.ts
import './<domain>/flag';
```
`src/index.ts` imports every domain's leaf files precisely (one line per leaf), so the contribution runs during bootstrap, before any scope is created — and therefore before any consumer resolves `IFlagService`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`.
- `id` must not be `flag`. A duplicate `id` throws when `FlagRegistryService` drains the contributions.
- `FlagId` is `string`, not a literal union: with no central catalog there is nothing to derive it from, so `enabled()` has no compile-time typo-checking. Cover gated behavior with tests instead.
- `surface`: `core` | `tui` | `both` (documentation/grouping only; not used in resolution).
## Consume a flag
Inject `IFlagService` and gate on it. It is resolvable from any scope (App ancestor):
```ts
constructor(@IFlagService private readonly flags: IFlagService) {}
// ...
if (!this.flags.enabled('my_feature')) return;
```
## Layering & scope
- Domain `flag` imports only `config` downward.
- It cannot live in `_base`: registering/reading the config section requires importing `config`, and `_base` is pure infrastructure that must not know any business domain.
- Scope: `IFlagRegistry` and `IFlagService` are both `App`. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at **import time** (top-level `registerFlagDefinition` calls), so they are queued before any scope is created and drained when `FlagRegistryService` is first instantiated — before `IFlagService` is first resolved.
- Tests build `FlagService` + `FlagRegistryService` directly with a real `ConfigRegistry`/`ConfigService` and an injected env map, then `register` the flags they exercise.
## Red lines (this topic)
- Gate unreleased behavior behind a registered flag; no ad-hoc env toggles.
- Contribute each flag from the **owning domain's** `flag.ts` (`src/<domain>/flag.ts`) via a top-level `registerFlagDefinition` call; there is no central catalog to edit. The directory names the domain, so the file is just `flag.ts`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`; `id` must not be `flag`.
- `FlagId` is `string` (decentralized registration) — do not reintroduce a central `FLAG_DEFINITIONS` array or a derived literal union.
- `flag` lives at `App` scope — never in `_base`, never per-session.

View file

@ -0,0 +1,295 @@
# Stage 3 — Implement
Write the contract leaf, implementation leaf (with its registration), and the package-entry lines that load them. Each section below introduces one DI building block as you need it. Source lives in `src/_base/di/`.
## Standard recipe for a new `IXxxService`
1. **Contract leaf** — `src/<domain>/<domain>.ts`: interface (with `_serviceBrand`) + `createDecorator` identity.
2. **Impl leaf** — `src/<domain>/<domain>Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, activation, '<domain>')`. The fourth argument is activation; the fifth is the domain.
3. **Entry** — `src/index.ts`: load each leaf precisely — `export * from './<domain>/<domain>';` for the contract and `import './<domain>/<domain>Service';` for the impl (importing the impl runs the registration). **No `src/<domain>/index.ts` barrel.**
4. **Tests** — see test.md.
There is **no central wiring file**: bindings live in each domain's impl file and are collected through import side effects.
## §1 Interface + identity (a global service, no deps)
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IGreeter {
readonly _serviceBrand: undefined; // type marker: tells DI "this is a service"
hello(): string;
}
export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('greeter');
```
`createDecorator(name)` produces a `ServiceIdentifier` that is three things at once: a runtime key, a parameter decorator, and a compile-time carrier of the `IGreeter` type.
> **The identity name is globally unique.** `createDecorator` caches by `name`; two domains using the same string collide and share one identity.
```ts
// greet/greetService.ts
import { LifecycleScope } from '#/app/scopes';
import { registerScopedService, ScopeActivation } from '#/_base/di/scope';
import { IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined; // mirrors the interface marker
hello(): string { return 'hi'; }
}
registerScopedService(
LifecycleScope.App, // lifetime: process-wide
IGreeter, // identity
Greeter, // implementation
ScopeActivation.OnScopeCreated, // construct when the App scope is created
'greet', // domain name (for diagnostics)
);
```
The scope a class binds to is an **intrinsic property of the class**, decided at the registration point, not the call site.
The impl's top-level `registerScopedService` runs as soon as the module is imported. There is no `greet/index.ts` barrel — instead, add the leafs to the package entry `src/index.ts`, one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService'; // this import runs registerScopedService
```
Anyone can now `accessor.get(IGreeter)` the single global instance.
## §2 Constructor injection (your service uses others)
```ts
export class SessionMetadata extends Disposable implements ISessionMetadata {
declare readonly _serviceBrand: undefined;
constructor(
@ISessionContext private readonly ctx: ISessionContext,
@IAtomicDocumentStore private readonly store: IAtomicDocumentStore,
@ILogService private readonly log: ILogService,
) {
super();
}
}
```
`@ISessionContext` records "parameter 0 needs `ISessionContext`" on the class metadata; the container fills it when constructing.
Three inviolable constraints:
1. **Do not `new` a class with `@IService` deps** — `new` bypasses registration, scope, and the singleton cache. Inject with `@IX` or `accessor.get(IX)`.
2. **`@IX` decorates constructor parameters only.** Decorating a field/method throws at runtime.
3. **Parameter order depends on how the object is built** — for `createInstance` non-singletons, static params come first (see §7); for scoped services, `@IX` params are conventionally first and any static params need defaults. See service-authoring.md §constructor-conventions.
Consumers resolve by interface and never import the impl class:
```ts
const meta = accessor.get(ISessionMetadata); // type is ISessionMetadata
```
> If you need "a config" rather than "a service", model it as a service (e.g. `IConfigService`) and inject it. If you need a per-turn, parameterized, non-singleton object, see §7.
## §3 Scoped registration (not global)
Swap the `scope` argument to bind to a different tier. Use `ScopeActivation.OnDemand` when the service should be constructed only on its first `get()`:
```ts
registerScopedService(
LifecycleScope.Session,
ISessionMetadata,
SessionMetadata,
ScopeActivation.OnDemand,
'sessionMetadata',
);
```
Remember the visibility rule from orient.md: a service may inject services from its own scope or any ancestor; never from a descendant.
## §4 Releasing resources (`Disposable`)
For a service that subscribes to events, starts timers, or holds handles:
```ts
import { Disposable } from '#/_base/di/lifecycle';
export class WSBroadcastService extends Disposable implements IWSBroadcastService {
declare readonly _serviceBrand: undefined;
constructor(@IEventService event: IEventService) {
super();
this._register(event.subscribe(() => { /* … */ })); // collect child resources
}
}
```
- Extend `Disposable`, collect any `IDisposable` with `this._register(d)` (event subscriptions, `toDisposable(fn)`, etc.).
- The container calls `dispose()` automatically when the service is torn down; child resources release in turn.
- Disposal order is deterministic (orient.md): child scopes first; within a scope the Ledger (`src/_base/lifecycle/`) tears entries down in strict reverse registration order, serially — `Disposable` / `DisposableStore` delegate to it.
- Extend `Service` (from `#/_base/di/service`) instead when the unit needs capability calls on `this` (`provide` / `effect` / `on` / `get` / `ref`) — e.g. contributing a record to a `collection` token. `Service` extends `Disposable` (so `_register` is unchanged) and adds the two-phase construction protocol: `provide` / `on` / `effect` calls inside the constructor are buffered and flushed by the kernel after `Reflect.construct`; `get` / `ref` throw inside the constructor — dependencies stay constructor parameters. A manually `new`ed `Service` has no capabilities: every capability call throws.
## §5 Scope activation
`ScopeActivation` is the only construction-timing choice for scoped services:
```ts
export enum ScopeActivation {
OnScopeCreated = 0,
OnDemand = 1,
}
```
```ts
// Default: construct the real instance while the App scope is created.
registerScopedService(
LifecycleScope.App,
ILogService,
LogService,
ScopeActivation.OnScopeCreated,
'log',
);
// Construct the real instance on the first get(IScopeRegistry).
registerScopedService(
LifecycleScope.App,
IScopeRegistry,
ScopeRegistry,
ScopeActivation.OnDemand,
'gateway',
);
```
`ScopeActivation.OnScopeCreated` is the default fourth argument. Scope creation activates every registration using this mode, after constructing its dependencies. An eager constructor failure no longer fails scope creation: the unit lands in sticky `Failed` — scope creation succeeds, resolving the unit rethrows its error, and an explicit `update()` reloads it (see the bootstrap note below). Use it for ordinary services and for constructor side effects that must exist when the scope becomes ready.
`ScopeActivation.OnDemand` stores the descriptor without constructing the service. The first `get()` constructs and caches the real instance directly; later `get()` calls return that same instance. Use it only when construction should wait until the service is actually requested.
Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`.
The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth.
**Bootstrap shares the dynamic provide path.** Scope creation (`Scope.createApp` / `Scope.createChild` / `createScopedChildHandle` in `src/_base/di/scope.ts`) submits the scope kind's entire `registerScopedService` batch as ONE cascade transaction via `provideAll`: every token registers before the activation wave runs, so **registration order never matters**, and untracked transitive `createInstance` resolutions succeed inside the batch. A seed occupying a token (the `extra` tuple in `ScopeOptions`) overrides the static registration for that token. `activateScopeServices` is gone — there is no separate static activation path.
## §6 Using a service inside a plain function (`invokeFunction`)
When you do not want a new class and just need a service once, or when you expose a `ServicesAccessor` to the outside:
```ts
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>): T => instantiation.invokeFunction((a) => a.get(id)),
};
```
`invokeFunction(fn)` hands `fn` a `ServicesAccessor` valid **only during that call**.
> **The accessor is valid only during the invocation.** Calling `accessor.get()` after `invokeFunction` returns throws `"service accessor is only valid during the invocation"`. Do not stash it for async use — inject the service in the constructor (§2) if you need it long-term.
## §7 Creating a non-singleton object with deps (`createInstance`)
For a per-turn executor that also has `@IService` deps:
```ts
class TurnRunner {
constructor(
private readonly input: string, // static param: passed by caller
private readonly turn: number, // static param: passed by caller
@ILogService private readonly log: ILogService, // service param: injected by container
) {}
}
const runner = instantiation.createInstance(TurnRunner, 'hello', 1);
```
Static params come first (you pass them), service params follow (the container fills them), then `Reflect.construct` builds the instance. This object is **not** placed in any scope's singleton cache — every call is a fresh instance — and it is not tracked as a cascade unit either: `createInstance` products are cascade-exempt leaves that no cascade tears down or rebuilds; their owner disposes them.
> This is why service params must follow static params **for `createInstance`**: the container sorts by the parameter positions recorded via `@IX`. `_serviceBrand` lets the compiler tell the two kinds apart. Scoped services built by `registerScopedService` follow a different convention (`@IX` params first, optional static params after) — see service-authoring.md §constructor-conventions.
## §8 Spawning a child scope / child container
For a service that "starts a new session / agent" and needs a child scope, inject `IInstantiationService` itself (every container binds itself as `IInstantiationService`):
```ts
export class ScopeRegistry implements IScopeRegistry {
declare readonly _serviceBrand: undefined;
constructor(@IInstantiationService private readonly instantiation: IInstantiationService) {}
createSession(opts: CreateSessionOptions): Promise<IScopeHandle> {
const collection = new ServiceCollection();
for (const entry of getScopedServiceDescriptors(LifecycleScope.Session)) {
collection.set(entry.id, entry.descriptor); // collect Session-tier descriptors
}
const child = this.instantiation.createChild(collection); // spawn child container
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>): T => child.invokeFunction((a) => a.get(id)),
};
const handle: IScopeHandle = { id: opts.sessionId, kind: LifecycleScope.Session, accessor };
this.sessions.set(opts.sessionId, handle);
return Promise.resolve(handle);
}
}
```
Key points:
- `getScopedServiceDescriptors(scope)` returns every descriptor registered at that tier; load them into a `ServiceCollection`.
- `instantiation.createChild(collection)` builds a child container whose parent pointer is the current container — so the child resolves upward to `App` services (the visibility rule).
- Expose the child to the outside by wrapping it in a `ServicesAccessor` via `invokeFunction` (§6).
> Higher-level code usually calls `Scope.createChild(kind, id)` (it does the "filter descriptors + build child" for you, then submits the whole batch through `provideAll` as one cascade transaction — see §5). Drop to the manual `ServiceCollection` form only when you need explicit control; to change bindings on an already-created container, prefer `provide` / `unprovide` / `update` over rebuilding a collection. Before the static batch lands, the scope-creation point runs the kernel's `ScopeUnits` fold (`_base/di/scopeUnits.ts` — materializes the recipes contributed to `ScopeUnits(kind)` as per-scope units) and then the `ScopeOptions.assemble` hook — the session domain uses the hook to construct its seed-adapter units (`session/sessionSeed/sessionSeedAdapters.ts`) so their provided tokens exist before the session services activate.
## §9 Cyclic dependencies (forbidden — refactor)
Business rule: **no cyclic dependencies.** The container rejects them; the correct response is to refactor, not to make it run.
### The container rejects synchronous cycles
If A needs B while being created and B needs A while being created, the container throws `CyclicDependencyError` with a `path` like `['A', 'B', 'A']`. Self-cycles (A depends on itself) are also rejected. This is a protection mechanism telling you the two services' responsibilities are mis-drawn.
### Why cycles are disallowed
- Scope layering makes normal dependencies a DAG (Agent → Session → Workspace → App, resolving upward); a cycle is almost always a design smell.
- "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug.
v2's stance: **the dependency graph must be acyclic.**
### How to refactor (in priority order)
1. **Extract a third service C.** Move the part A and B both need into C; let A and B both depend on C instead of each other. The most common fix.
2. **Decouple with an event.** If A only needs to know about a change in B, have B emit via `IEventService` and A subscribe, rather than A holding a reference to B.
3. **Re-partition scope.** One of them may belong at a different tier — moving it makes the cycle disappear.
### Activation does not break cycles
Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct through the same synchronous dependency graph. Changing activation cannot make a cycle valid. On `CyclicDependencyError`, refactor per the above.
## Interface cheat sheet
| Interface | Section | Role |
|---|---|---|
| `createDecorator<T>(name)` → `ServiceIdentifier<T>` | §1 | identity (runtime key + compile-time type + param decorator) |
| `@IService` | §2, §7 | declare a dependency on a constructor param |
| `registerScopedService(scope, id, ctor, activation, domain)` | §1, §3, §5 | bind an impl to a lifetime tier and construction time |
| `ServicesAccessor.get(IX)` | §2, §6 | resolve an instance by interface |
| `IInstantiationService.invokeFunction(fn, …)` | §6, §8 | obtain a temporary accessor inside a function |
| `IInstantiationService.createInstance(ctor, …args)` | §7 | build a non-singleton object with deps injected |
| `IInstantiationService.createChild(collection)` | §8 | spawn a child container |
| `getScopedServiceDescriptors(scope)` | §8 | retrieve all descriptors registered at a tier |
| `Disposable` / `DisposableStore` / `IDisposable` | §4 | resource management and disposal |
| `Scope` / `LifecycleScope` | §3, §8 | the lifetime tree |
| `ScopeActivation` | §3, §5 | choose scope-created or first-`get()` construction |
| `Service` (`_base/di/service`) | §4 | unit base class — `this.provide/effect/on/get/ref` capabilities, two-phase construction |
| `collection<T>(name)` / `CollectionView<T>` (`_base/di/collection`) | §4 | contribution-point token + the fold's live view (provider death withdraws the record) |
| `SyncDescriptor` | (tests / low-level) | package a constructor + static args into a pending descriptor |
> Legacy export (not used in v2, just recognize it): `refineServiceDecorator` is a VS Code leftover DI helper. v2 src/test has zero references; always use `registerScopedService`.
## Red lines (this stage)
- No `new` on a class whose constructor carries `@IService` deps — inject or `accessor.get(IX)`.
- `@IX` decorates constructor params only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services — see service-authoring.md).
- Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique.
- `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use.
- No cyclic dependencies — refactor (extract / event / re-scope); activation does not change cycle detection.

View file

@ -0,0 +1,77 @@
# Stage 1 — Orient
Understand the DI × Scope black box and the file conventions before touching business code.
## The DI black box
When writing business code you declare three things; the container handles the rest (when to construct, whether it is the same instance, ordering, disposal):
- **Who am I** — an identity that is both a runtime key and a compile-time type.
- **Whom do I need** — the dependencies that provide my capabilities.
- **How long do I live** — which lifetime tier I belong to.
Classes talk only to interfaces and never care how an implementation is constructed.
## The four `LifecycleScope` tiers
Lifetimes form a tree, from longest to shortest:
```text
App process-wide, single global instance
└── Workspace one workspace handler (a materialized workspace root)
└── Session one session
└── Agent one agent
```
```ts
// src/app/scopes.ts — the business layer declares the tiers and their order;
// the DI kernel only knows opaque string kinds plus the declared topology.
export enum LifecycleScope {
App = 'app',
Workspace = 'workspace',
Session = 'session',
Agent = 'agent',
}
```
- Later in the topology = shorter life = closer to a leaf.
- "Singleton" means **one per scope**: `ILogService` is global once; each `Session` scope has its own `ISessionMetadata`.
- `kind` must advance along the declared topology in the parent→child direction.
### Visibility rule
A child scope sees its ancestors; a parent never sees its children. Resolution walks *up* the tree:
- ✅ An `Agent` service injects a `Session` or `App` service (found upward).
- ❌ An `App` service injects a `Session` service (the parent does not look down, and the child may not exist yet).
> **Short-lived may inject long-lived; never the reverse.** The tree structure enforces this — it is not a matter of discipline.
### Disposal order
Deterministic: **child scopes die first; within one scope, teardown runs in strict reverse registration order, one entry at a time.** The mechanism is the Ledger (`src/_base/lifecycle/`): ordered effect bookkeeping, dual-track (sync + async disposers), serial reverse-order teardown (never parallel), with the teardown reason (`'scope-close' | 'cascade' | 'unload'`) passed through to every disposer. `Disposable` / `DisposableStore` (`src/_base/di/lifecycle.ts`) delegate to it — "reverse construction order" is a Ledger property, not a container convention. Business code declares which tier it lives in and never disposes by hand.
## Dynamic DI: units and cascades
Registration is not the end of the story. Every unit a container tracks — static registrations and runtime `provide`s alike — lives in a small state machine owned by the scope's cascade engine (`src/_base/di/cascadeEngine.ts`, one per scope container, orchestrating tree-wide). Vocabulary you will meet in errors, tests, and the debug surface:
- **Unit states** — `Pending → Activating → Active`, plus `Unloading` during teardown and a sticky `Failed`. A construction failure parks the unit in `Failed` with no auto-retry: resolving it rethrows its error; an explicit `update()` reloads it.
- **Waiting area** — a unit whose declared dependencies are missing sits `Pending` and auto-activates when they arrive, including cross-scope wake-up when an ancestor gains the token. An `ondemand` unit counts as available: consumers pull it transitively at materialization.
- **Cascade transaction** — every `provide` / `unprovide` / `update` runs as one tree-wide transaction: contagion set from the persistent dependency graph (instance edges, child→parent across scopes) → abort hook → global reverse-topo teardown → apply the change → waiting-area recheck fixpoint → history ring. Static bootstrap shares this path: scope creation submits the kind's whole registration batch as one `provideAll`, so registration order never matters.
## Import boundaries
There is no domain-layer numbering — a domain may import any other domain, guided by the dependency-direction judgment in design.md. The only mechanically enforced import boundaries are (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core` or any subpath).
- The kosong subtree (`src/kosong/{contract,protocol,provider,model}`) keeps its strict internal order (`contract ← protocol ← provider/model`), purity bans (no SDKs in `contract`/`protocol`), and the `provider/bases` registration boundary.
## Comment convention
`packages/agent-core-v2/AGENTS.md` bans comments entirely: no file headers, no section banners, no statement-level narration, no JSDoc (not even on exported symbols) — the code is the source of truth. The only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`) for a deliberate pattern; other tooling directives (`@ts-expect-error`, …) are banned: fix the underlying lint/type problem instead, and put negative type-safety cases in compiler-asserted fixtures. Scope is carried by the filename: `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md).
## Red lines (this stage)
- Import via the `#/...` alias (mapped to `src/`); never reach into another domain's internals by relative path.
- Short-lived may inject long-lived; never the reverse.
- No comments — not file headers, not beside statements, no JSDoc anywhere; `oxlint-disable` / `eslint-disable` are the only exception.

View file

@ -0,0 +1,213 @@
# Topic — Permission
The target design for the agent-core permission system. Read this when touching `permission`, `permissionMode`, `permissionRules`, or when adding a new permission dimension.
> **The permission system should be a composable, registrable chain of responsibility (a microkernel).** The kernel only runs the chain in order, first hit wins; concrete permission dimensions (policies) are contributed by their owning Domain Services through a registry; tools only declare standardized resource access (`accesses`) in `resolveExecution`, and generic dimensions consume that metadata.
>
> **The chain adjudicates risk only.** A policy node answers "how dangerous is this call, and may the user override that judgment?" — its `ask`/`deny` outcomes are always user-overridable. **Harness constraints are not permissions**: a mechanism that limits the agent for its own correctness (plan-mode write guard, AgentSwarm batch exclusivity, btw side-question fork, goal budget rejection) produces a hard deny with no ask channel and no per-call user exemption. Those live in their owning domains as `onBeforeExecuteTool` veto listeners that call `event.veto(...)` (precedent: `goalService.ts`'s budget/stale rejection). Product reviews (plan review, goal-start review) are likewise not permissions: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives the shared `IAgentToolApprovalService` round-trip itself, so the review only starts once no other listener vetoed the call.
>
> **Do not introduce Casbin** — the hard part here is *decision behavior* (continuations, side effects, RPC, state machines), not "match + scalar decision".
## 1. Problem definition
The permission system answers one question: **for each tool call, in the current agent and current mode — allow / deny / ask the user?** Three traits shape the architecture:
1. **Decisions carry behavior.** Returning `ask` is not an enum value — it is a workflow with an RPC round-trip, hooks, telemetry, state writes, and a continuation; returning `deny` may be the result of running an external hook.
2. **Heterogeneous policies.** Some check a tool-name set, some count same-batch `AgentSwarm` calls, some run a hook, some inspect the plan state machine — no uniform `(sub, obj, act)` shape.
3. **Multi-agent × multi-mode × external extension.** Different agents / modes need different permissions, and outsiders (org admins, plugins) must contribute rules or behavior in a decoupled way.
## 2. Current state (v1) at a glance
Code lives in `packages/agent-core/src/agent/permission/`.
- **Architecture: ordered chain of responsibility, first hit wins.** `PermissionManager` holds `PermissionPolicy[]`; evaluation iterates in order, the first non-`undefined` result wins.
- **`PermissionPolicyResult` is a behavior bundle, not a scalar:** `approve` (with `executionMetadata`), `deny` (with `message`), or `ask` (with `resolveApproval` / `resolveError` continuations).
- **11 dimensions, 19 policies**, hardcoded in `policies/index.ts#createPermissionDecisionPolicies()`. Order is a high-to-low safety cascade: external force → structural deny → state-machine deny → static deny → mode allow → session-memory allow → static ask → static allow → flow allow → sensitive-path ask → default allow → fallback ask.
- **Resource-access declaration:** tools declare accessed resources in `resolveExecution(input)` via `accesses` (`ToolAccesses`, currently `file` and `all`); generic dimensions read `context.execution.accesses`.
### v1 pain points the target design fixes
1. The chain is hardcoded — outsiders cannot contribute.
2. `mode` is an `if` inside each policy (`YoloModeApprove` / `AutoModeApprove` self-guard).
3. No per-agent chain entry point (only scattered `agent.type === 'sub'` checks).
4. No external extension point beyond the single `PreToolUse` hook slot.
## 3. Why not Casbin
- **`policy_effect` is unusable** — composition here is a fixed, intentionally hardcoded safety cascade; the real complexity lives in each policy's `evaluate` behavior, which a Casbin expression cannot absorb. Externally tunable safety knobs are already exposed via `mode` + allow/deny/ask rules.
- **Flexible priority is unusable** — there is no plugin injection point, no multi-subject/RBAC, and a fixed subject (agent/user), so priority collisions do not arise. Casbin's `(sub, obj, act)`, `g()`, and domains would idle.
- **Fundamental mismatch: decisions are not scalars.** `enforce()` maps a request to an effect; agent-core decisions are behavior bundles (continuations, side effects, synthesized results). Even if Casbin computed `ask`, the surrounding behavior would still need to be rewritten — Casbin would degrade to an enum generator.
- **When Casbin becomes worth it:** when the hard part is matching semantics itself — role inheritance, domain isolation, ABAC expressions, policies loaded from a DB. Not before.
## 4. Design-pattern placement
Permission orchestration is a layered combination, not a single pattern:
| Layer | Pattern | Role |
|---|---|---|
| Runtime decision | **Chain of Responsibility** | multiple candidates in order; first hit wins, rest short-circuit |
| Single handler | **Strategy** | each policy is an interchangeable "permission adjudication" algorithm |
| Assembly / external extension | **Plugin / Microkernel** | minimal kernel + explicit extension points + pluggable policies |
| Landing support | **Registry + Factory** | collect plugins; assemble the chain per `(agent, mode)` on demand |
Casbin = single Strategy + data-driven. This design = multiple Strategies + chain-of-responsibility composition. Behavior-heavy systems must choose the latter — behavior cannot be flattened into data rows.
## 5. Target design
### 5.1 Core principles
1. **The chain encodes "permission dimensions", not "tools".** Adding a tool does not lengthen the chain; only adding a dimension adds a node.
2. **Two contribution paths:** high-frequency trivial specifics go through the **data path** (rules); low-frequency new dimensions with behavior go through the **code path** (policies).
3. **Guard/review off-chain, risk on-chain:** harness constraints and product reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners (§5.4); risk dimensions contributed by a domain self-register as chain policies in DI, mirroring v2's "domain self-registers tools".
4. **Tools declare resources; generic dimensions consume them:** bash/write/read only declare `accesses`; file/security dimensions judge centrally.
### 5.2 Core abstractions
```ts
type Phase =
| 'guard' | 'user-deny' | 'mode' | 'session'
| 'user-ask' | 'default' | 'fallback';
interface PermissionPolicyEntry {
name: string;
phase: Phase;
modes?: PermissionMode[]; // declare which modes this applies in (no more in-evaluate if)
agentTypes?: AgentType[];
factory: (accessor: ServicesAccessor) => PermissionPolicy;
}
// App scope — collects every domain's registration
interface IPermissionPolicyRegistry {
register(entry: PermissionPolicyEntry): IDisposable;
list(): readonly PermissionPolicyEntry[];
}
```
`PermissionPolicyService` (Agent scope) changes from a hardcoded list to "assemble by `(agent, mode)`":
```ts
this.policies = registry.list()
.filter(e => !e.modes || e.modes.includes(mode))
.filter(e => !e.agentTypes || e.agentTypes.includes(agentType))
.sort(byPhaseThenRegistrationOrder)
.map(e => e.factory(accessor));
```
Key points:
- `modes` / `agentTypes` are **declarations** — they lift the `if (mode !== 'yolo') return` out of `YoloModeApprove` into metadata.
- `factory`, not `instance`: a node may depend on agent-scoped services (mode, rules) and must be instantiated in the Agent scope — symmetric to `IToolDefinitionRegistry` (App) storing factories and `IToolService` (Agent) instantiating tools.
- **Different `(agent, mode)` produce differently-shaped chains** — under yolo the ask/fallback phases are physically filtered out.
### 5.3 Two contribution paths
| What is being added | Path | Chain length |
|---|---|---|
| New tool, new org rule, new user preference ("deny `Bash(curl *)`") | **Data path**: add a `PermissionRule` to an existing node | unchanged |
| New cross-cutting behavior (custom approval UI, audit log, new mode) | **Code path**: register a new policy node | +1 |
Most growth goes through the data path — node count is bounded by "kinds of behavior"; rule count grows with specifics (rule matching is a cheap Set/glob).
### 5.4 Domain dimensions: guard/review via the executor veto event, policy registration for risk
**Harness constraints and product reviews no longer live on the chain.** A domain that owns one registers an `onBeforeExecuteTool` veto listener and adjudicates through the event:
```ts
// src/plan/planService.ts — constructor
constructor(@IAgentToolExecutorService executor, ...) {
executor.onBeforeExecuteTool((event) => this.guardToolExecution(event));
}
```
- The veto event carries no id and no ordering contract. Listeners answer with `event.veto(result)` (first one wins, ends adjudication), `event.allow()` (final pass, ends everything including the permission gate's own listener), `event.pass(metadata)` (pass with an `executionMetadata` trace, ends nothing), or `event.waitUntil(factory)` (defer to a cold factory).
- **Guard** (hard deny): call `event.veto(denyToolExecution(toolApproval.formatDenyMessage(...)))`. An immediate veto suppresses every pending `waitUntil` factory, so a deny can never be preceded by someone else's approval prompt.
- **Review** (product approval): intercept the tool with `event.waitUntil(() => ...requestToolApproval(event, ask, origin))`. The factory is cold — the executor only invokes it after every listener ran without a veto or an allow, so the review's Interaction starts only once the call is otherwise clear to proceed; abstain (no statement) for every case you do not review so user rules still apply.
- **Plain allow**: do NOT `allow()` casually — prefer putting the tool in `default-tool-approve`'s whitelist so user deny/ask rules keep their precedence; reserve `allow()` for cases like the plan-file write guard that must bypass even the permission chain.
**Risk dimensions contributed by a domain still go through the chain** (the registry path below): a domain whose state changes the *risk* verdict registers its policy via `IPermissionPolicyRegistry`, mirroring v2's "domain self-registers tools". A complex domain may register a single **composite** node externally and run a small internal chain, hiding its internal order from the global chain.
### 5.5 Tools declare resources at runtime (`resolveExecution` / `accesses`)
In `resolveExecution(input)`, before execution, declare accessed resources with the `ToolAccesses.*` builders:
```ts
resolveExecution(args: WriteInput): ToolExecution {
const path = resolvePathAccessPath(args.path, { kaos, workspace, operation: 'write' });
return {
accesses: ToolAccesses.writeFile(path), // declares: write this file
approvalRule: literalRulePattern(this.name, path),
matchesRule: (ruleArgs) => matchesPathRuleSubject(ruleArgs, path, ...),
execute: () => this.execution(args, path),
};
}
```
Current resource types:
```ts
type ToolResourceAccess =
| { kind: 'file'; operation: 'read'|'write'|'readwrite'|'search'; path: string; recursive?: boolean }
| { kind: 'all' }; // non-enumerable side effects (pessimistic, globally exclusive)
```
Two complementary channels:
- **Enumerable resources** (write/read/edit/grep/glob) → use `accesses`; generic file dimensions cover them automatically.
- **Non-enumerable resources** (bash running arbitrary commands) → do not declare `accesses`; use the `matchesRule` DSL (e.g. `Bash(rm *)` globs by command string).
**kaos's role:** kaos is the execution-environment abstraction (fs/process/pathClass) used by the file dimension for path normalization and judgment — it is **not** the permission-dimension abstraction itself. Permission semantics live one layer above kaos, at "file access".
**v2 evolution:** extend the `ToolResourceAccess` union so non-file resources can be declared structurally:
```ts
type ToolResourceAccess =
| { kind: 'file'; operation: FileOp; path: string; recursive?: boolean }
| { kind: 'network'; operation: 'connect'; host: string }
| { kind: 'shell'; command: string }
| { kind: 'datastore'; operation: 'read'|'write'; table: string }
| { kind: 'all' };
```
Each new resource kind can pair with a generic dimension that consumes it; tools always only **declare**.
### 5.6 Dimension ownership
| Dimension | Owner | Type |
|---|---|---|
| external hook veto | `externalHooks` domain | generic |
| tool-batch exclusivity | `swarm` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| plan-mode write guard | `plan` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| plan review | `plan` domain — same listener's `waitUntil` + `toolApproval` | product review (off-chain) |
| goal-start review | `goal` domain — veto listener's `waitUntil` + `toolApproval` | product review (off-chain) |
| goal budget / stale rejection | `goal` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| btw tool disablement | `btw` domain — veto listener on the fork | harness constraint (off-chain) |
| runtime-mode posture (auto/yolo) | `permissionMode` domain (chain nodes, pending the level×routing split) | generic |
| static config rules | `permissionRules` domain | generic (data path) |
| session approval memory | `permissionRules` domain | generic |
| sensitive / special paths | generic "file-access/security" dimension | generic (consumes `accesses`) |
| tool intrinsic risk | core permission (`default-tool-approve`) | generic (consumes tool declarations) |
| workspace write trust | generic "file-access/security" dimension | generic (consumes `accesses`) |
| fallback | core permission | generic |
| approval round-trip | `toolApproval` domain — shared by gate asks and domain reviews | infrastructure |
Pattern: **harness constraints and reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners; risk dimensions ship as chain policies (self-registered once the registry lands); generic dimensions register centrally and apply across tools via the declared `accesses`.**
## 6. Evolution path
Incremental, not big-bang:
1. ~~**Sink domain dimensions.**~~ **Done** — plan guard/review, goal-start review, swarm batch exclusivity, and btw deny-all moved out of the chain into their owning domains as `onBeforeExecuteTool` veto listeners (immediate `veto` / `allow` / `pass` statements plus cold `waitUntil` factories for approval round-trips); the shared approval round-trip was extracted to `IAgentToolApprovalService`; `registerPolicy` was removed (btw was its only production user). The chain now holds 12 risk-adjudication nodes only.
2. **Level × routing split.** Separate "risk level" (read-only / read-write / yolo posture — what `yolo-mode-approve` really is) from "interaction routing" (what `auto-mode-approve` / `auto-mode-ask-user-question-deny` really are: route permission asks and reviews without the user). The routing layer lands on the `session/approval` broker; the three remaining mode policies leave the chain here.
3. **Registry + Composer.** Replace the hardcoded `new`s in `PermissionPolicyService` with reads from `IPermissionPolicyRegistry`; lift mode guards into `modes` metadata. Chain shape becomes selectable per `(agent, mode)` and externally extensible.
4. **(On demand) extend resource types.** When non-file resources (network/DB/shell) need structural dimensions, extend the `ToolResourceAccess` union.
5. **(On demand) swap the matching kernel for Casbin.** Only when external rules genuinely need RBAC/ABAC semantics, swap the data-path rule-matching kernel for Casbin. Not before.
## Red lines (this topic)
- Do not introduce Casbin — decisions are behavior bundles, not scalar effects.
- The chain adjudicates risk only. A node whose deny/ask the user cannot per-call exempt is a harness constraint: implement it as an `onBeforeExecuteTool` veto listener in the owning domain (`event.veto(...)` / `event.allow()`), never as a chain policy.
- Product reviews (plan/goal) are not permissions either: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives `IAgentToolApprovalService` itself; the gate only handles chain asks.
- The chain encodes dimensions, not tools: a new tool must not lengthen the chain.
- New specifics go through the data path (rules); only new risk behavior goes through the code path (a policy node).
- Tools only declare `accesses`; generic dimensions consume them. kaos is the execution environment, not the permission abstraction.
- Use `factory` (Agent-scope instantiation), not `instance`, for registered policies.

View file

@ -0,0 +1,204 @@
# Topic — Persistence layering
How business code persists data in `agent-core-v2`: the three-layer model (`Store → Storage → backend`), the naming rules for each layer, and how to decide which layer a domain should depend on. Read this before adding any persistence to a domain.
A domain `I{Domain}EntityService` is a business facade over these layers, not a replacement for them. Before naming or bundling EntityServices by `session` / `agent` / `turn`, read [domain-boundaries.md](domain-boundaries.md).
## The three-layer model
Persistence is split into three layers, each hiding one kind of change:
```text
Business Service
│ inject
▼
┌────────────────────────────────────────┐
│ Store (semantic layer) │ ← access-pattern facade
│ IAppendLogStore / IAtomicDocumentStore│ append-log / atomic-doc / blob
└────────────────────────────────────────┘
│ inject
▼
┌────────────────────────────────────────┐
│ Storage (byte layer) │ ← byte primitives
│ IFileSystemStorageService │ read/write/append/list/delete
└────────────────────────────────────────┘
│ implements
▼
┌────────────────────────────────────────┐
│ Backend (deployment-specific) │ ← File / Postgres / Redis / S3
│ FileStorageService / PostgresStorage │
└────────────────────────────────────────┘
│ uses
▼
┌────────────────────────────────────────┐
│ Platform primitives │ ← hostFs / dbClient / redisClient
└────────────────────────────────────────┘
```
Each layer hides exactly one concern:
| Layer | Hides | Business code sees |
|---|---|---|
| **Store** | how an access pattern works (append-log reads, atomic-doc serialization) | "append this record" / "save this document" |
| **Storage** | byte primitives (atomic write, ordered append, prefix list) | `read/write/append/list/delete` over `(scope, key)` |
| **Backend** | deployment environment (file vs DB vs Redis vs S3) | nothing — chosen at the composition root |
## The one-sentence rule
> **Business code expresses *what* to store or fetch, never *how* to store it.**
If business code contains any "how to persist" detail, it has punched through the layer it should depend on:
| Business code contains | It has punched through | Depend on instead |
|---|---|---|
| `INSERT INTO …` / `SELECT …` | Storage + backend | a Store |
| file paths / `rename` / `fsync` | Storage | Storage or a Store |
| `JSON.parse` / `JSON.stringify` | Store (serialization) | `IAtomicDocumentStore` |
| append offsets / sequential cursors | Store (log semantics) | `IAppendLogStore` |
| `hash(data)` used as a key | Store (blob semantics) | `IBlobStore` |
| `pathe.join / relative / basename` on `homeDir` etc. | Bootstrap (path layout) | `IBootstrapService.scope(...)` / scope contexts |
| only `read/write/list/delete` on bytes | nothing — this is the byte layer | `IFileSystemStorageService` directly ✅ |
## Where scopes come from — `IBootstrapService` and scope contexts
Business code **never assembles scope strings from paths**. Scope strings come from three places:
1. **`IBootstrapService.scope(name)`** — well-known top-level scopes (`'config' | 'sessions' | 'blobs' | 'store' | 'logs' | 'cache' | 'credentials'`). App-scope, deployment-agnostic contract.
2. **`ISessionContext.scope(subKey?)`** — persistence scope rooted at the current session; `scope('agents/main')` etc.
3. **`IAgentScopeContext.scope(subKey?)`** — persistence scope rooted at the current agent; `scope('cron')`, `scope('blobs')` etc.
The bootstrap layer decides how each semantic scope maps to concrete addressing. In the file deployment, `FileBootstrapService` reads a `ResolvedEnvironment` (the paths bag) and returns homeDir-relative scopes; a server deployment could bind a different `IBootstrapService` implementation that maps `'sessions'` to a DB table without any business change.
```ts
// ❌ Wrong — path arithmetic on homeDir/sessionDir leaks the file layout
const scope = relative(bootstrap.homeDir, join(session.sessionDir, 'agents', agentId, 'cron'));
// ✅ Right — the agent already knows its own scope root
const scope = agentCtx.scope('cron');
```
Absolute paths (`sessionDir`, `agentHomedir`) are still available on `IBootstrapService` for the very small number of legacy APIs that expose on-disk paths (session log rotation, background task tail file). Prefer scope strings; ask before adding a new absolute-path caller.
## Which layer to depend on — decision tree
```text
Need to persist
│
├─ read-whole / write-whole, JSON-serializable?
│ └─ IAtomicDocumentStore
│
├─ append-only writes / sequential reads, independent records?
│ └─ IAppendLogStore
│
├─ large object, addressed by content hash?
│ └─ IBlobStore
│
├─ custom byte layout (index / cache / binary) that read/write/list cover?
│ └─ IFileSystemStorageService directly
│
├─ new, reusable access semantics (multi-field query / time-range / graph)?
│ └─ add a new Store; business depends on the Store
│
└─ business-specific, trivial, one or two lines?
└─ IFileSystemStorageService directly; if it grows, extract a private Store
```
## Naming — Store by access pattern, not by business
A Store abstracts an **access pattern**, not a business data type. Name it after the pattern so its reusability is obvious from the name.
| Access pattern | Store name | Backend examples |
|---|---|---|
| append-log (append / sequential read) | `IAppendLogStore` | `FileAppendLogStore` / `PostgresAppendLogStore` |
| atomic-document (read/write whole) | `IAtomicDocumentStore` | `FileDocumentStore` / `RedisDocumentStore` |
| blob (hash-addressed large object) | `IBlobStore` | `FileBlobStore` / `S3BlobStore` |
**Do not name a generic Store after a business concept.** `IRecordStore` / `IConfigStore` make a reusable access pattern look like a private store for one feature. Any domain that needs an append-log uses `IAppendLogStore`; any domain that needs an atomic document uses `IAtomicDocumentStore`.
**Exception — business-specific Stores are named after the business.** When a Store captures one domain's unique query semantics (not a generic access pattern), name it after the domain:
```text
ISessionIndex query / enumerate sessions by workspace ← business-specific
```
Test: is the Store's semantics a *generic access pattern* (append-log / atomic-doc / blob) or *one domain's unique query*? Generic → name by pattern; unique → name by domain.
## Storage — a filesystem-specific byte layer
The byte layer is a single `IFileSystemStorageService` interface (read / readStream / write / append / list / delete / watch / flush / close). As the name says, it is **filesystem-specific**: it exposes the two irreducible durable primitives a local filesystem implements optimally — atomic whole-value replacement (`write`, via tmp + rename) and ordered durable extension (`append`, via `open('a')`). The node-fs Store backends (`AppendLogStore`, `JsonAtomicDocumentStore`, `BlobStoreService`) are built on it.
```ts
export interface IFileSystemStorageService {
read(scope: string, key: string): Promise<Uint8Array | undefined>;
readStream(scope: string, key: string): AsyncIterable<Uint8Array>;
write(scope: string, key: string, data: Uint8Array, options?: { atomic?: boolean }): Promise<void>;
append(scope: string, key: string, data: Uint8Array, options?: { durable?: boolean }): Promise<void>;
list(scope: string, prefix?: string): Promise<readonly string[]>;
delete(scope: string, key: string): Promise<void>;
watch?(scope: string, key: string): Event<void>;
flush(): Promise<void>;
close(): Promise<void>;
}
```
Two backends implement it today, both bound at the composition root:
```ts
// Production — local filesystem rooted at homeDir
collection.set(IFileSystemStorageService, new FileStorageService(homeDir));
// Tests — in-memory backend seeded by the test harness
collection.set(IFileSystemStorageService, new InMemoryStorageService());
```
**Non-filesystem backends (Postgres, S3, Redis) do not implement this interface.** Atomic-rename and byte-append have no native equivalent in those stores, so they implement the **Store** interfaces directly via their own clients instead:
```ts
// Server profile — append-logs on Postgres, atomic documents on Redis.
// Each Store is backed by a native client; IFileSystemStorageService is not involved.
collection.set(IAppendLogStore, new PostgresAppendLogStore(db, 'records'));
collection.set(IAtomicDocumentStore, new RedisDocumentStore(redis, 'config'));
```
Use the `scope` parameter to express **business namespace** within a backend. Do not overload `scope` to route backends — bind a different Store implementation at the composition root instead.
## Store `acquire(scope, key)` — flush-on-dispose handle
Stores that buffer writes expose an `acquire(scope, key)` handle so a business can flush them on disposal:
```ts
export interface IAppendLogStore {
// …
/**
* Acquire a disposable handle for `(scope, key)`. Register it with your
* `Disposable` (via `this._register(...)`); when you are disposed, pending
* appends for that log are flushed. The shared store itself is not disposed.
*/
acquire(scope: string, key: string): IDisposable;
}
```
`IAppendLogStore.acquire` flushes the log's pending appends on dispose — it exists because `append` is fire-and-forget. `IAtomicDocumentStore.acquire` is a no-op today (atomic documents are durable on write) and exists for interface symmetry. Businesses that do not need flush-on-dispose simply do not call `acquire`.
## When the byte layer does not apply
`IFileSystemStorageService` covers only the local-filesystem byte primitives. It is not a universal storage abstraction:
- **Non-filesystem backends** (Postgres / S3 / Redis) implement the **Store** interfaces directly via native clients — they never implement `IFileSystemStorageService`.
- **Blobs** are a Store-level interface (`IBlobStore`) with their own backends; the node-fs `BlobStoreService` sits on `IFileSystemStorageService`, but an `S3BlobStore` would not.
- **A backend has a fast primitive the Store interface cannot express** (e.g. Postgres `COPY`) → as an exception, extend that backend's Store implementation directly. This is an exception, not the default.
## Platform primitives are deployment-coupled, not core abstractions
`hostFs` (local filesystem) is a **platform primitive** used only by local backends (`FileStorageService`, `LocalFileSystemBackend`, `LocalSkillCatalog`, `HostFolderBrowser`). It is **not** a core abstraction and must not appear in business-domain dependency graphs. A server deployment swaps those backends for DB / S3 implementations and never registers `hostFs`.
## Red lines (this topic)
- Business code never contains "how to persist" details (serialization / paths / SQL / append offsets) — if it does, drop a layer.
- Business code never assembles scope strings from paths (`pathe.join / relative / basename` on `homeDir` / `sessionDir` / …). Use `IBootstrapService.scope(name)` for well-known scopes, `ISessionContext.scope(subKey?)` for session-rooted scopes, and `IAgentScopeContext.scope(subKey?)` for agent-rooted scopes.
- Name generic Stores by access pattern (`IAppendLogStore` / `IAtomicDocumentStore` / `IBlobStore`), never by business concept (`IRecordStore` / `IConfigStore`).
- Business-specific Stores (unique query semantics) are named after the domain (`ISessionIndex`).
- `IFileSystemStorageService` is the filesystem byte-layer interface; non-filesystem backends implement the **Store** interfaces directly. Route backends by binding a different Store implementation at the composition root, not by overloading `scope`.
- `hostFs` is a local-only platform primitive; business domains must not import `node:fs` or `hostFs` directly.
- Only the file-backed bootstrap (`FileBootstrapService`) and file backends import `pathe`; business domains do not.
- Do not create a pass-through `Store` that only forwards `read/write` — a Store must hide a real access-pattern concern, or it is noise; use `IFileSystemStorageService` directly instead.

View file

@ -0,0 +1,253 @@
# Subskill — Server align (expose `agent-core-v2` over `server-v2`)
Wire a v2 domain into `packages/kap-server`, and — when the endpoint is part of the established `/api/v1` wire contract — keep the wire shape **byte-for-byte compatible** with what released v1 clients expect. This is the server-side counterpart of [align.md](align.md): `align.md` ports v1 *business logic* into v2; this file exposes the v2 result over HTTP / WS, reusing the v1 wire contract where it already exists.
Use this when the task is "expose the new v2 Service on the server", "add a `/sessions/:sid/...` route to the `/api/v1` surface", or "keep server-v2 speaking the same `/api/v1` contract released clients rely on".
## The one-paragraph mental model
`server-v2` serves **two HTTP surfaces** off the same `agent-core-v2` scope tree:
- **`/api/v2/:sa`** — the native v2 RPC surface, driven by the `actionMap` allowlist (`packages/kap-server/src/transport/actionMap.ts`). One `resource:action` segment maps to one `Service.method`. New v2-native capabilities land here. See [edge-exposure.md](edge-exposure.md).
- **`/api/v1/...`** — the v1-compatible surface, hand-written routes in `packages/kap-server/src/routes/*.ts` that **implement the established v1 wire contract path-for-path and schema-for-schema**, mounted by `registerApiV1Routes.ts`. This surface IS the v1 contract now (the legacy v1 server is gone); it exists so existing v1 clients keep working against server-v2 unchanged.
The two surfaces can point at **different Services** for the same feature. v2's native `IAgentPromptService` serves `/api/v2`; a v1-shaped `IAgentPromptService` serves `/api/v1`. Keeping them separate is what lets v2's domain design stay clean while the wire stays compatible.
## Decision: which surface?
```text
Is the endpoint part of the established /api/v1 wire contract (protocol schema
+ released-client expectation)?
├─ YES → /api/v1 mirror route (this file, §schema-fidelity + §legacy-service).
│ Reuse the protocol schema; add a LegacyService if v2 semantics diverge.
└─ NO → /api/v2 native action (edge-exposure.md).
Add to actionMap, wrapping in a facade if the method fails §2 there.
```
A feature often needs **both**: the v1 mirror so old clients keep working, and the v2 action so new clients get the cleaner shape. Do them as two routes / two action-map entries over the same scope tree.
## The server-align workflow
```text
Pick surface → Read the v1 route (if any) → Reuse / add the protocol schema
→ Choose native Service vs LegacyService → Wire the route / actionMap entry
→ Map errors → Test against the v1 wire shape → Verify
```
### 1. Pick the surface
Apply the decision above. For a v1-matched endpoint, the **spec** is the protocol schema plus the existing mirror routes:
- `packages/kap-server/src/protocol/rest-<resource>.ts` — the wire schema you must match.
- `packages/kap-server/src/routes/<resource>.ts` — the file you are writing (create it if missing); sibling route files show the conventions.
The protocol schema is the source of truth. Do not re-derive the wire shape from memory or from the v2 domain model.
### 2. Reuse (or add) the protocol schema
The wire schema lives in **`packages/kap-server/src/protocol`** under `rest-<resource>.ts` (e.g. `promptSubmissionSchema`, `promptListResponseSchema`, `configResponseSchema`) — or in the owning `agent-core-v2` domain contract when the engine's service speaks the shape. Every `/api/v1` route in `packages/kap-server` imports from it — that single import is what guarantees the server speaks the same shape released clients expect.
Actions:
- **Schema already in protocol** → import it in the server-v2 route and use it in `defineRoute` (`body`, `success.data`, error `dataSchema` / `detailsSchema`). Do **not** re-declare the schema inline in server-v2.
- **Schema missing** → add it to `packages/kap-server/src/protocol/rest-<resource>.ts` first (or to the owning v2 domain contract if its service speaks the shape), then consume it from the route. The shared schema is the source of truth; server-v2 never re-declares a v1 wire schema inline.
- **Schema exists but only v1 uses it** → keep it in `packages/kap-server/src/protocol` and import it into server-v2; do not fork a copy.
#### Schema-fidelity rule (the hard rule)
For a `/api/v1` endpoint, the request and response schemas **must be the established protocol schema** (or a strict superset):
- ✅ **Adding** an optional field is allowed (`field: z.string().optional()`). Old clients ignore it; new clients may send it.
- ❌ **Renaming** a field, **changing** its type, **tightening** its validation, or **changing its meaning** is a wire break — do not do it in a mirror route. If the v2 domain genuinely needs a different shape, that shape belongs on `/api/v2`, not on the `/api/v1` mirror.
- ❌ Re-declaring the schema inline in server-v2 (even if it "looks identical") is forbidden — it drifts. One schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`.
Self-check: "would a released v1 client get a byte-identical envelope from `packages/kap-server` for this request?" If you cannot answer yes from the shared schema, the route is wrong.
### 3. Choose native Service vs LegacyService
Resolve the v2 Service that will back the route. Two cases:
**Case A — the v2 native Service already matches the v1 contract.** Use it directly. Most data/command Services (`IConfigService`, `IWorkspaceService`, `IApprovalService`, `IQuestionService`, `IFileStore`, …) land here: the route is a thin adapter that resolves the scope, calls the method, and wraps the result. Examples: `routes/config.ts`, `routes/messages.ts`, `routes/questions.ts`, `routes/files.ts`.
**Case B — the v1 contract needs behavior that would distort the v2 domain.** Introduce a **`*LegacyService`** — an edge adapter that implements the v1 contract **on top of** the v2 native Service, leaving the native Service untouched. The v2 native Service keeps serving `/api/v2`; the LegacyService serves `/api/v1`.
Reach for a LegacyService when **any** hold:
- The v1 endpoint carries state the v2 domain deliberately dropped (e.g. a FIFO queue, a `prompt_id`, idempotent `abort`/`steer`, auto-start-next).
- The v1 method returns a handle/stream that v2 wraps differently, and the v1 clients expect the old envelope shape.
- Matching v1 would force a `Map<sessionId, …>`-at-`App` anti-pattern or a scope/domain-direction violation into the native Service (see [align.md](align.md) red lines).
- The native Service's error set / return type would have to grow v1-only branches.
Do **not** put v1 quirks into the native v2 Service "to keep the route simple". That is the conflict this rule exists to prevent: the native Service serves the v2 architecture; the LegacyService serves the wire contract.
#### LegacyService recipe
A LegacyService is a normal v2 Service (service-authoring.md) with one extra convention: its contract is shaped by the **protocol** types, not by the v2 domain model.
```text
packages/agent-core-v2/src/<domain>Legacy/
├── <domain>Legacy.ts ← contract: protocol-typed interface + decorator
├── <domain>LegacyService.ts ← impl: delegates to the native v2 Service(s)
└── errors.ts ← v1-compatible error codes (KimiError codes)
```
Skeleton (matches `prompt/`):
```ts
// prompt.ts — contract shaped by the v1 wire schema (kap-server/src/protocol)
import type { PromptSubmitResult, PromptSubmission } from '../../protocol/rest-prompt';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IAgentPromptService {
readonly _serviceBrand: undefined;
submit(body: PromptSubmission): Promise<PromptSubmitResult>;
// ...the rest of the v1 contract, typed by protocol
}
export const IAgentPromptService: ServiceIdentifier<IAgentPromptService> =
createDecorator<IAgentPromptService>('agentPromptLegacyService');
```
```ts
// promptService.ts — impl delegates to the native v2 Service
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
constructor(@IAgentPromptService private readonly prompt: IAgentPromptService /*, ... */) {}
// submit() builds v2-native input, calls the native Service, projects the result
// back into the protocol PromptSubmitResult.
registerScopedService(
LifecycleScope.Agent, // scope = the lifetime of the legacy state
IAgentPromptService,
AgentPromptLegacyService,
ScopeActivation.OnDemand,
'prompt',
);
```
Conventions:
- **Name** the domain `<domain>Legacy` and the interface with the scope prefix, `I<Scope><Domain>LegacyService` (e.g. `prompt` / `IAgentPromptService`), per service-authoring.md.
- **Role is carried by the name** — `<domain>Legacy` marks it as an `edge adapter`; the v1 contract it implements and the native v2 Service it leaves untouched stay evident from its delegation targets (see `prompt.ts`).
- **Scope** = the lifetime of the *legacy* state it holds (the `prompt` queue is per-agent → `LifecycleScope.Agent`). Apply [orient.md](orient.md) / [design.md](design.md) normally — a LegacyService is not exempt from scope rules.
- **Delegate, do not duplicate** business logic. The LegacyService translates the v1 contract into native-Service calls and translates results back; the real work stays in the native Service.
- **Contract types come from the v1 wire schema homes** (the owning v2 domain contract or `kap-server/src/protocol`), so the interface cannot drift from the wire shape.
### 4. Wire the route / actionMap entry
**For `/api/v1` (mirror):** add a route file under `packages/kap-server/src/routes/<resource>.ts` using `defineRoute`, then register it in `registerApiV1Routes.ts`. Resolve the scope from the URL (`session_id` → Session scope, agent → Agent scope via `IAgentLifecycleService.getHandle`), then `accessor.get(IX)` the native or Legacy Service. Match the established verbs, paths (`:sid` / `{session_id}`), and `parseActionSuffix` actions (`:steer`, `:abort`) exactly — sibling routes under `packages/kap-server/src/routes/` are the reference.
```ts
const route = defineRoute(
{
method: 'POST',
path: '/sessions/{session_id}/prompts',
body: promptSubmissionSchema, // ← from kap-server/src/protocol
params: sessionIdParamSchema,
success: { data: promptSubmitResultSchema }, // ← from kap-server/src/protocol
errors: {
[ErrorCode.SESSION_NOT_FOUND]: {},
[ErrorCode.SESSION_BUSY]: {},
[ErrorCode.PROMPT_ALREADY_COMPLETED]: { dataSchema: z.object({ aborted: z.literal(false) }) },
},
operationId: 'submitPrompt',
tags: ['prompts'],
},
async (req, reply) => {
try {
const result = await resolveLegacy(core, req.params.session_id).submit(req.body);
reply.send(okEnvelope(result, req.id));
} catch (error) {
sendMappedError(reply, req.id, error);
}
},
);
app.post(route.path, route.options, route.handler);
```
**For `/api/v2` (native):** add a `resource:action` entry to `actionMap` ([edge-exposure.md](edge-exposure.md) §3). If the method fails the direct-exposure rules (returns a handle / stream / bytes, takes a live object), add a wire-safe orchestration method to the owning domain Service first — as `prompts:submit` maps to `IAgentPromptService.submit`, which settles `{turn_id}` engine-side instead of returning the live `PromptHandle`.
### 5. Map errors
The route translates domain `KimiError` codes into protocol `ErrorCode` numbers. Two registries must stay in sync:
- **Domain code** — register in `agent-core-v2/src/errors.ts` (`ErrorCodes`) and throw from the Service (errors.md). Co-located domain errors go in `<domain>Legacy/errors.ts` (e.g. `prompt.not_found`, `session.busy`).
- **Wire code** — register the matching number in `packages/kap-server/src/protocol/error-codes.ts` and reference it in the route's `errors` map and `sendMappedError`.
```ts
function sendMappedError(reply, requestId, err) {
if (isKimiError(err)) {
switch (err.code) {
case 'session.not_found':
case 'agent.not_found':
return reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
case 'prompt.not_found':
return reply.send(errEnvelope(ErrorCode.PROMPT_NOT_FOUND, err.message, requestId));
// ...
}
}
return reply.send(errEnvelope(ErrorCode.INTERNAL_ERROR, String(err), requestId));
}
```
Match the v1 route's status codes and idempotent-conflict envelopes (e.g. `prompt.already_completed` → `40903` with `{ data: { aborted: false } }`). The error envelope is part of the wire contract — it is covered by the same schema-fidelity rule.
### 6. Test against the v1 wire shape
Add a `packages/kap-server/test/<resource>.test.ts` that boots the server and hits the route. Assert on the **envelope + protocol shape**, not on the v2 domain internals:
- success envelope `{ code: 0, data: <protocol shape>, request_id }`;
- each declared error envelope `{ code: <ErrorCode>, msg, data, request_id }`;
- the fields v1 clients read are present with the same names/types.
Where the route mirrors v1, the test is the regression guard for the schema-fidelity rule: if someone drifts the protocol schema or the projection, this test breaks.
### 7. Verify
- `pnpm -C packages/kap-server test` — server routes green.
- `pnpm -C packages/kap-server test` — server routes green (incl. any wire-schema guards).
- `pnpm -C packages/agent-core-v2 test` — native + Legacy Service tests green.
- `pnpm -C packages/agent-core-v2 run lint:imports` — the import boundaries (v1 ban, kosong subtree) still hold for a LegacyService.
- `pnpm -C packages/klient test` (optionally with `KIMI_SERVER_URL` for the live legacy suites) when a v1 parity scenario exists.
## Worked example — porting v1 `/sessions/:sid/prompts`
This is the reference alignment (commits `feat(server-v2): port v1 /sessions/:sid/prompts routes`, `feat(server-v2): return turn ids for prompt actions`). It shows all three decisions at once.
**The mismatch.** v1 `IPromptService` is a per-agent *scheduler*: it owns a FIFO queue, assigns `prompt_id`s, supports `steer`/`abort`, and auto-starts the next queued prompt when a turn settles. v2's native `IAgentPromptService` is a *turn driver*: a submission *is* a turn, there is no queue and no `prompt_id`. Forcing the queue into the v2 native Service would distort the v2 domain.
**The split.**
- `/api/v2` keeps the native shape — `prompts:submit` / `steer` / `undo` / `clear` / `cancel` map to the domain Services (`IAgentPromptService.submit` / `submitSteer`, `IAgentConversationUndoService.undo`, `IAgentLoopService.cancelFromUser`) in `actionMap`.
- `/api/v1` gets an `AgentPromptLegacyService` (`prompt/`, `LifecycleScope.Agent`) that re-implements the v1 scheduler — queue, `prompt_id`, steer/abort, auto-start-next — **on top of** the native `IAgentPromptService`. The `/api/v1` routes consume the LegacyService.
**The schema.** Both surfaces import `promptSubmissionSchema` / `promptSubmitResultSchema` / `promptListResponseSchema` / `promptSteerRequestSchema` / `promptSteerResultSchema` / `promptAbortResponseSchema` from the shared v1 wire schemas (see `packages/kap-server/src/protocol`). The `/api/v1` and `/api/v2` routes are therefore compatible with released clients by construction; the LegacyService projects v2 turn results back into those protocol shapes.
**The errors.** v1 codes (`prompt.not_found`, `session.busy`, `prompt.already_completed`) are registered in `agent-core-v2` (`prompt/errors.ts`) and in `packages/kap-server/src/protocol` (`error-codes.ts`), then mapped in the route's `sendMappedError` — including the idempotent `prompt.already_completed` → `40903 { data: { aborted: false } }`.
**The lesson.** When the v1 contract and the v2 domain disagree, add an adapter (LegacyService) at the edge; do not let the wire contract leak into the native domain. The two surfaces share the protocol schema but not the Service.
## Migration checklist
Before submitting a server-align change:
- [ ] Surface chosen deliberately: `/api/v1` mirror for a v1-matched endpoint, `/api/v2` for a new native capability (both if needed).
- [ ] For a `/api/v1` mirror, the route matches the established v1 contract (protocol schema + sibling routes) path-for-path, verb-for-verb, action-for-action.
- [ ] Request and response schemas come from their owning home (the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`); no inline re-declaration in server-v2.
- [ ] Existing schema fields are unchanged in name, type, and semantics; only optional fields added (if any).
- [ ] Native v2 Service left clean; v1-only behavior isolated in a `<domain>Legacy` / `I<Domain>LegacyService` edge adapter when the semantics diverge.
- [ ] LegacyService registered with the correct `LifecycleScope` and named as the `<domain>Legacy` edge adapter preserving the native Service.
- [ ] Domain error codes registered in `agent-core-v2`; wire codes registered in `packages/kap-server/src/protocol`; route maps them in `sendMappedError`, matching v1's status codes and idempotent envelopes.
- [ ] Route resolves the scope from the URL by `accessor.get(IX)`; no cached scope; finishes before disposal.
- [ ] Tests assert the wire envelope + protocol shape; wire-shape guards added/updated where the route mirrors v1.
- [ ] `lint:imports` passes; the LegacyService did not invert scope direction.
## Red lines (this subskill)
- One wire schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`. Never re-declare a v1 wire schema inline in server-v2.
- A `/api/v1` mirror route must keep every existing schema field's name, type, and semantics; only optional additions are allowed. A different shape belongs on `/api/v2`, not on the mirror.
- Do not distort the native v2 Service to satisfy a v1 quirk — add a `<domain>Legacy` edge adapter instead. The native Service serves the v2 architecture; the LegacyService serves the wire contract.
- A LegacyService is still a v2 Service: it follows scope, domain-direction, and DI rules. "Edge adapter" describes its role, not an exemption.
- The established wire schema (in its owning home — the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`) plus the existing mirror routes are the spec for a `/api/v1` route — match them; do not re-derive the wire shape from the v2 domain model or from memory.
- Register every new error code in **both** `agent-core-v2` and `packages/kap-server/src/protocol/error-codes.ts`; an unmapped code is a wire break.
- Events stream over WS (`listen`), never over the REST mirror; do not invent REST polling for something v1 pushed as an event.

View file

@ -0,0 +1,352 @@
# Topic — Service authoring
How to write a Service in `packages/agent-core-v2`: file layout, naming, what goes in the contract vs the impl, interface style, constructor / field conventions, events, multi-Service domains, and the comment rules. This is the day-to-day reference for stage 3 (implement.md covers the DI *mechanics*; this file covers the *authoring details*).
## File layout
One folder per domain, **camelCase**: `session/`, `sessionActivity/`, `contextMemory/`, `toolDedup/`. Inside, six kinds of files:
```text
<domain>/
├── <name>.ts ← interface file: exactly one IXxx + its createDecorator + the types it owns
├── <name>Service.ts ← impl file: exactly one class + exactly one registerScopedService(...)
├── <concern>.ts ← pure function(s): no Service suffix, no class, no registration
├── <targetDomain>.ts ← contribution file (common): registers into another domain's extension point
├── <what>.contrib.ts ← contribution file (uncommon / ad-hoc)
└── <domain>.types.ts ← shared types that no single interface owns
```
- **Strictly one service per file.** An interface file holds exactly one injectable interface and exactly one `createDecorator(...)`; an impl file holds exactly one service implementation class and exactly one `registerScopedService(...)`. No exceptions for "tightly-coupled" groups: even same-scope collaborators each get their own `<name>.ts` + `<name>Service.ts` pair.
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)).
- A domain therefore has as many impl files as it has services (e.g. `logService.ts` for the App `ILogService`, `sessionLogService.ts` for the Session `ISessionLogService`). See [Multi-Service domains](#multi-service-domains).
The package entry `src/index.ts` imports and `export *`s every domain's leaf files precisely (one line per leaf), so importing the package still runs every `registerScopedService(...)` side effect — exactly as the old per-domain barrels did.
## Naming
### Interfaces and classes
| Artifact | Rule | Example |
|---|---|---|
| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Workspace` / `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `IWorkspaceDirs`, `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) |
| Class | the interface name minus the leading `I`, plus `Service` if it does not already end in `Service`; `implements` the interface | `SessionLogService implements ISessionLogService`, `AppendLogStoreService implements IAppendLogStore` |
| Decorator string | lowerCamelCase of the interface name minus the leading `I`; **globally unique and stable** (it surfaces in `CyclicDependencyError.path` and "no service registered" errors) | `createDecorator<ISessionLogService>('sessionLogService')` |
| Model / non-service types | PascalCase, no `I` prefix | `SessionMeta`, `LogEntry`, `ConfigSection` |
The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Workspace, Session and Agent services always carry `Workspace` / `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names.
> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md).
### File names
File names derive from the interface / class names so that scope and role are visible in the tree:
| File kind | Rule | Example (interface → file) |
|---|---|---|
| Interface file | interface name minus leading `I`, minus trailing `Service` if present; acronym-aware lowerCamelCase | `ISessionLogService` → `sessionLog.ts`; `IAppendLogStore` → `appendLogStore.ts`; `ILogService` → `log.ts` |
| Impl file | the class name; acronym-aware lowerCamelCase | `SessionLogService` → `sessionLogService.ts`; `AppendLogStoreService` → `appendLogStoreService.ts` |
| Pure-function file | the function / concern name; no `Service` suffix | `formatLogEntry.ts`, `levelEnabled.ts` |
| Contribution file (common) | the **target** domain name | `config.ts` (registers a config section), `tool.ts`, `flag.ts` |
| Contribution file (uncommon) | `<what>.contrib.ts` | `slackWebhook.contrib.ts` |
| Shared-types file | `<domain>.types.ts` | `log.types.ts` |
| Errors file | `<name>.errors.ts` | `appendLogStore.errors.ts` |
Acronym-aware lowerCamelCase lowercases a leading acronym as a group: `ILLMRequester` → `llmRequester.ts`, `IWSGateway` → `wsGateway.ts`, `IOAuthToolkit` → `oauthToolkit.ts`, `IMcpServerService` → `mcpServerService.ts`.
Because the impl class always ends in `Service` and the interface file never does, the two files of one service never collide — even for `Store` / `Registry` / `Resolver` interfaces (`IAppendLogStore` → `appendLogStore.ts` + `appendLogStoreService.ts`).
## The contract file (`<domain>.ts`)
Holds the public surface of the domain. A typical contract:
```ts
/**
* `greet` domain (Ln) — one-line role.
*
* Defines the `Greeting` model and the `IGreeter` used by … Bound at … scope.
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface Greeting { // model — no _serviceBrand
readonly message: string;
}
export interface IGreeter { // injectable service — carries _serviceBrand
readonly _serviceBrand: undefined;
hello(): Greeting;
}
export const IGreeter: ServiceIdentifier<IGreeter> =
createDecorator<IGreeter>('greeter');
```
What belongs here:
- **Model types** (`type` / `interface`) the domain exposes — `SessionMeta`, `LogEntry`, `ConfigSection`.
- **Service interface(s)** — the contract consumers depend on.
- **Decorator(s)** — one `createDecorator` per injectable service.
- **Helper types and pure functions** tightly bound to the contract — e.g. option bags, `satisfies`-checked seeds, predicate functions like `levelEnabled`.
### Which interfaces carry `_serviceBrand`
Only interfaces used as a **DI token** carry `readonly _serviceBrand: undefined`. Everything else does not:
- ✅ Service interface resolved via `@IX` / `accessor.get(IX)` → carries `_serviceBrand`.
- ❌ Base interface extended by a service (e.g. `ILogger` extended by `ILogService`) → no `_serviceBrand`.
- ❌ Plain model / data interface (`LogEntry`, `SessionMeta`) → no `_serviceBrand`.
```ts
export interface ILogger { // base interface — no brand
info(message: string): void;
}
export interface ILogService extends ILogger { // DI token — branded
readonly _serviceBrand: undefined;
setLevel(level: LogLevel): void;
}
```
## Interface style
- **Sync methods** return a concrete type; **async methods** return `Promise<T>`. Do not wrap a sync return in `Promise`.
- **Readonly fields** for immutable exposed state: `readonly ready: Promise<void>`, `readonly modelAlias: string | undefined`.
- **Optional members** with `?`: `flush?(): Promise<void>`, `close?(): Promise<void>`.
- **Generics** where the caller supplies the shape: `get<T = unknown>(domain: string): T`.
- **Extend** a base interface to share method groups: `interface ILogService extends ILogger`.
- **Events** as `readonly onDid…` / `onWill…` properties typed `Event<T>` — see [Events](#events).
```ts
export interface IConfigService {
readonly _serviceBrand: undefined;
readonly ready: Promise<void>;
readonly onDidChange: Event<ConfigChangedEvent>;
get<T = unknown>(domain: string): T;
set(domain: string, patch: unknown): Promise<void>;
reload(): Promise<void>;
}
```
## The impl file (`<domain>Service.ts`)
Holds the concrete class(es) and the top-level registration. A typical impl:
```ts
/**
* `greet` domain (Ln) — `IGreeter` implementation.
*
* … collaborators as roles ("logs through `log`") … Bound at App scope.
*/
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { ILogService } from '#/log';
import { type Greeting, IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined;
constructor(@ILogService private readonly log: ILogService) {}
hello(): Greeting {
this.log.info('hello');
return { message: 'hi' };
}
}
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```
What belongs here:
- **Imports** — `LifecycleScope` + `ScopeActivation` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/<domain>` alias; the contract's types + decorator via a relative `./<domain>` import.
- **Class** — `XxxService implements IXxxService`, with `declare readonly _serviceBrand: undefined`.
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this` — `provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable` (leave a NOTE comment saying so).
## Constructor conventions
- Declare every dependency with `@IX` on a constructor parameter.
- Use `private readonly` (or `protected readonly`) to store a used dependency as a field.
- For an injected dependency the class does **not** directly use (e.g. passed through, or only needed to force construction order), drop the visibility modifier and prefix with `_`: `@IEventService _event: IEventService`.
- Service parameters and static parameters may both appear; the ordering rule depends on how the object is created — see below.
### Parameter order: scoped service vs `createInstance`
- **`registerScopedService` services** — the container injects only the `@IX` parameters; any static parameters must have defaults and are left at their default when the container builds the instance. Order is therefore not enforced by the container, but the common style is **`@IX` parameters first, optional static parameters after**:
```ts
constructor(
@ILogWriterService protected readonly writer: ILogWriterService,
private readonly bound: LogContext = {},
level: LogLevel = 'info',
) {}
```
- **`createInstance` objects** (non-singletons built with `instantiation.createInstance(Ctor, …staticArgs)`) — static parameters **must come first**, service parameters after, because the caller passes the static prefix positionally:
```ts
constructor(
private readonly input: string, // static — passed by caller
@ILogService private readonly log: ILogService, // service — injected
) {}
```
### Factory methods
A scoped Service may expose a factory method that returns a **new** instance of itself (or a related class) with extra context bound — e.g. `ILogger.child(ctx)` returns `new LogService(this.writer, { …this.bound, …ctx }, this._level)`. This is not a DI violation: it is an explicit factory, not a request for the container to build a Service. Do not use it to circumvent scope or singleton semantics.
## Fields and state
- `private readonly` for fields set once at construction (injected deps, derived config).
- `private _name` (underscore prefix) for mutable private state: `private _level: LogLevel`.
- `readonly` public fields only for immutable exposed state; prefer a getter (`get level()`) when the value can change.
- Keep state minimal — a Service owns only the state that matches its scope's identity (design.md §2). Anything else belongs in a different Service.
### Runtime state goes into the per-scope state container
Workspace/Session/Agent-scope Services register their runtime state into the scope's state container (`IWorkspaceStateService` / `ISessionStateService` / `IAgentStateService`, all over `_base`'s `StateRegistry`) instead of holding it in bare instance fields, so per-scope state lives in one observable place (`snapshot()` / `onDidChange`) and dies with the scope. Reference: `session/interaction/interactionService.ts`.
- Declare keys in the domain file and export them: `export const interactionPendingKey = defineState<Map<string, Pending>>('interaction.pending', () => new Map())` — `<domain>.<field>` naming, factory initializers.
- Inject `@ISessionStateService private readonly states` (or the Agent token) and `this.states.register(key)` per key at the top of the constructor.
- Replace the field with accessors: a getter for collections only mutated in place (`this.foo.add(...)` keeps working — the container stores references, never clones); add a setter routed through `states.set` for reassigned scalars. Call sites stay unchanged.
- Values must be plain data: scalars, arrays, and literal objects/Maps/Sets built from them. Never register class instances, resource handles (disposables, abort controllers, Promise locks), or objects holding service references — the regression precedent: one registry key whose class instances reached the whole DI graph deep-copied to hundreds of MB on `snapshot()` and OOM-killed the server. This means registries whose entries carry resources (the tool registry, the task map, prompt queues) stay as instance fields alongside Emitters, hook slots, disposable slots, waiter arrays, caches, and queue instances.
- `snapshot()` additionally recurses plain data only: values with a custom prototype collapse to a `'(ClassName)'` marker — a `_base`-level backstop, not a license to register resource-bearing values.
- Durable, replayable state does NOT belong here — it stays on wire Models. The container is memory-only.
## Events
v2 has two distinct event mechanisms. Pick by audience:
### `Event<T>` / `Emitter` — typed property on a Service
Use when a Service exposes a typed event its consumers subscribe to. Lives in `'#/_base/event'`.
```ts
// contract
import type { Event } from '#/_base/event';
export interface IConfigService {
readonly onDidChange: Event<ConfigChangedEvent>;
}
// impl
import { Emitter, type Event } from '#/_base/event';
export class ConfigService extends Disposable implements IConfigService {
private readonly _onDidChange = this._register(new Emitter<ConfigChangedEvent>());
readonly onDidChange: Event<ConfigChangedEvent> = this._onDidChange.event;
private notify(changed: ConfigChangedEvent): void {
this._onDidChange.fire(changed);
}
}
```
Conventions:
- Back the public `Event<T>` with a private `Emitter<T>`, registered with `this._register(...)` so it disposes with the Service.
- Naming: `onDid…` for "happened" (past tense, after the fact); `onWill…` for "about to happen" (may allow `waitUntil` participation / veto — see `AsyncEmitter` / `IWaitUntil` in `'#/_base/event'`).
- A service must be constructed before consumers can subscribe to its events. Use the default `OnScopeCreated` activation when subscriptions must be available as soon as the scope is ready.
### `IEventService` — global pub-sub bus
Use to broadcast protocol events across domains. Lives in `'#/event'`.
```ts
export interface IEventService {
readonly _serviceBrand: undefined;
publish(event: ProtocolEvent): void;
subscribe(handler: (event: ProtocolEvent) => void): IDisposable;
}
```
Inject `@IEventService` and `publish(...)`; `subscribe(...)` returns an `IDisposable` to register with `this._register(...)`. This is the bus for "a fact happened, react if you care" (design.md §4) — not for typed per-Service events.
## Multi-Service domains
A domain may define several Services. Each Service gets its own pair of files regardless of scope or coupling:
- **One pair per Service** → `<name>.ts` for the contract + `<name>Service.ts` for the implementation.
- **Different scopes** → the scope prefix in the Service name makes this obvious (`logService.ts` for App `ILogService`, `sessionLogService.ts` for Session `ISessionLogService`).
- **Same interface, multiple role tokens** (e.g. `IAtomicDocumentStore` and `IAtomicTomlDocumentStore` share one interface type but are distinct DI tokens) → each token is its own Service identity and must be registered and resolved independently.
There is no `index.ts` barrel: consumers import each contract/impl from its precise leaf path (e.g. `import { ILogService } from '#/log/log'`), never the domain directory.
## No barrel — the package entry loads leafs precisely
A domain has **no `index.ts` barrel**. Its files are the contract leaf (`<name>.ts`) and the impl leaf (`<name>Service.ts`), and consumers import the precise file — never the directory:
```ts
import { IGreeter, type Greeting } from '#/greet/greet';
```
Self-registration is unchanged: `greetService.ts` keeps its top-level `registerScopedService(...)`. The package entry `src/index.ts` loads the domain's leafs precisely — `export *` for the contract, a side-effect `import` for the impl — one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService';
```
Importing the package therefore fires every `register*` side effect, exactly as the old per-domain barrels did. When you add a new domain, write the contract + impl leafs (with their top-level `register*`), then add the leaf path(s) to `src/index.ts`. **Do not create an `index.ts`.**
- Load the impl file too — its top-level `registerScopedService(...)` only runs when the module is imported.
- `export *` helper modules only if they are part of the domain's public surface.
## Comments
- **No comments** (orient.md): no file headers, no statement-level narration, no JSDoc — not on exported symbols either; the only exception is a load-bearing `oxlint-disable` / `eslint-disable` directive.
- **Methods and fields carry no comments.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).
## Complete minimal example
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface Greeting { readonly message: string; }
export interface IGreeter {
readonly _serviceBrand: undefined;
hello(): Greeting;
}
export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('greeter');
```
```ts
// greet/greetService.ts
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { type Greeting, IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined;
hello(): Greeting { return { message: 'hi' }; }
}
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService';
```
## Red lines (this topic)
- One folder per domain, camelCase; one service per file pair: contract `<name>.ts` + impl `<name>Service.ts`; **no `index.ts` barrel** — `src/index.ts` loads each leaf file precisely.
- Exactly one injectable interface and one `createDecorator(...)` per contract file.
- Exactly one service implementation class and one `registerScopedService(...)` per impl file.
- `IXxxService` / `XxxService` naming; decorator string is lowerCamelCase, globally unique, and stable.
- Name Services by owning domain, never by scope (`IAgentEntityService`, `ISessionEntityService`).
- `_serviceBrand` only on interfaces used as a DI token — never on base interfaces or plain models.
- Sync methods return concrete types, async return `Promise<T>`; do not `Promise`-wrap sync work.
- `createInstance` objects put static parameters before service parameters; scoped services put `@IX` parameters first (static params need defaults).
- Never `new` a `@IService`-carrying Service — except inside an explicit factory method, which is not a DI request.
- Events: typed per-Service event → `Event<T>`/`Emitter` from `'#/_base/event'`; cross-domain broadcast → `IEventService` from `'#/event'`.
- `src/index.ts` must import/export every leaf file (including the impl) so each `register*` side effect runs.
- No comments by default (orient.md); stubs throw `NotImplementedError`.

View file

@ -0,0 +1,97 @@
# Topic — Telemetry
Telemetry infrastructure for agent-core-v2: how business services emit events, how context propagates, and how events reach a destination through appenders.
Telemetry is a **layer-1 root** domain (alongside `log`): the facade lives at `App` scope (a per-Agent ambient context service is bound at `Agent` scope), stateless, with no business-domain dependencies. It is a thin facade — enrichment, batching, and transport belong to the appenders, not to this layer.
## Where things live
- `src/app/telemetry/telemetry.ts`: contract — `ITelemetryService` (facade), `ITelemetryAppender` (destination), `TelemetryProperties`, `nullTelemetryAppender`, and `TelemetryServiceOptions`.
- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent<P>` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent<P>`.
- `src/app/telemetry/telemetryService.ts`: `TelemetryService` impl + `registerScopedService(LifecycleScope.App, …)`.
- `src/app/telemetry/agentTelemetryContext.ts` + `agentTelemetryContextService.ts`: `IAgentTelemetryContextService` — Agent-scoped mutable request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) snapshot into turn telemetry at launch. Agent identity (`agent_id`) is not part of it — identity is bound by the Agent-scoped `ITelemetryService` view.
- `src/app/telemetry/consoleAppender.ts`: `ConsoleAppender` — echoes events to a log function (dev / debug).
- `src/app/telemetry/cloudAppender.ts`: `CloudAppender` — sanitizes + PII-cleans properties, batches + enriches + posts to the telemetry endpoint.
- `src/app/telemetry/cloudTransport.ts`: `CloudTransport` — HTTP transport behind `CloudAppender`.
- `src/app/telemetry/privacy.ts`: outbound PII redaction (`cleanTelemetryProperties`) — URLs, emails, tokens, and absolute file paths become `<REDACTED: ...>` labels; `node_modules/` tails are kept.
## Emitting events (business services)
Inject `ITelemetryService` and call `track2` with a registered event:
```ts
import { ITelemetryService } from '#/app/telemetry/telemetry';
constructor(@ITelemetryService private readonly telemetry: ITelemetryService) {}
this.telemetry.track2('cron_fired', { task_id: taskId, coalesced_count: 0, stale: false, buffered: false, recurring: true });
```
`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent<P>({ owner, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent<P>` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
`TelemetryService.track` merges the bound context into the properties and fans the event out to every registered appender. A single throwing appender is isolated via `onUnexpectedError` and never blocks the rest.
### Context (sessionId / agent_id / turn_id)
The root service carries a bound context (`sessionId`) that is merged into every event, and each Agent scope gets its own telemetry view seeded with `agent_id` (by `agentLifecycle`), so Agent-scoped services emit their identity without call-site plumbing. Mutable per-agent request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) lives in `IAgentTelemetryContextService` and is snapshot into a per-turn view at turn launch. Derive a scoped view with `withContext`:
```ts
const child = telemetry.withContext({ agent_id: 'agent-0' });
child.track2('tool_call', { turn_id: 1, tool_call_id: 'c1', tool_name: 'bash', outcome: 'success', duration_ms: 12 }); // wire carries sessionId + agent_id
```
`withContext(patch)` returns a lightweight forwarding view: transport state (appenders, enabled flag) stays with the root, so later `addAppender` / `setEnabled` calls apply to every view, and per-call properties override bound context on key collision. `setContext(patch)` on the root mutates the root context and propagates to appenders that implement `setContext`; on a view it mutates only that view's own context.
## Appenders (destinations)
An appender is the destination an event is fanned out to. It is **not a DI Service** — it is a plain object implementing `ITelemetryAppender`, held by `TelemetryService`.
```ts
export interface ITelemetryAppender {
track(event: string, properties?: TelemetryProperties): void;
withContext?(patch: TelemetryContextPatch): ITelemetryAppender;
setContext?(patch: TelemetryContextPatch): void;
flush?(): Promise<void> | void;
shutdown?(): Promise<void> | void;
}
```
Built-in appenders:
- `ConsoleAppender` — `[telemetry] <event> <json>` to a log function (default `console.log`); options `prefix` / `pretty` / `log`.
- `CloudAppender` — batches events, enriches with common context (`app_name` / `version` / `platform` / …), and posts to `https://telemetry-logs.kimi.com/v1/event` through `CloudTransport` (Bearer auth, retry, on-disk fallback). Options: `homeDir` / `deviceId` / `sessionId?` / `appName` / `version` / `uiMode?` / `model?` / `getAccessToken?` / `endpoint?` / `flushThreshold?` / `flushIntervalMs?`.
### Registering appenders (bootstrap)
Appenders are added after the App scope exists, by resolving the service and calling `addAppender`:
```ts
const app = createAppScope();
const telemetry = app.accessor.get(ITelemetryService);
telemetry.addAppender(new ConsoleAppender({ prefix: '[dev]' })); // dev echo
telemetry.addAppender(new CloudAppender({ // production
homeDir, deviceId, sessionId,
appName: 'kimi-code', version, uiMode: 'shell', model,
getAccessToken: () => auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME),
}));
```
`addAppender` returns an `IDisposable` that removes the appender when disposed. `setAppender(appender)` resets to a single appender (mainly for tests). `removeAppender(appender)` drops one.
> There is no production bootstrap wired yet — `TelemetryService` defaults to `[nullTelemetryAppender]`, so `track(...)` is a no-op until `addAppender` is called at startup.
## Lifecycle
- `setEnabled(false)` drops `track` (service-level switch); `setEnabled(true)` resumes. `flush` / `shutdown` are unaffected by the switch.
- `flush()` / `shutdown()` fan out to all appenders concurrently; a single rejecting appender is swallowed. Await `shutdown()` before process exit so buffered events (e.g. in `CloudAppender`) are sent.
## Red lines (this topic)
- Business services depend only on `ITelemetryService` — never import an appender class.
- Telemetry is layer-1 root: do not inject any business-domain service into it, and keep the facade at `App` scope (only the ambient context service binds at `Agent`).
- Appenders are plain `ITelemetryAppender` objects, not DI Services — register them with `addAppender`, never via `registerScopedService`.
- `track` is fire-and-forget and must not throw; appender `track` must be synchronous — buffer and send asynchronously via `flush` / `shutdown`.
- Await `telemetry.shutdown()` before process exit when a buffering appender is registered.
- Keep event names stable; register every business event in `events.ts` and emit via `track2` — properties must be JSON-serializable primitives (non-primitives are dropped with a warning by `CloudAppender`).
- Agent identity is ambient: agent-scope events go through `defineAgentTelemetryEvent` and get `agent_id` from the scoped telemetry view — do not pass `agent_id` at business call sites (per-event identities such as `subagent_created` and the cron events are the exception).

View file

@ -0,0 +1,270 @@
# Stage 4 — Test
Exercise the **same path production uses**: a service is reached by its interface through the container, its `@IService` dependencies are resolved from the container, and — where the scope layer matters — through the scope tree. Tests that `new` a service and paper over its constructor with hand-rolled objects bypass that path and let the `registerScopedService(IX → Impl)` binding rot untested.
`@IService` parameter decorators run under vitest (the build uses `experimentalDecorators`), so fixtures declare dependencies exactly like production code. There is **no** `param()` helper, no manual `(Id as …)(Ctor, '', 0)`, and no capturing `accessor` inside a constructor to synchronously `.get()` a peer.
## The one rule
**Resolve the system under test by its interface, through the container. Never call `new` on a production service whose constructor carries `@IService` dependencies.**
```ts
// ✅ resolve by interface — the IX → Sut binding is exercised
ix.set(IMessageService, new SyncDescriptor(MessageService));
const svc = ix.get(IMessageService);
// ❌ construct the implementation directly — the registration is never run
const svc = new MessageService(stubContext);
```
Resolving by interface is what makes `registerScopedService(ISut, Sut, …)` part of the test. Constructing the class directly (or via `ix.createInstance(Sut)`) tests the class in isolation but leaves the binding, the scope layer, and the delayed/eager flag unverified.
Pure functions, value objects, and services with **no** `@IService` dependencies may be constructed directly.
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — annotate it with a comment explaining why.
## Two harnesses
Pick the harness by *whether the scope layer is part of what you are testing*.
| Under test | Harness | Resolve the SUT with |
|---|---|---|
| A single service's behavior (unit) | `TestInstantiationService` (flat) | `ix.get(ISut)` after `ix.set(ISut, new SyncDescriptor(Sut))` |
| Cross-scope wiring, or which layer a service lives in | `createScopedTestHost` (scope tree) | `host.<scope>.accessor.get(ISut)` |
### Unit harness — `TestInstantiationService`
Default for domain service unit tests. It is an `InstantiationService` that also implements `ServicesAccessor` (so you can `ix.get(...)` directly) and owns sinon (so `dispose()` restores stubs).
```ts
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices } from '#/_base/di/test';
import type { TestInstantiationService } from '#/_base/di/test';
import { registerRecordsServices } from '../records/stubs';
describe('XxxService', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
beforeEach(() => {
disposables = new DisposableStore();
ix = createServices(disposables, {
base: [registerRecordsServices],
additionalServices: (reg) => {
reg.define(IContextService, ContextService); // 1. real collaborator, by interface
reg.define(IXxxService, XxxService); // 2. system under test, by interface
},
});
});
afterEach(() => disposables.dispose());
it('does the thing', () => {
const svc = ix.get(IXxxService); // 3. resolve by interface
expect(svc.thing()).toBe('…');
});
});
```
`createServices` builds the container from domain **service groups** plus per-test overrides (see Service groups). Reach for `ix.stub(...)` / `ix.set(...)` directly only inside an `it` when a single test needs to swap a registration:
- whole service, partial object: `ix.stub(IId, { method() { return … } })`;
- single method: `ix.stub(IId, 'method', value)` returns a sinon stub; `ix.spy(IId, 'method')` returns a spy;
- a prebuilt instance or descriptor: `ix.set(IId, instance)` / `ix.set(IId, new SyncDescriptor(Impl))`;
- when a collaborator's behavior must vary per test, model it as a `Test*Service` subclass whose methods read suite-scoped `let` variables rather than rebuilding the container each test.
### Scope harness — `createScopedTestHost`
Reach for this only when *which layer a service lives in* is itself the thing being asserted, or when the SUT reads from parent/child scopes. It builds the real `Scope` tree and resolves through it.
```ts
import { beforeEach, describe, expect, it } from 'vitest';
import { LifecycleScope } from '#/app/scopes';
import {
ScopeActivation,
_clearScopedRegistryForTests,
registerScopedService,
} from '#/_base/di/scope';
import { createScopedTestHost, stubPair } from '#/_base/di/test';
describe('XxxService (scoped)', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
registerScopedService(
LifecycleScope.Agent,
IXxxService,
XxxService,
ScopeActivation.OnDemand,
'xxx',
);
});
it('resolves from the Agent scope with ancestor deps injected', () => {
const host = createScopedTestHost([stubPair(ILogService, stubLog())]);
const agent = host.child(LifecycleScope.Agent, 'main');
const svc = agent.accessor.get(IXxxService); // by interface
expect(svc.thing()).toBe('…');
host.dispose();
});
});
```
Always `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`. Do not rely on a production module's top-level `registerScopedService(...)` side effect: import order then becomes part of the test, and another suite's `_clearScopedRegistryForTests()` can wipe it.
## Register the SUT by interface
Whichever harness you use, the SUT is registered under its interface (`ix.set(IX, new SyncDescriptor(Impl))` or `registerScopedService(scope, IX, Impl, …)`) and resolved by that interface. This is non-negotiable: it is the only thing that keeps the production registration honest.
A test that does `ix.createInstance(Impl)` is testing the class, not the service. Convert those (see Migration).
## Shared stubs
Hand-rolled stubs (`noopLog`, `noneEvent`, `unusedRecords`, …) must not be copied between test files. Each domain that owns a frequently-stubbed interface exports a stub from a `stubs.ts` **in the `test/` tree**, never from `src/`:
```text
test/log/stubs.ts → stubLog() / stubLogger()
test/turn/stubs.ts → stubTurn()
test/records/stubs.ts → stubAgentRecords()
test/environment/stubs.ts → stubEnvironment()
```
All test support lives under `test/` so test-only code stays out of the production source tree. Because `tsdown` builds from `src/index.ts`, anything under `test/` is unreachable from the entry and is never bundled into `dist/`.
Conventions:
- export a **factory** (`stubXxx()`), not a shared singleton, so tests cannot leak state through a stub;
- name it `stub<Interface>` — e.g. `stubAgentRecords`;
- the stub satisfies the full interface so the compiler, not a cast, guarantees it stays in sync;
- import it with a **relative path** — `./stubs` from the same domain's tests, `../<domain>/stubs` from another domain. Never import stubs from `#/…` (that alias is for production `src/`) and never import one test file from another;
- a `stubs.ts` may import its domain's production types via `#/<domain>/…`.
If a stub is needed by two test files, it belongs in that domain's `test/<domain>/stubs.ts`.
## Service groups
Most unit tests stub the same handful of collaborators (`ILogService`, `IAgentRecords`, `IConfigService`, `ITelemetryService`, …). Rather than repeat `ix.stub(...)` lines in every `beforeEach`, each domain exports a `register*Services` function from its `stubs.ts` that registers the default test doubles for that domain:
```ts
// test/log/stubs.ts
export function registerLogServices(reg: ServiceRegistration): void {
reg.defineInstance(ILogService, stubLog());
}
```
`createServices(disposables, { base, additionalServices })` composes them:
- `base` — an ordered list of service groups. Each group's registrations are deduped (first writer wins), so groups supply safe defaults without clobbering each other.
- `additionalServices` — applied after `base`. Registrations here **overwrite** any base default, so a test can swap a stub for a spy, register the system under test, or supply a one-off collaborator.
```ts
ix = createServices(disposables, {
base: [registerLogServices, registerConfigServices, registerRecordsServices],
additionalServices: (reg) => {
reg.definePartialInstance(IAgentKaos, {}); // one-off collaborator
reg.define(IAgentRecords, spyRecords); // override a base default
reg.define(IXxxService, XxxService); // system under test
},
});
```
`ServiceRegistration` offers three verbs:
- `define(id, Ctor)` — lazy `SyncDescriptor`; the service is instantiated on first resolve. Use for real collaborators and the system under test.
- `defineInstance(id, instance)` — a fully-built instance (a fake such as `stubLog()`, or `new ConfigRegistry()`).
- `definePartialInstance(id, { ... })` — a partial mock; only the supplied members are provided. Use for collaborators the test does not exercise.
Conventions:
- a group registers the domain's services **as dependencies** (a fake, or a `{}` partial when no fake exists yet). When a service is the system under test, the test registers the real implementation via `additionalServices` and does not rely on the group's default for it;
- keep groups small and domain-local. A service that is almost always the system under test, or that every consumer configures differently, should not have a group — register it inline via `additionalServices`;
- import groups with a **relative path** (`../<domain>/stubs`), never from `#/…`.
`createServices` defaults to `strict: false` (missing dependencies warn rather than throw), matching `new TestInstantiationService()`. Pass `strict: true` to surface unregistered `@IService` dependencies.
## Declaring dependencies
Always use `@IService` constructor decorators — in fixtures and in production services alike.
```ts
// ✅
class Consumer {
constructor(@IGreeter private readonly greeter: IGreeter) {}
}
// ❌ no param() helper, no inline cast
class Consumer {
constructor(private readonly greeter: IGreeter) {}
}
param(IGreeter, Consumer, 0);
```
Because the decorator runs when the class is defined, the `createDecorator` identifier must be initialized **before** the class that uses it. Declare the identifier, then the class:
```ts
const IDep = createDecorator<IDep>('dep');
class Consumer {
constructor(@IDep private readonly dep: IDep) {}
}
```
For two services that depend on each other (a cycle), declare both identifiers first, then both classes, so neither class references an uninitialized binding.
Declare fixtures at module top, interface + decorator + implementation co-located, and keep `_serviceBrand` on the interface when it represents a real service — `GetLeadingNonServiceArgs` relies on the brand to tell service parameters apart from static ones. Pure throwaway fixtures may omit `_serviceBrand`.
## Lifecycle / teardown
One `DisposableStore` per suite. Add the **container** and any event subscriptions to it; dispose in `afterEach`.
```ts
beforeEach(() => { disposables = new DisposableStore(); /* … */ });
afterEach(() => disposables.dispose());
```
Do **not** add the system-under-test itself to the store. `TestInstantiationService` disposes every service it creates when the container is disposed, so `ix.get(IX)` instances are cleaned up automatically via `disposables.add(ix)`. Wrapping the SUT in `disposables.add(...)` would double-dispose it. For the same reason, do not call `svc.dispose()` at the end of a test unless you are asserting something about disposal itself.
Scope-host tests call `host.dispose()` in `afterEach` (or at the end of the `it`). Route teardown through the store so ordering is deterministic and nothing leaks when a test fails mid-way.
## Cascade: asserting unit state
The cascade engine's test vocabulary lives in two files: `test/_base/di/cascade.test.ts` (the mechanism matrix, including cross-scope orchestration) and `test/_base/di/provide.test.ts` (provide/unprovide semantics).
- **Assert unit states, not internals.** Every container exposes its engine as `container.cascade`: `stateOf(IX)` → `'Pending' | 'Activating' | 'Active' | 'Unloading' | 'Failed'`; `failureOf(IX)` → the sticky error of a `Failed` unit; `pendingSnapshot()` → the waiting-area contents.
- **The waiting area parks units with unregistered dependencies** — a unit whose declared deps are missing stays `Pending` (no throw), so a test must seed the full dependency chain. Example: a root→agent chain with no session container must seed the session-scope dependency explicitly — `ix.set(ISessionStateService, new SessionStateService())` in `test/session/agentLifecycle/agentLifecycle.test.ts` — or the dependent unit never activates.
- **Eager activation failure is sticky `Failed`, not a scope-creation throw.** Assert state + rethrow: `expect(ix.cascade.stateOf(IX)).toBe('Failed')`, then `expect(() => ix.invokeFunction((a) => a.get(IX))).toThrow(…)`. Do not expect scope/host creation itself to throw for a failing eager constructor.
## Assertions and naming
- One behavior per `it`; describe observable behavior (`child shadows parent registration`), not implementation (`calls _getOrCreateServiceInstance`).
- For cycles, assert `CyclicDependencyError` and its `path` array (e.g. `['A', 'B', 'A']`), not merely `toThrow`.
- For disposal order, capture events in an array and assert the sequence (`['C', 'B', 'A']` — children before parents).
## Migrating existing tests
Most legacy tests build the SUT with `ix.createInstance(Impl)`. Converting one is mechanical:
1. import the interface (`IX`) and the descriptor;
2. register the SUT by interface — `reg.define(IX, Impl)` inside `additionalServices` (or `ix.set(IX, new SyncDescriptor(Impl))`);
3. replace `ix.createInstance(Impl)` with `ix.get(IX)`;
4. drop the `disposables.add(...)` wrapper around the SUT and any trailing `svc.dispose()` — the container disposes it;
5. replace any hand-rolled collaborator object with the domain's shared stub or service group (or add one to `test/<domain>/stubs.ts` if it does not exist);
6. delete now-unused imports.
Before / after:
```ts
// before
const svc = ix.createInstance(MessageService);
// after — registration in beforeEach additionalServices
reg.define(IMessageService, MessageService);
// after — resolution in the test body
const svc = ix.get(IMessageService);
```
## Red lines (this stage)
- Resolve the SUT by interface — never `new` a production service with `@IService` deps; prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Shared stubs live in `test/<domain>/stubs.ts` (never `src/`); import by relative path, never `#/...`.
- Scope tests call `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`; do not rely on production import-order side effects.
- One `DisposableStore` per suite; add the container, dispose in `afterEach`; do not add the SUT itself.
- Declare fixture dependencies with `@IService`; initialize `createDecorator` identifiers before the classes that use them.

View file

@ -0,0 +1,32 @@
# Stage 5 — Verify & submit
Run the guards and re-scan the red lines before submitting.
## Commands
Run from the package (or with `--filter @moonshot-ai/agent-core-v2`):
- `pnpm --filter @moonshot-ai/agent-core-v2 lint:imports` — import-boundary guard (`scripts/check-import-boundaries.mjs`). Catches v1 imports (`@moonshot-ai/agent-core`) and kosong subtree violations.
- `pnpm --filter @moonshot-ai/agent-core-v2 typecheck` — `tsc -p tsconfig.json --noEmit`.
- `pnpm --filter @moonshot-ai/agent-core-v2 test` — `vitest run`.
## Changesets (when the change ships through the CLI)
If the change is user-facing and ships through the CLI, generate a changeset with the repository's `gen-changesets` skill (root `AGENTS.md` workflow). `agent-core-v2` is an internal package; if its change enters the CLI bundle, the changeset lists `@moonshot-ai/kimi-code` and describes the real change — do not present an internal-only change as a user-facing feature. Never write a `major` bump without explicit user confirmation.
## Pre-submit checklist
Walk the stages you touched and confirm:
- **Design** — scope follows state identity; no `Map<sessionId, …>` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around.
- **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior.
- **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`.
- **Files** — no comments at all (no JSDoc either; only load-bearing `oxlint-disable` / `eslint-disable` survive); registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
Then re-read the [global red lines](SKILL.md#global-red-lines) once — they catch most cross-stage mistakes in a single scan.
## Red lines (this stage)
- Do not skip `lint:imports` — it is the only automated check for the v1-import ban and the kosong subtree rules.
- Do not list internal packages in a changeset when the change enters the CLI bundle — list `@moonshot-ai/kimi-code` and describe the real change.
- Never write a `major` changeset without explicit user confirmation.

View file

@ -0,0 +1,21 @@
---
name: agent-core-review
description: Use ONLY for code review and test write/review guidance in `packages/agent-core-v2` (the DI × Scope agent engine). Does NOT apply to the legacy `packages/agent-core` or to any other package — for those, do not load this skill. Groups the review and testing lenses used for agent-core-v2 — `slop` (single-level-of-abstraction / layered error-handling review, invoked only on explicit request) and `test` (contract-driven per-test rules for both authoring and reviewing tests). Apply the sub-skill that matches the task; do not apply `slop` unprompted.
has-sub-skill: true
---
# kc-review
> **Scope: `packages/agent-core-v2` only.** These lenses are calibrated for the v2 engine (DI × Scope). Do not apply them to the legacy `packages/agent-core` or to other packages.
A bundle of the lenses used when reviewing or testing `packages/agent-core-v2`. Each sub-skill is self-contained; invoke the one that matches the task.
## Sub-skills
- **`slop/`** — Single Level of Abstraction & layered error handling. A *review dimension*: a function should read as a straight-line description of its own layer, with errors handled above or below. The agent reports detections and measurements, not severity grades. **Invoke only when the user explicitly asks for this lens** — do not apply it unprompted to general reviews or refactors.
- **`test/`** — Per-test rules behind "test the contract / responsibility, not the implementation," serving two modes. **Write mode:** author a test — one behavior per `it`, drive through the public surface, stub only the true external boundary, control time/config via documented knobs, keep tests clear, isolated, and refactor-resilient (CCCR). **Review mode:** audit existing tests against the same rules and report findings with `file:line`. Use when writing, modifying, or reviewing tests, or when asked how to write a good single test.
## Routing
- Reviewing code structure / abstraction layers / where error handling belongs → `slop` (only on explicit request).
- Writing or modifying tests, reviewing test quality, or advising on a single test → `test`.

View file

@ -0,0 +1,133 @@
---
name: slop
description: Invoke only when the user explicitly asks to review code through the "single level of abstraction / layered error handling" lens — a function does only its own layer's business logic while errors are handled above or below. The agent reports detections, raw-count measurements, and move directions. Apply only when the user explicitly requests this lens.
---
# Single Level of Abstraction & Layered Error Handling
North star: **a function should read as a straight-line description of what its own layer does. Anything that is not that — input validation, error handling, error-to-response translation, logging, retries, low-level mechanics — belongs to a layer above or below, not inline.**
This is a review dimension, not a hard rule. See "Exemption checklist" at the end.
## Scope of this skill — detect and measure
The agent applying this lens is a **sensor**. Its one job is to report *whether* a function mixes levels and *by how much*; deciding *how serious* it is belongs downstream. Severity labels (`Block` / `Request changes` / `Nit`) compress a continuous quantity into an uncalibrated three-point scale and are the main source of review-to-review variance, so they are produced downstream — by a deterministic rubric, anchored examples, or a human — from the facts the agent reports.
The agent's output is exactly these four things:
- **Detection (yes/no):** does this statement / block / function violate a rule of the lens?
- **Measurement (raw factual counts only):** mechanically countable quantities — body size, control-flow keywords, named syntactic shapes (see "Quantify"). Anything that first requires classifying a line (core/foreign, happy/error, high/low level) is recorded under detection, not here.
- **Direction (where it moves):** for each foreign concern, the destination layer — push **down** into a value / parser / infra helper, or push **up** into the edge handler.
- **Exemption flags:** which items, if any, hit the exemption checklist — recorded, not weighed.
Severity grades, merge/block verdicts, and "is splitting worth it" calls live downstream, derived from the four items above.
## When to use
Apply this lens only when the user asks for it explicitly (for example "用单一抽象层次审视一下", "check whether this function does too much", "errors should be handled above/below, right?"). Leave general reviews and refactors to other lenses unless the user names this one.
## The principle
One function, one level of abstraction, one responsibility. Three mutually reinforcing rules:
1. **Single Level of Abstraction (SLAP).** Every statement inside a function sits at the same conceptual level. High-level intent ("reserve inventory, charge payment, create the order") must not be interleaved with low-level mechanics (building headers, escaping strings, opening sockets, parsing bytes). If some lines read as "what" and others as "how", they belong in different functions.
2. **Error handling is its own concern (Clean Code).** A function either does the work or handles the error — not both. Business logic describes the happy path and *signals* failure (throw or return a result); the catch, mapping, logging, and recovery live in a dedicated handler, usually one layer up. Prefer exceptions / result types over threaded check-and-return ladders that interrupt the main flow.
3. **Separation of concerns by layer.** Each layer owns exactly one kind of knowledge: low-level code knows formats and protocols; mid-level code knows business rules; edge code knows the outside world (HTTP / CLI / UI). A function that knows two of these at once is leaking a layer.
The combined test: **could you explain this function to someone without using the word "and"?** If the explanation is "it reserves stock AND validates the email format AND maps the error to a status code AND logs to metrics", it is doing more than its layer's job.
Concerns that usually do **not** belong in a business function:
- Format / range / null validation that a lower value or parser could guarantee once.
- Mapping domain failures to an external protocol (status code, exit code, UI message) — that is the edge layer's job.
- Catch-and-swallow, retry loops, backoff, timeout, circuit breaking around a single call — infrastructure, push down.
- Cross-cutting telemetry / log / metric noise woven through every step — extract or push to a wrapper.
- Check-and-return ladders that occupy more space than the business core — replace with signal + a handler above.
## Methodology — fixing a function that violates it
Work top-down. Never start by shuffling lines.
1. **Name the level.** In one sentence, write what this function is for at its own layer. If you cannot, the function has no clear level — split before polishing.
2. **Classify every statement.** Tag each line or block as: **core** (this layer's business), **down** (a detail a lower abstraction should own), **up** (a concern an upper / edge layer should own), or **cross-cutting** (log / metric / retry). Unlabeled lines are where the mess hides — do not "just leave them".
3. **Decide down vs. up for each foreign item.**
- Push **down** when it is a guarantee a lower building block can provide: a value that can only be constructed valid, a parser that returns a typed result, an infra helper that already retries. The business function then assumes validity and stays clean.
- Push **up** when it is about translating or reacting to failure for the outside world: status codes, messages, exit codes, aggregation of many errors. The edge layer catches once and maps; business code just signals.
- Rule of thumb: if removing it would change what the business rule says, it is core and stays; if removing it only changes how a failure is reported or a detail is computed, it moves.
4. **Extract, do not interleave.** Pull each foreign concern into its own named function or layer. Keep the original function as a readable sequence of same-level calls. For error handling specifically, separate the work body from the recovery body into distinct functions so neither clutters the other.
5. **Signal, do not handle, in the middle.** Mid-layer business functions throw / return and let the right layer react. Do not catch-and-log-and-continue in business code unless continuing is itself the business rule.
6. **Re-read for level.** After the moves, every remaining line should be explainable at the same altitude. If not, repeat from step 1.
Keep the change minimal: move the smallest thing that restores the level. Do not invent abstractions, frameworks, or generic "handler" machinery beyond what the function actually needs. Three straight-line, same-level calls beat a premature pipeline.
## Review method — applying the lens to a diff
Read each changed or touched function and, for each check, record only: **the hit (yes/no) plus evidence (`file:line`)**, and — where the check points at a construct — a raw factual count from "Quantify".
1. **Altitude check.** Are all lines at the same level of abstraction? Record each place where a "what" line is immediately followed by a "how" block (or vice versa) inside the same function, with `file:line`.
2. **Happy-path check.** Can you read the business intent top to bottom without stepping through error branches? Record whether error handling sits inline between business steps (yes/no + `file:line`), supported by raw counts from "Quantify" (e.g. number of `catch` clauses, `continue` statements).
3. **Ownership check.** For each validation, catch, mapping, log, retry: is this layer the rightful owner, or is it borrowed from above / below? Record each borrowed item with `file:line` and its destination (down / up), using the rules from the methodology.
4. **Layer-leak check.** Does a business function mention an external protocol (status code, exit code, UI text, wire field)? Does an edge function contain a business rule? Record each leak candidate with `file:line` and whether it names an *external* protocol or an *internal* domain shape.
5. **Explanation test.** Describe the function in one sentence with no "and". Record whether "and" was needed; if so, list the proposed split as candidate moves (down / up).
### Quantify — report only raw factual counts
Report only quantities that can be counted **mechanically from the text**. Anything that first requires classifying a line (core vs foreign, happy-path vs error-handling, high-level vs low-level) is recorded under detection (the five checks above) as evidence, not as a number here.
Report, per function:
- **Body size** — lines and/or statements of the function body; state the basis (e.g. "statements, excluding lone braces").
- **Control-flow keywords (raw counts)** — `if`, `continue`, early `return`, `throw`, `try` / `catch` / `finally`, `await`, loops (`for` / `while` / `.forEach`).
- **Named syntactic shapes a check points at** — when a check cites a construct, count it verbatim and name the exact token: e.g. number of object literals, string literals, `.trim()` calls, `.length` reads, `origin.` property reads, spread `[...x]` operations.
- **Recovery presence (raw)** — number of `catch` clauses, and number of log / metric calls inside them.
Quantities that embed a prior classification — out-of-level vs core counts, guard-to-core ratios, happy-path vs error-handling volume, "repeated boundary checks a lower layer could guarantee once", "low-level literals in a high-level flow" — are captured as evidence under the relevant check (`file:line` + the verbatim tokens). A downstream rubric derives any ratio from those raw facts.
### Red flags
Record each as evidence (yes/no + `file:line`); these are candidates, not verdicts:
- A body that is mostly check-and-return / check-and-throw ladders around a thin core.
- A recovery block that logs, maps, and returns inline, sitting next to business steps.
- A function that both computes a value and decides how that value's failure is shown to the user.
- Low-level literals (byte offsets, header strings, format codes) inside a high-level workflow.
- A name that needs "And" / "Or" / "With" to be honest, or a name so vague ("handle", "process", "do") that it hides multiple levels.
- Catch-and-swallow that hides a failure the caller needed to see.
- Defensive null / format checks repeated at every call site instead of guaranteed once at the boundary.
### Severity grading belongs downstream
The agent's facts (detections, raw counts, directions, exemptions) feed a downstream grade; the agent reports those facts and stops there. Grades compress a continuous quantity into an uncalibrated three-point scale and are exactly where identical evidence gets labeled differently across runs. Grading happens above the agent:
- A **deterministic rubric** — a versioned threshold table over the raw counts from "Quantify"; or
- **Anchored examples** — the reviewer judges relative to repo-known reference functions rather than against an absolute adjective like "materially"; or
- A **human**, for items that land near a threshold boundary.
If a downstream consumer still asks the agent for a grade, the agent returns the underlying facts and the threshold band it would fall under, with `confidence: low` on boundary cases; the grade itself is produced downstream.
### How to report findings
Report **evidence + direction**. Lead with the location and the level, then the proposed move. Prefer "this block is one level lower than the rest of the function (`file:line`) — move it **down** into X" over "this is ugly" or "this is a request-changes". The destination layer (down into a value / parser / infra helper, or up into the edge handler) is the actionable output and the deliverable. Attach the "Quantify" numbers and any exemption flags to each finding.
## Exemption checklist
This is a lens, not a law. For each foreign concern, check whether any exemption below applies and **record the hit (yes/no) plus the reason**. The agent records exemptions as facts; a recorded exemption is then used downstream to cap the grade (e.g. to `Nit`) deterministically.
- **Tiny function:** the function is small enough that splitting would add indirection with no reader benefit.
- **Foreign concern is the single job:** the "foreign" concern is in fact the function's one purpose — a dedicated error mapper, a validator, an infra wrapper, or an index-bookkeeping helper whose low-level arithmetic *is* its level.
- **Atomicity / correctness / performance:** the steps genuinely must stay together (e.g. a re-check after an `await` to guard state that may have changed).
- **Edge-translator role:** an edge / handler function whose job is to translate an external event into internal indices; naming the wire fields is its job.
Keep a split that would make the code harder to read as a recorded candidate for downstream review. When the evidence lands on an exemption boundary, record both sides and set `confidence: low`.
## Output contract
Return, per function, items 1–5 only:
1. **Level statement** — one sentence: what the function is for at its own layer.
2. **Per-check results** — for each of the five review checks: `hit: yes/no`, evidence `file:line`, and (only where the check points at a construct) a raw factual count.
3. **Measurements** — the raw factual counts from "Quantify".
4. **Exemptions** — checklist hits (yes/no + reason).
5. **Proposed moves** — for each foreign concern: `file:line` → destination (down into X / up into Y). This is the actionable deliverable.
Severity grades, block/merge verdicts, and "worth splitting" calls live downstream, derived from items 1–4. When a consumer asks for a label, hand back items 1–4 and the threshold band, with `confidence: low` on boundary cases.

View file

@ -0,0 +1,115 @@
---
name: test
description: Use when writing or reviewing tests, or when asked how to write a good single test. Encodes the per-test rules behind the "test the contract / responsibility, not the implementation" principle — name and structure one behavior per `it`, drive through the public surface, stub only true external boundaries, control time and config via documented knobs, and keep tests clear, isolated, and refactor-resilient. The same rules drive both authoring (write mode) and auditing existing tests (review mode).
---
# Tests — write & review
Per-test rules that operationalize one principle: **test the contract / responsibility, not the implementation**. This is the how-to for a single `it`, and the lens for reviewing one.
## Two modes, one rule set
- **Write mode** — authoring a test. Apply the rules below to produce it.
- **Review mode** — auditing an existing test or test diff. Apply the same rules as a checklist; report each violation with `file:line`, the rule it breaks, and the fix. See "Review mode" near the end.
The rules are identical in both modes — only the posture changes (produce vs. audit).
## Test contract, not implementation
- Drive the system through its **public control plane** and assert on **observable effects** (returned values, persisted state, emitted events, injected messages), never on source details.
- Resolve collaborators through their contract — the interface plus its identifier — not the module that binds a concrete implementation.
- Do not reach into private fields or add backdoors "for testing". If you feel the need, the seam is wrong — fix the design, not the test.
## One behavior per `it`
Each `it` covers exactly one responsibility / scenario. If the name needs "and", split it.
```ts
it('returns 401 when the caller is unauthorized', ...);
it('does not double-fire when the same tick repeats', ...);
```
## Name and structure
- `describe('<slice> (<responsibilities>)'` — name the **responsibility**, not the class.
- An `it(...)` reads as a sentence, but it must still encode three things — the **behavior / method**, the **state or condition**, and the **expected outcome**: `it('<behavior> when <condition>, <outcome>')`. A name like `does X when Y` with no result is too vague to fail usefully.
- Use spaces, not the Java-style `method_state_outcome` underscores — that convention exists only because Java test methods cannot contain spaces. A string-named test reads fine as a sentence.
- Good: `it('returns 401 when the caller is unauthorized')` · `it('advances the cursor and does not double-fire on a repeat tick')`
- Bad: `it('works')` · `it('handles auth correctly')` — no condition, no outcome
- Arrange / Act / Assert. A short `// Given` `// When` `// Then` is fine when it aids reading; do not paste it mechanically on trivial tests.
## Build a small rig
When several tests share setup, write a factory (`rig()`, `createHost()`, whatever fits the codebase) that returns the **smallest surface the test needs**. Tests reach into the rig; they do not rebuild the world each time. Keep the rig dumb: wiring only, no assertions.
## Stub only the real external boundary
Default to real collaborators wired the way production wires them. Stub the **minimum seam** that is genuinely external:
- A remote / model / service boundary — spy on the contract method (the interface), and capture what the system sends across it. Do not stand up the real external thing.
- Network / other-process boundaries — stub at the boundary, not the internals.
- Time, timers, jitter — use the documented control knobs the system exposes (env, an injected clock, a manual tick). Do **not** use fake timers or real `setTimeout` to drive time.
- Env / config knobs are usually snapshotted at bootstrap — set them **before** building the system under test, and restore them in `afterEach`.
## Keep tests DAMP and keep cause next to effect
- DAMP over DRY: use **literal expected values** in assertions; do not compute the expectation with the same logic as the code under test.
- Keep the key preconditions inside the `it` (or its rig), where the reader can see cause next to effect. Reserve `beforeEach` for cross-cutting plumbing (env snapshot, cleanup), not for hiding the scenario's setup.
```ts
// Good — the expected value is a literal the reader can check.
expect(discount).toBe(15);
// Bad — re-derives the expectation; mirrors the implementation.
expect(discount).toBe(price * rate);
```
## Assert only what is relevant
Assert the effect that proves the contract. Use matchers / partial-object matching to ignore incidental fields. Do not assert internal counters, call orders, or shapes the user cannot rely on.
## Isolate and clean up (no flakes)
Every test must be hermetic and order-independent. In `afterEach`:
- restore every mock / spy
- restore every env var you touched (snapshot in `beforeEach`)
- dispose the host / container and reset its reference
No dependence on wall-clock time, run order, or leftover on-disk state — give each scenario its own isolated identity / workspace when state persists.
## Quality bar: CCCR
Before finishing, check each test against:
- **Clarity** — a stranger can tell what broke from the failure message alone.
- **Completeness** — covers the responsibility's success, error, and boundary paths.
- **Conciseness** — no duplicate or speculative cases; one scenario per `it`.
- **Resilience** — survives an internal refactor with no test change (because it asserts contract, not implementation).
## Per-file scenario header
Start each test file with a short header comment: the **scenario**, the **responsibilities** asserted, the **wiring** (which collaborators are real vs. the single stubbed boundary), and how to run it.
## Review mode — auditing existing tests
Apply the rules above as a checklist against each test in scope (a file, a diff, or a named `it`). For every hit, report `file:line` + the rule it breaks + the fix; do not rewrite unless asked. Lead with the contract question: *what observable behavior does this test prove, and would it survive a refactor?*
Check, in order:
1. **Contract, not implementation** — asserts observable effects, not private fields, call order, or internal shapes the user cannot rely on.
2. **One behavior per `it`** — the name carries behavior + condition + outcome; "and" in the name means a split is owed.
3. **Boundary discipline** — only the true external seam is stubbed; time is driven by documented knobs, not fake timers / real `setTimeout`.
4. **DAMP expectations** — expected values are literals, not re-derived by the code under test's logic.
5. **Isolation** — mocks / spies / env / host restored in `afterEach`; no wall-clock, run-order, or leftover on-disk dependence.
6. **CCCR read-through** — Clarity, Completeness (success / error / boundary), Conciseness, Resilience.
Report findings as evidence + fix, e.g. "`foo.test.ts:42` asserts on `service.internalMap` (contract) — assert the returned value instead." If a test passes the lens, say so briefly; silence on a rule means it held.
## Quick checklist (write & review)
- Resolved through the contract; no concrete-impl import
- One behavior per `it`; name carries behavior + condition + outcome; AAA
- Stubbed only the true external seam; time via knobs, not fake timers
- Literal expectations; relevant assertions only
- Mocks / env / host restored in `afterEach`; hermetic, no flakes
- CCCR read-through done

View file

@ -5,7 +5,7 @@ description: Use when generating changesets in the kimi-code repository — deci
# Generate Changesets
The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, kosong, oauth, telemetry, and so on) are internal.
The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, agent-core, kosong, kaos, oauth, telemetry, and so on) are internal.
## 1. Whether to Write
@ -32,7 +32,6 @@ Fix occasional loss of tool call results in long conversations.
Wording:
- One short, user-facing English sentence that states only what changed. Drop trailing clauses that explain the cause, the benefit, or the mechanism.
- When a default flips or an existing behavior is removed, name the behavior users lose, not only the new default. State the escape hatch if one exists, e.g. `Stop hot-reloading config, skills, and AGENTS.md by default; set [watch] enabled = true or KIMI_CODE_WATCH=1 to turn it back on.`; if there is none, say plainly that the old behavior is no longer available.
- New features: say plainly what it is plus one line on how to use it, e.g. `Add the /foo slash command to list active sessions. Run /foo to see them.`
- Experimental features: also state how to enable them (the flag, config key, or env var).
- No file, class, or function names, and no PR numbers. No vague words like refactor, optimize, or improve. No real internal identifiers — use neutral placeholders such as `example.com` or `YOUR_API_KEY`.
@ -61,4 +60,4 @@ Wording:
4. **Show the changeset text to whoever requested the work and get their confirmation before committing.**
5. Do not guess at changes you do not understand: finish the parts that are clear, then list what is unclear and ask whether you may dig into the code.
Before a release, review the accumulated `.changeset/` entries and delete the non-user-facing ones — the release PR regenerates from `.changeset/` on the selected release branch, so deleting a file removes its changelog entry without touching shipped code.
Before a release, review the accumulated `.changeset/` entries and delete the non-user-facing ones — the release PR regenerates from `.changeset/` on main, so deleting a file removes its changelog entry without touching shipped code.

View file

@ -13,8 +13,6 @@ This skill reuses `sync-changelog`'s strip / classify / translate rules. Read `s
### 1. Locate the release PR
Select the PR targeting the intended `release/*` branch, not the legacy main version PR. Report its latest head CI link and result alongside the preview.
```bash
gh pr list --state open --search "ci: release packages in:title" \
--json number,title,url,headRefName,baseRefName

View file

@ -1,192 +0,0 @@
---
name: review-pr
description: 在 kimi-code 仓库里 review 一个 PR 时使用:按 PR 模板逐节核对描述与 diff,并单独做一轮"回归与用户影响"评审,给出影响等级与评审摘要,评审摘要用用户当前使用的语言。
disable-model-invocation: true
---
# Review PR
这个仓库过去的回归大多不是逻辑错误,而是"改动相对于自身意图是对的,但悄悄改掉了某个现有人群依赖的行为":方言分支收窄、默认值翻转、删 system prompt 句子、重构或移植时丢掉旧路径的功能、修复引入启动时序变化。通用 review 会把这类"有意的功能变更"排除在 bug 之外,所以这里把它单列成一轮。
评审判据只有一条:**改动前能工作的任何输入(配置键、环境变量、命令行参数、provider 响应形态、旧版本写的会话数据、客户端请求、hook 载荷)改动后必须行为不变,除非 PR 明确声明了这个变化并给出了退路。**
## 工作流
1. 拉 PR 元数据与描述:
```bash
gh pr view <number> --json title,body,url,files,additions,deletions,baseRefName,headRefName
```
2. 读完整 diff(大 PR 按文件分批读):
```bash
gh pr diff <number>
```
3. 读足够的周边代码来验证描述里的每个断言。只看 diff 看不出"改动前谁走到这个分支"。
4. **第一遍:定性。** 判断改动类型,决定第三遍的深度,见"第一遍:定性"。
5. **第二遍:按模板逐节核对。** 见"模板逐节核对"。
6. **第三遍:回归与用户影响。** 尽量用一个独立子 agent 跑:只给它 PR 号、base 与 head、两三句改动简介和 `surfaces.md` 的路径,不给它作者关于影响范围的结论,让它先从 diff 和代码独立列清单,回来后再与作者填的表对比。没有子 agent 能力时自己按同样顺序做:先列清单,再读作者的表。见"第三遍:回归与用户影响"。
7. 用用户当前使用的语言写评审摘要,格式见"输出"。
## 第一遍:定性
| 改动类型 | 第三遍深度 |
|---|---|
| 纯内部重构、测试、文档、测试 CI | 只需给出"没有可观测变化"的证据:哪些分支、默认值、契约文件没动 |
| 发布与打包:release、native 构建、VS Code 发布工作流,打包脚本,web 产物同步 | 全做,人群按安装方式与平台分:npm 包、各平台 native 二进制、VS Code 市场、内置 web 产物 |
| 新增功能,不动既有路径 | 只查新分支是否抢走了既有输入:新增条件是否排在旧条件之前 |
| 行为变更、默认值翻转、校验或权限收紧或放宽 | 全做 |
| prompt 文本变更:system prompt、工具描述、提醒、overlay、内置 skill | 全做,且每一句改动单独成行 |
| 移植、替换旧路径、大重构:v1 到 v2、runtime binding、协议替换 | 全做,并要求作者给出旧路径功能清单 |
| 协议、磁盘格式、SDK 导出、hook 载荷、命令行契约变化 | 全做,并点名仓库外的消费方 |
| 修复类改动改了启动顺序、生命周期、异步时序 | 全做,重点看谁依赖原来的顺序 |
一个 PR 可以同时属于多类,按最重的那类处理。
## 第三遍:回归与用户影响
### 1. 行为变化清单
列出 diff 改变的每一个可观测行为,包括作者说"不变"的和作者认为"内部"的。内部包会打进 CLI 发布包,"内部"不等于用户无感。
每一行记录:行为、变化前、变化后、变化类型(新增 / 修改 / 删除 / 默认翻转 / 收紧 / 放宽)、证据(改动前后各一个 `file:line`,一侧不存在时给可达路径)。
读法:
- 对每个被删除或被收窄的分支、条件、默认值、句子,问"改动前谁走到这里,改动后他们去哪了"。
- 对每个新增的分支或条件,问"哪些既有输入现在会先命中新分支"。#3492 的回归就是这样产生的:数组字段存在时不再读字符串字段,而 OpenRouter 形态的网关两者同时发。
- 测试里"把旧断言改成新行为"的改动是行为变化的直接证据,逐条对应到清单。
- 先看 diff 文件列表有没有命中 `surfaces.md` 的"触发线索"。命中的 surface 必须在清单里有对应行,否则就是漏项。
### 2. 受影响人群
每一行对应到 `surfaces.md` 里的具体人群,写出人群名字和"这个人群存在"的证据:文档页、issue、配置示例、provider 载荷形态。找不到人群的,不进 finding,进"待作者确认"。
### 3. 影响等级
等级只由改动的事实决定,不看 changeset、文档、测试这些补救状态。三步判定:
1. 数据丢失、安全边界放宽、老数据不可读、某人群的功能整体不可用:直接 L3。
2. 否则,可感知且有一键退路(config、env、flag):L1;可感知且无退路,旧行为不可恢复:L2;无可观测变化:L0。
3. 静默升级:用户提供的输入(配置、环境变量、数据、请求字段)被忽略或丢弃,且没有任何错误信号,用户无法自行发现:升一级,最高 L3。行为公开地变了、用户能在输出里看到的,不算静默。
广度三档,必须写出人群名字:全体用户 / 某类配置或平台人群 / 边缘环境。
每一行单独定级,PR 的等级取最高。静默升级的理由:没有信号就没有上报,回归会一直存活到有人偶然发现。
### 4. 补救核对
固定四问,逐条回答有 / 无 / 不适用,并给出证据:
- 退路开关:有没有 config、env 或 flag 能恢复旧行为。
- changeset:有没有点明用户**失去了什么**,而不只是新默认是什么;没有退路时是否明说旧行为不可恢复。
- 老数据与老客户端:旧版本写的会话、迁移来的数据、上一版发布的客户端(desktop、web、VS Code、ACP)是否仍然可用;回滚到上一版是否安全。
- guard 测试:有没有一条测试把**未被改动的人群**的旧行为钉住。
再加一问:`docs/en` 与 `docs/zh` 里对这个 surface 的承诺是否同步更新。
### 等级对应的处理要求
- L3:建议阻塞。必须补 guard 测试,并提供退路、flag 门控或迁移,缺一不可合。
- L2:需要维护者在 PR 里显式拍板;changeset 必须写明失去的行为;文档同步。
- L1:核对 changeset 与文档即可。
- L0:一句话说明为什么是 L0,给出证据。
### finding 与"待作者确认"的界线
- finding 必须同时有:改动前后各一处 `file:line` 证据,和一个具名人群及其存在证据。两者缺一,降级为"待作者确认"。新增整个文件或删除整段实现时一侧天然为空,这时给存在的那一侧加上受影响可达路径的 `file:line` 即可。
- "待作者确认"最多 5 条,按可能的影响等级排序,多出来的只报数量。
- 不要报:PR 之前就存在的问题(严重的单独一句"顺带发现")、lint 和类型检查会抓的问题、风格问题。
- 不要把这些当成"无影响"的证据:作者说"不变"、没有测试引用被删的句子、改动只在内部包。
## 模板逐节核对
输出为中文时节名用下表的译名,其他语言沿用模板原文的节名:
| English | 中文 |
|---------|------|
| Requirement or Bug | 需求或 Bug |
| Bug Reproduction Steps | Bug 复现步骤 |
| Root Cause | 根本原因 |
| Code Changes | 代码变更 |
| Behavior Changes and Affected Users | 行为变化与受影响人群 |
| Checklist | 检查清单 |
旧 PR 若仍用 `Impact Scope` 这一节,按"行为变化与受影响人群"的标准核对。
### 需求或 Bug
- 关联的 issue 是否有效、相关?
- 没有 issue 时,需求是否用一两句话说清楚了?
### Bug 复现步骤
- bug 类 PR:步骤是否清晰、可复现?
- 按步骤能否在 base 分支上确认 bug 存在?
### 根本原因
- bug 类 PR:根因解释是否令人信服?
- 描述的根因与 diff 是否一致?
- 是根本修复还是绕过,是否说清楚了?
### 代码变更
- 描述与实际 diff 是否一致?
- 可视化大纲(diff 块、调用树、文件树)是否准确、有帮助?
- 方案是否合理?有没有更简单的替代?
- 有没有作者漏掉的边界情况?
### 行为变化与受影响人群
- 把第三遍独立列出的清单与作者填的表逐行对比:作者漏列的行为就是 finding;作者写了但你没找到证据的,进"待作者确认"。
- 人群是否具名,还是只写了"部分用户"?
- 写了 `None` 的,证据是否成立?
- 受影响模块与测试覆盖是否与 diff 文件列表一致?有没有带风险的未测路径?
### 检查清单
- 适用的项是否都勾了?
- 标为"不需要"的项,你是否同意?
## 输出
用用户当前使用的语言,按下面的顺序,示例为中文:
```markdown
## 结论
一句话:可合 / 需修改 / 建议阻塞。附 PR 影响等级与改动类型。
## 回归与用户影响
- 改动类型:…
- 影响等级:Lx(广度:…;是否静默:…)
| 行为 | 变化前 | 变化后 | 受影响人群 | 证据 | 等级 | 退路 |
|---|---|---|---|---|---|---|
补救核对:
- 退路开关:…
- changeset 是否点明失去的行为:…
- 老数据与老客户端:…
- guard 测试:…
- 文档:…
待作者确认(最多 5 条):
1. …
## 模板逐节核对
逐节说明是否填写充分,指出缺失、不准确、与 diff 不一致之处。
## 需要修改的项
可执行的列表;没有则写"无"。
```
## 复盘回填
每次线上回归复盘后,把新发现的人群或触发线索补进 `surfaces.md`,并在它的"历史案例"里加一行。清单不更新就会过时。

View file

@ -1,129 +0,0 @@
# 人群与 surface 清单
`review-pr` 第三遍用的仓库专属清单:谁在依赖现有行为,diff 里碰到什么就必须列行为变化。它会过时,每次回归复盘后补一行,见 `SKILL.md` 的"复盘回填"。
## 一、人群:谁在依赖现有行为
### 客户端与入口
| 人群 | 说明 | 契约位置 |
|---|---|---|
| TUI 交互用户 | `kimi` 默认入口 | `apps/kimi-code/src/tui/` |
| print 模式用户 | `kimi -p`,含解析 `--output-format stream-json` 输出的脚本。脚本解析者算二等人群,但输出格式变化必须写进 changeset(#3146) | `apps/kimi-code/src/cli/options.ts` |
| desktop 与 web 用户 | 源码在 code-app 仓库,只消费 kap-server 的 `/api/v1` REST 与 `/api/v1/ws`。本仓 diff 看不到它们的用法,改契约时必须点名让作者去对方仓库核对 | `packages/kap-server/src/routes/`、`packages/kap-server/src/transport/ws/`、`docs/en/reference/server-api.md` |
| VS Code 扩展用户 | 走 SDK 事件适配层,webview 有自己的渲染契约(#3250:v2 不再发 `display` 字段导致卡片空白) | `apps/vscode/` |
| ACP 客户端用户 | Zed 等,`kimi acp`。stdio MCP 是 ACP 的默认传输,客户端会无条件发(#3167);能力矩阵是公开承诺(#3081) | `packages/acp-server/`、`docs/en/reference/kimi-acp.md` |
| SDK 调用方 | `@moonshot-ai/kimi-code-sdk` 的导出面;其中 `KimiForCodingProvider` 走 kosong 的 provider 实现,CLI 与服务端不用 kosong | `packages/node-sdk/src/index.ts`、`packages/node-sdk/package.json` 的 `exports`、`packages/kosong/src/providers/` |
| 远程控制用户 | `kimi web --remote-control`,凭据槽位与 relay 按登录区域选择(#3969) | `packages/remote-control/` |
### provider 方言
| 人群 | 说明 |
|---|---|
| Kimi 官方 | 默认路径,OAuth 托管登录,区域与 relay 差异 |
| `openai` Chat Completions 兼容网关 | 最杂的一群:new-api 等中转、OpenRouter 形态(`reasoning` 字符串与 `reasoning_details` 数组并存,#3900)、只发 `reasoning_content` 的、严格校验历史消息的(#2819)、第三方模型对 `anyOf` 参数的处理(#3383) |
| `anthropic`、`openai_responses`、`google-genai`、`vertexai` | 各自的 thinking、工具调用、usage 字段形态 |
| 自定义模型配置 | `api_key_env`、models.dev catalog 导入、手动配置的 thinking 参数(#3448)、`[secondary_model]` 与 subagent model pool(#3192、#3903) |
代码位置:`packages/agent-core-v2/src/human/llm/requester/bases/`。
### 平台与环境
| 人群 | 已知敏感点 |
|---|---|
| Windows | 深路径与递归监听压力(#3931 的动机)、驱动器根与 UNC(#2876)、IME 组合输入(#3915)、CSI-u 键盘序列 |
| Linux | 无 hardlink 的文件系统(#3535)、根目录海量文件导致的 OOM(#3892) |
| macOS | 默认开发环境,最容易被当成"全体用户" |
| 网络代理 | 后台子 agent 重试(#3721) |
| native 二进制 vs npm 安装 | 启动路径、更新机制不同 |
| 安装渠道 | npm 包、各平台 native 二进制、VS Code 市场包、内置 web 产物,由发布与打包工作流决定,`src/` 一行不动也能丢掉一个平台 |
### git 仓库形态
| 人群 | 已知敏感点 | 契约位置 |
|---|---|---|
| 子模块或 worktree 内启动的用户 | `core.worktree` 已设置;路径含符号链接时(macOS `/tmp`、链接的 home、Windows 映射盘)realpath 与字面路径不一致。#3964 的 git 探针在这里整体失败,已随 #4013 回滚 | `packages/agent-core-v2/src/app/git/gitService.ts` |
| git-lfs、git-crypt 用户 | `.git/config` 里的 filter 驱动带 `required = true`;任何"清空 filter 命令"的加固都会让 `git status` 对已修改的被过滤文件直接 fatal,除非同时清 `required`。#3964 踩过,已随 #4013 回滚 | 同上 |
| 大仓库用户 | 依赖 `core.fsmonitor`;footer 的后台 git 有超时预算,超时后静默当作 clean | `apps/kimi-code/src/utils/git/` |
| 要求签名提交或 husky hooks 的仓库 | 任何自动提交路径(Tower 等)关闭 hooks 或签名都会改变提交结果 | `packages/agent-core-v2/src/features/tower/` |
### 配置状态
| 人群 | 说明 | 契约位置 |
|---|---|---|
| 默认配置用户 | 任何默认值翻转对他们都是行为变更 | `packages/agent-core-v2/docs/config-manifest.toml` |
| experimental flag 用户 | `[experimental]` 与 `KIMI_CODE_EXPERIMENTAL_*`;flag 的 `default` 翻成 true 等于面向全体发布 | `packages/agent-core-v2/src/app/flag/flagRegistry.ts`、各域的 `flag.ts` |
| legacy flag 用户 | `KIMI_CODE_LEGACY_FLAG`(#3820:TUI 与 print 模式静默忽略) | |
| 自定义 agent 用户 | `--agent` 的 `disallowedTools` 等策略(#3417) | |
| 配了 hooks 的用户 | 事件名与载荷是外部脚本的契约;配了 hook 时审批面板不渲染并被静默批准(#3888)、v2 不触发 PreToolUse(#3431) | `packages/agent-core-v2/src/features/externalHooks/internal/types.ts`、`docs/en/customization/hooks.md` |
| skills、plugins、MCP 用户 | 含 stdio MCP、用户级 skill 根目录、`[watch]` 热加载(#3931 关掉默认,#4015 翻回;关掉时连带 AGENTS.md 变更提醒失效,`/reload` 只覆盖 config 与 skills) | `docs/en/customization/` |
| 环境变量用户 | 同一个变量在 TUI、print 模式、kap-server 三个入口要行为一致(#3498:三个入口两种行为) | `docs/en/configuration/env-vars.md` |
| 主题、键位、TUI 模式自定义用户 | `tui.toml` | `docs/en/customization/themes.md`、`docs/en/reference/keyboard.md` |
### 数据年代
| 人群 | 说明 | 契约位置 |
|---|---|---|
| 旧版本写的会话 | wire 日志、turn 编号、压缩后的 transcript(#3561、#3866、#3922);新版本能否读、回滚后能否读 | `packages/agent-core-v2/docs/wire-manifest.d.ts`、`packages/agent-core-v2/docs/state-manifest.d.ts` |
| `kimi migrate` 迁来的会话 | 从 `~/.kimi/` 迁到 `~/.kimi-code/` | `packages/migration-legacy/` |
| 搜索索引 | minidb 快照与 WAL | `packages/minidb/` |
| 后台任务与 cron 持久化 | 非正常退出后的恢复(#3924) | |
| 目录布局 | 任何读写 `~/.kimi-code/` 的代码 | `docs/en/configuration/data-locations.md` |
### 外部自动化
| 人群 | 契约 |
|---|---|
| 解析 `stream-json` 的脚本 | 事件形态与顺序 |
| hook 脚本 | 事件名、载荷字段、返回值语义 |
| `kimi export` 的消费者 | 导出格式 |
| 关闭遥测的用户 | `KIMI_DISABLE_TELEMETRY` 在所有入口生效 |
### 模型行为人群
prompt 文本改动影响全体用户,且没有测试能证明"无影响"。已知依赖具体句子的人群:
- 依赖"不碰工作目录外文件"的(#3879 删除了这条规则)。
- 依赖"cwd 即项目根"的(#3929 删除了这条断言)。
- plan 模式提醒节奏(#3489)。
基于外部基准分析做的 prompt 改动,本质是把一个人群的收益换成另一个人群的损失,必须写明两边各是谁。
## 二、触发线索:diff 里碰到就必须列行为变化
| diff 里出现 | 代表的 surface | 必做的检查 |
|---|---|---|
| `packages/agent-core-v2/src/app/agentProfileCatalog/system.md`,以及 `packages/agent-core-v2/src/**/*.md`(工具描述、提醒、overlay、内置 skill) | 模型行为 | 每删或改一句单独成行:这句改动前约束了什么行为,谁依赖,现在由什么来约束 |
| `packages/agent-core-v2/docs/config-manifest.toml`(由 `src/app/config/configSectionContributions.ts` 生成,`test/app/config/configManifest.test.ts` 守护同步) | 配置键、默认值、校验 | 默认翻转与校验收紧必列;改了 manifest 没改 `docs/en/configuration/config-files.md` 视为文档过时 |
| `packages/agent-core-v2/src/**/flag.ts`、`packages/agent-core-v2/src/app/flag/flagRegistry.ts` | experimental flag | `default` 翻转等于面向全体发布,按行为变更处理 |
| `packages/agent-core-v2/src/human/utils/watch.ts`(热加载开关)以及任何模块级默认常量、`?? true` / `?? false` 兜底被配置键覆盖的地方 | 默认值来源 | 默认翻转必列;退路的 config 路径要有 guard 测试,不能只靠 env;退路是否依赖 ConfigService 先于消费方构造 |
| `packages/agent-core-v2/docs/wire-manifest.d.ts`、`packages/agent-core-v2/docs/state-manifest.d.ts` | 磁盘会话格式 | 旧会话能否读、回滚到上一版能否读、有无迁移 |
| `packages/kap-server/test/__snapshots__/apiSurface.snapshot.test.ts.snap`、`packages/kap-server/src/routes/`、`packages/kap-server/src/transport/ws/` | desktop、web、VS Code 的服务端契约 | 消费方在仓库外,点名让作者去 code-app 核对;上一版客户端连新服务端是否仍可用 |
| `packages/node-sdk/src/index.ts`、`packages/node-sdk/package.json` | SDK 导出面 | 删除或改签名即破坏性变更 |
| `packages/klient/src/` | SDK 与 ACP 下面的客户端门面契约 | 只改 klient 的 PR 碰不到 node-sdk 导出面,按 SDK 调用方与 ACP 客户端两个人群核行为变化 |
| `packages/transcript/src/contract/` | kap-server 发给 desktop、web、VS Code 的 transcript 载荷契约 | 消费方在 code-app,点名让作者去核;上一版客户端能否解析新载荷 |
| `.github/workflows/release.yml`、`_native-build.yml`、`manual-native-bundle.yml`、`vscode-publish.yml`,`apps/kimi-code/scripts/native/`、`apps/kimi-code/scripts/check-web-assets.mjs` | 发布产物 | 按安装方式与平台核:npm 包内容、各平台 native 二进制、VS Code 市场包、内置 web 产物是否仍齐全 |
| `packages/acp-server/` | ACP 客户端 | 对照 `docs/en/reference/kimi-acp.md` 的能力矩阵逐项核对 |
| `apps/kimi-code/src/cli/` | 命令行参数、子命令、`--output-format` | 对照 `docs/en/reference/kimi-command.md` |
| `packages/agent-core-v2/src/features/externalHooks/` | hook 事件与载荷 | 对照 `docs/en/customization/hooks.md` 的事件参考 |
| `packages/agent-core-v2/src/human/llm/requester/bases/` | provider 方言分支 | 枚举每种上游载荷形态改动前后各走哪条分支,尤其是多个字段并存的网关 |
| `packages/agent-core-v2/src/app/git/`、`apps/kimi-code/src/utils/git/` | 后台 git 的调用方式、config 覆盖、超时 | 对照"git 仓库形态"人群逐行核:前置探针失败是否让整个后台 git 静默不可用;覆盖的 config 键是否有 `required`、`gpgSign`、`hooksPath` 这类会把"禁用"变成"失败"的键;Windows 用例是否全部 skip |
| `packages/migration-legacy/`、`packages/minidb/`、任何读写 `~/.kimi-code/` 布局的代码 | 数据年代 | 老数据是否仍可读、可迁移 |
| 任何环境变量读取点的新增、删除或语义变化,不限写法(`process.env.X`、`process.env['X']`、动态键、helper)也不限 `KIMI_` 前缀 | 环境变量 | 对照 `docs/en/configuration/env-vars.md` 的全部条目;TUI、print 模式、kap-server 三个入口是否一致 |
| `packages/agent-core-v2/src/app/scopes.ts`、`packages/agent-core-v2/src/features/featureAssemblyService.ts`、config ready 相关 | 启动顺序与生命周期 | 谁依赖原来的顺序;#3471 在合入 34 分钟后被 #3486 整体 revert |
| `docs/en/**`、`docs/zh/**` 的 reference、configuration、customization 页 | 公开承诺 | 改了行为没改文档,或改了文档没改行为,都要指出 |
| 测试里把旧断言改成新行为 | 行为变化的直接证据 | 逐条对应到行为变化清单 |
## 三、历史案例:用来校准等级
| 类型 | 案例 | 当时漏掉的问题 | 事后等级 |
|---|---|---|---|
| 方言分支收窄 | #3492 引入,#3900 用户报告,#3910 修复 | PR 正文写明"只有没有数组时才读字符串",没人问谁会两个字段同时发 | L2 无退路,输入被静默丢弃升 L3,openai 兼容网关人群 |
| 默认值翻转 | #3931,五天后 #4015 翻回默认开 | changeset 只写新默认,没写运行中编辑 config、skills、AGENTS.md、agent 文件、mcp.json 不再自动生效;AGENTS.md 变更提醒随之失效,`/reload` 与 `/new` 不重读 AGENTS.md;review 未覆盖行为变化 | L1 有退路,但改了文件不生效也无任何信号,静默升 L2;changeset 没点明失去的行为,config 路径的退路无 guard 测试 |
| prompt 删句子 | #3879、#3929 | "没有测试引用这句"被当成无影响证据 | L2 无退路,全体用户;行为公开可见,不算静默 |
| 重构丢功能 | #2961 引入,#3081、#3167 用户报告,#3183 修复 | 重构删掉了 ACP 本地执行兜底与 stdio MCP 接受,文档承诺仍在 | L3,功能整体不可用,ACP 人群,报错响亮但无退路 |
| 移植漏项 | #3498、#3098、#3250、#3431、#3820 | v1 路径有、v2 路径没有:env 变量、上下文用量、卡片字段、hook、legacy flag;没人列过旧路径功能清单 | L2 无退路,配置被静默忽略升 L3 |
| 修复引入时序变化 | #3471 合入后 34 分钟被 #3486 revert | 延迟装配改变了启动顺序 | 全体用户 |
| 输出契约无声变化 | #3146 | headless 事件输出变了,没有 changelog | L2,脚本解析者 |
| 静默批准 | #3888 | 配了 hook 时审批面板不渲染,待审调用被静默批准 | L3,安全边界 |
| 安全收紧连带 | #3964(2.1.0),两天后 #4013 整体回滚 | 盲审列出四条:后台 git 探针在子模块 + 符号链接路径下整体失败;filter 驱动 `required = true` 仓库 `git status` fatal;print、ACP、SDK 无信任入口导致 `additional_dir` 静默为空;`~` 条目让 local.toml 整文件失效并静默退出。PR 正文未列任何人群 | L3 候选,四条均无退路、changeset 未提;回滚后代码已不在 main |

View file

@ -126,12 +126,7 @@ Public-text rule: do not copy real internal endpoints, key names, account names,
### 4. Merge, Deduplicate, And Classify Entries
First, count the stripped upstream entries in the version range being synced. The count decides how much curation happens before the human review checkpoint:
- **10 entries or fewer: list everything and let the user decide.** Classify every entry into its section and list them all — do not fold, merge, or drop entries on your own, and do not write a catch-all line. At the human review checkpoint (step 8), present the full list with advisory notes (which entries look low-signal under the two gates below, and why) so the user decides what stays, what merges, and what folds.
- **More than 10 entries: curate first, then review.** Merge related entries and fold low-signal ones into the catch-all line before writing the page, using the rules below. The reviewer still owns the final cutoff — surface the borderline calls instead of resolving them silently.
The rules below are the shared curation standard: they drive the curator's fold/merge decisions when there are more than 10 entries, and they drive the advisory notes when there are 10 or fewer.
Before classifying, merge related entries and drop redundant ones from the user-facing changelog:
- **Curate for end users: collapse low-signal entries into one catch-all line.** The docs changelog is the only curated, user-facing outlet; the full entry list always remains in the upstream package changelog, so hiding detail here loses nothing. Apply two gates to every candidate entry. Gate 1, the reader-action test: **after reading this, is there something the reader must do, or something they must re-evaluate?** Gate 2, the channel test: **is the changelog the only channel that can deliver this?** The changelog is the channel of last resort — when the product itself surfaces the information in context, at the moment of need, to exactly the affected users, the entry is redundant no matter how real the improvement is. "Surfaced" means pushed into the user's path, not merely present on screen: an event-triggered card, prompt, or post-install screen forces the encounter, while a toggle, menu item, command, or settings page only waits to be found. Users do not explore — a capability that lives only in ambient UI is effectively undiscoverable, so the changelog must announce it. What in-product surfacing cannot deliver: hidden controls (env vars, config keys, opt-out flags nobody would find unprompted), invalidations of existing habits or expectations (in-product discovery comes as confusion), and capabilities users would not know to seek. An entry that fails either gate folds. Anchor both gates to the changelog's reader, never to the bug's victim: someone who hit a loud failure does not need the changelog to confirm the fix — the product working again is the notification — and a reader who never hit it gets nothing from the entry.
- `Features`: keep when users would try it or must react to it — new capabilities create demand readers did not know to seek. Collapse only behavior that takes effect solely behind an experimental flag.
@ -329,7 +324,7 @@ Check:
- Each version has the same section set and order on both pages.
- Each section has the same number of entries on both pages.
- Within each section, the most valuable, obvious, and larger entries appear before smaller or narrower entries.
- Entry count decided the curation level: a version range with 10 or fewer upstream entries lists everything classified, with no catch-all line (fold/merge decisions deferred to the review checkpoint); with more than 10, low-signal entries were collapsed into the single catch-all line, placed last under `Bug Fixes` — or under `Polish` when nothing folded is a fix (both the reader-action test and the channel test applied); the catch-all wording matches what was folded and never claims fixes that did not happen; section sizes stay within the density defaults (about 2 Polish, 3 Bug Fixes) unless extra qualifying entries were deliberately kept and flagged for review. The catch-all line ends with the upstream changelog pointer (file link, no version anchor).
- Low-signal entries were collapsed into the single catch-all line, placed last under `Bug Fixes` — or under `Polish` when nothing folded is a fix (both the reader-action test and the channel test applied); the catch-all wording matches what was folded and never claims fixes that did not happen; section sizes stay within the density defaults (about 2 Polish, 3 Bug Fixes) unless extra qualifying entries were deliberately kept and flagged for review. The catch-all line ends with the upstream changelog pointer (file link, no version anchor).
- PR links and commit hashes were stripped.
- No `Thanks ...!` credit remains (remove it every time).
- Real internal identifiers were replaced with neutral placeholders.
@ -358,7 +353,7 @@ If the user chooses review:
git diff docs/en/release-notes/changelog.md docs/zh/release-notes/changelog.md
```
2. Summarize synced versions, section counts, and anything that needed manual classification. When the synced version range had 10 or fewer upstream entries and everything was listed, present the full entry list with advisory notes (which entries look low-signal and why) and ask the user to decide what stays, what merges, and what folds. When entries were folded into a catch-all line (more than 10 upstream entries), list every folded entry (short titles, one line each), any section that exceeds the density defaults, and every borderline call flagged during curation — the reviewer cannot own a cutoff they cannot see.
2. Summarize synced versions, section counts, and anything that needed manual classification. List every entry folded into a catch-all line (short titles, one line each), any section that exceeds the density defaults, and every borderline call flagged during curation — the reviewer cannot own a cutoff they cannot see.
3. Tell the user to reply when they are done reviewing, or to ask for edits.
4. Do **not** commit, push, or open a PR until the user explicitly says review is complete, or asks to proceed.
@ -439,7 +434,6 @@ Return the PR URL to the user when done.
- Never edit upstream `apps/kimi-code/CHANGELOG.md`.
- Do not backfill unreleased `.changeset/*.md` drafts into the docs site.
- If upstream wording is wrong, leave upstream alone and fix it in a future changeset.
- A version range with 10 or fewer upstream entries is listed in full; fold/merge/drop decisions belong to the user at the review checkpoint, not the curator.
- Always sync on a `docs/changelog-sync-*` branch and open a PR; never push changelog docs sync directly to `main`.
- Wait for the human review checkpoint before committing, pushing, or opening a PR.
@ -452,8 +446,7 @@ Return the PR URL to the user when done.
| Leaving the `Thanks ...!` credit in docs | Remove it every time, including the multi-author form |
| Leaving near-duplicate micro-tweaks as separate bullets | Merge small tweaks to the same surface into one higher-level entry (e.g. composer height + font → composer's default styling) |
| Listing many narrow fixes to the same surface as separate bullets | When three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry; keep genuinely distinct or high-value fixes standalone |
| Listing low-signal fixes or internal changes as standalone bullets when curating (>10 entries) | Collapse them into the single catch-all line (`Fix several known issues.`) placed last under Bug Fixes; treat the section-size defaults (about 2 Polish, 3 Bug Fixes) as a density guard, not a quota |
| Folding, merging, or dropping entries on your own when the version range has 10 or fewer upstream entries | List everything classified, with no catch-all line, and let the user decide at the review checkpoint |
| Listing low-signal fixes or internal changes as standalone bullets | Collapse them into the single catch-all line (`Fix several known issues.`) placed last under Bug Fixes; treat the section-size defaults (about 2 Polish, 3 Bug Fixes) as a density guard, not a quota |
| Folding a qualifying entry just to hit the section-size default | The defaults are density guards; keep entries that genuinely pass the reader-action test and flag the overflow for the human reviewer |
| Keeping a fix because it was severe or hard-won | Severity makes the announcement redundant — the fix itself notifies whoever was hit; keep only behavior-change fixes and retrospective notices with a concrete, locatable action |
| Keeping an improvement the product surfaces in context (recovery cards, post-install guidance, progress displays) | The product is the better channel — right users, moment of need; fold it (channel test) |

View file

@ -1,140 +0,0 @@
---
name: write-pr
description: 在 kimi-code 仓库里创建或更新 PR 时使用:如何把 PR 模板的每一节写得简洁、便于 reviewer 理解,包括"行为变化与受影响人群"表。
---
# 写 PR 描述
为当前分支创建或更新 PR,描述要让 reviewer 明白这个改动为什么存在、实现长什么样、现有用户会失去什么。
## 工作流
1. 读 PR 模板:
`Read(.github/pull_request_template.md)`
2. 找到或创建 PR:
- 先查当前分支有没有 PR:`gh pr view --json url,number,title,state 2>/dev/null`。
- 没有的话,看 `git status --short --branch` 和当前分支上的提交。
- 提交剩余改动,带 upstream 推送分支,用 `gh pr create` 创建 PR。
- 遵守仓库的 git 安全规范。
3. 收集解释改动所需的上下文:
- 读关联的 issue 和相关任务产物。
- 读完整 diff(`git diff main...HEAD`)和足够的周边代码,理解行为与归属。
- PR 已存在时,用 `gh pr view` 拿元数据和改动文件列表。
4. 按模板各节写描述。描述用用户当前使用的语言,行为变化表也一样;只有三样固定:PR 标题保持英文 Conventional Commit,changeset 保持英文(见 `gen-changesets` skill),节标题沿用模板原文。
- **Requirement or Bug**:一句话,或 `Resolve #<number>`。不要多写。
- **Bug Reproduction Steps**:只有 bug 类 PR 需要;feature 写 `N/A`。步骤已在 issue 里的写 `See linked issue`。
- **Root Cause**:只有 bug 类 PR 需要;feature 写 `N/A`。说明根因,以及这是根本修复还是绕过。
- **Code Changes**:能用可视化大纲说清楚的就不要写散文,见"代码变更的可视化大纲"。
- **Behavior Changes and Affected Users**:先填行为变化表,再列受影响模块与测试覆盖,见"行为变化与受影响人群"。
- **Checklist**:勾上所有适用的项。
5. 发布描述:
- 先存到临时文件,再 `gh pr edit <number> --body-file <path>` 或 `gh pr create --body-file <path>`。
- 确认更新成功。
## 行为变化与受影响人群
这一节回答一个问题:合入之后,现有用户会失去什么。这个仓库过去的回归大多来自没人回答这个问题,而不是逻辑错误。
判据:**改动前能工作的任何输入(配置键、环境变量、命令行参数、provider 响应形态、旧版本写的会话数据、客户端请求、hook 载荷)改动后必须行为不变,除非这里明确声明了变化并给出退路。**
写法:
1. 列出 diff 改变的每一个可观测行为,一行一个。包括你认为"不变"但分支条件挪动了的:新增的条件会不会先于旧条件命中既有输入;删除或收窄的分支改动前谁在走。内部包会打进 CLI 发布包,"内部"不等于用户无感。表格用 PR 描述的语言,下面的示例用英文只是示意形状:
| Behavior | Before | After | Who relies on the old behavior | Escape hatch |
|---|---|---|---|---|
| reasoning parsing when `reasoning_details` is an array | string `reasoning` also read | string ignored | OpenAI-compatible gateways that send both (OpenRouter dialect) | none |
2. 人群要具名,不要写"部分用户"。从 `.agents/skills/review-pr/surfaces.md` 里取:客户端(TUI、print 模式、desktop 与 web、VS Code、ACP、SDK)、provider 方言、平台、配置状态、数据年代、外部脚本。
3. 改了 prompt 文本(system prompt、工具描述、提醒)的,每一句单独成行,写清这句改动前约束了什么行为、谁依赖、现在由什么来约束。"没有测试引用这句"不是无影响的证据。
4. 替换或绕过旧路径的(移植、runtime binding、协议替换),额外列出旧路径的功能清单:它认哪些环境变量、有哪些兜底、接受哪些输入,以及每一项在新路径里的位置。
5. 确实没有可观测变化的,写 `None`,并说明证据:哪些分支、默认值、契约文件没动。
6. 表后面列受影响模块与每一行对应的测试覆盖。默认翻转或删除行为的,必须有一条测试把未被改动的人群的旧行为钉住,并且 changeset 要写明用户失去了什么,见 `gen-changesets` skill;没有退路的,在 PR 里请维护者显式拍板。
## 代码变更的可视化大纲
优先用结构化视图,少用散文。用能说清实现的最小组合,没变的类别不要列。
逻辑或算法变化用伪代码 diff:
```diff
on(save)
- write content
+ if content is unchanged
+ return cached result
+ write new content
+ invalidate cache
```
运行时控制流用调用树 diff:
```diff
submitForm
createSession
persistPrompt
+ expandSkillMention
launchAgent
- navigateToSession
+ navigateToSession
+ subscribeToEvents
```
文件职责变化用浅层文件树 diff:
```diff
src/
├── commands/
+│ └── show-me.ts # expands the slash command
├── sessions/
-└── transport.ts
+└── transport/
+ ├── client.ts
+ └── stream.ts
```
组件或 UI 结构变化用树 diff:
```diff
<SessionPage>
useSessionEvents()
<SessionToolbar>
+ <RunSkillButton />
<SessionTimeline>
+ <SkillResultCard />
```
组件交互、控制流或数据流用 Mermaid,解释 bug 机制时尤其有用:
```mermaid
sequenceDiagram
participant User
participant UI
participant Daemon
User->>UI: choose command
UI->>Daemon: send expanded prompt
Daemon-->>UI: stream result
```
关键数据结构或类型变化用对应语言的代码块:
```ts
interface SessionEvents {
onTurnStart(cb: (turn: Turn) => void): void;
onTurnEnd(cb: (turn: Turn) => void): void;
}
```
可视化大纲的规则:
- 重点是"变了什么"且周边形状已存在时,用 `diff` 块。
- 大部分是新内容,或 diff 记法会掩盖归属和顺序时,用对应语言或 `text` 块给出完整目标形状。
- 按最容易理解的顺序讲:先文件还是先数据结构,哪个合适用哪个。
- 像一个人对另一个人讲话:简单、连贯、简洁。

View file

@ -23,7 +23,7 @@ For any list dialog, selector, input box, or status/toggle list, the interaction
- `editor-keyboard.ts` — editor keyboard handling, exit shortcuts, external editor, clipboard image.
- `auth-flow.ts` — login/auth orchestration (`refreshConfigAfterLogin`, etc.).
- `src/tui/commands/` — slash-command declaration, parsing, ordering, and dynamic skill-command generation. Parsing and types only; execution is dispatched from `KimiTUI`'s slash-command handler section, and complex execution sinks into `utils` or focused components.
- `src/tui/components/` — pi-tui components by UI type: `chrome/` (footer, todo, welcome, loader, device code), `dialogs/` (selectors, approval/question panels, settings popups that replace the editor), `editor/` (input box + mention provider), `media/` (image, diff, code highlight), `markdown/` (the app's Markdown wrapper and mermaid art), `messages/` (transcript blocks + tool-renderers), `panes/` (activity, queue).
- `src/tui/components/` — pi-tui components by UI type: `chrome/` (footer, todo, welcome, loader, device code), `dialogs/` (selectors, approval/question panels, settings popups that replace the editor), `editor/` (input box + mention provider), `media/` (image, diff, code highlight), `messages/` (transcript blocks + tool-renderers), `panes/` (activity, queue).
- `src/tui/reverse-rpc/` — adapts SDK approval/question callbacks into UI panel data and the user's choice back into an SDK response.
- `src/tui/theme/` — themes, color tokens, style helpers, pi-tui markdown theme, terminal-background detection. The single source of truth for color.
- `src/tui/utils/` — TUI-only utilities (need `TUIState` or a component). App-wide, UI-independent helpers go in `src/utils/`.
@ -68,7 +68,7 @@ Themes are managed centrally under `src/tui/theme/`:
- `bundle.ts` — packs `colors`, `styles`, `markdownTheme` into a `KimiTUIThemeBundle`.
- `index.ts` / `detect.ts` — theme type and auto/dark/light resolution.
> **Keep the color-token set in sync.** `ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core-v2/src/features/skill/catalog/builtin/custom-theme.md`).
> **Keep the color-token set in sync.** `ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core/src/skill/builtin/custom-theme.md`).
Apply / switch flow:

View file

@ -15,10 +15,13 @@ Current publishable packages:
All other workspace packages are private internal packages, are not published to npm, and are excluded via `ignore` in `.changeset/config.json`:
- `@moonshot-ai/agent-core`
- `@moonshot-ai/kaos`
- `@moonshot-ai/kimi-code-oauth`
- `@moonshot-ai/kimi-telemetry`
- `@moonshot-ai/kosong`
- `@moonshot-ai/migration-legacy`
- `@moonshot-ai/protocol`
- `@moonshot-ai/vis`
- `@moonshot-ai/vis-server`
- `@moonshot-ai/vis-web`
@ -99,27 +102,23 @@ Commit messages must follow Conventional Commit style. Do not include any author
### 4. CI generates the release PR
Changesets accumulate on `main`. Create `release/<cycle>` from the selected commit; pushes to that branch use `changesets/action@v1` to create or update its release PR. Manual **Release** runs on a release branch only prepare the PR. The workflow explicitly dispatches CI for the generated version branch.
Once the changeset file is merged into `main`, `.github/workflows/release.yml` uses `changesets/action@v1` to create or update a release PR.
The release PR runs:
- `pnpm changeset version`: bumps publishable package versions and updates changelogs;
- Deletes the consumed `.changeset/*.md` files;
- Uses the title `ci: release packages`.
- Uses the title `[CI]: Release packages`.
### 5. Merge the release PR
Review the version PR, latest CI and CLI/extension pre-changelog together. Once the matching version PR is merged into `release/<cycle>`, publication waits for that exact commit’s push CI. Ordinary pushes do not publish. The existing publication steps then run:
Once the release PR is merged into `main`, the same workflow runs:
- `pnpm install --frozen-lockfile`
- `pnpm build`
- `pnpm changeset publish`
The packages are then published via npm Trusted Publishing, and a GitHub Release is created. CLI native builds still depend on Changesets reporting a newly published CLI package; npm-succeeded/native-failed whole-run recovery is not changed by this migration.
VS Code uses the same approved release commit and CI gate, publishing only when the extension version changed in that push. It retains its existing build and marketplace checks. It no longer publishes on main pushes or through a standalone manual entry. The documentation workflow is dispatched on main after CLI artifacts are uploaded; its deployment result is tracked separately. CDN remains manual.
Migration: merge the main infrastructure PR first to disable the old publisher, then the release infrastructure PR. These PRs do not publish. Close the obsolete main version PR separately. After the actual release, synchronize versions, changelogs, lockfile and consumed changeset removals back to main, preserving later main work. Main synchronization does not publish npm or VS Code. Do not change Desktop's submodule pointer as part of this step.
The packages are then published via npm Trusted Publishing, and a GitHub Release is created.
## Manual Publishing (Not Recommended)
@ -144,9 +143,9 @@ The root-level `pnpm run publish` first runs typecheck, lint, sherif, test, buil
- Every PR that affects publishable-package behavior or public API should include a corresponding changeset.
- Changes under `plugins/` (the bundled official plugins such as `kimi-datasource`) do **not** need a changeset: each plugin carries its own version in `kimi.plugin.json` and `plugins/marketplace.json` and is distributed via the marketplace CDN, separately from the `@moonshot-ai/kimi-code` npm package.
- Changeset files must be committed to the repository — release PRs are only triggered after they're merged.
- Release PRs require human review and approval. They do not publish before merge; merging the approved PR automatically publishes after its commit passes CI.
- Release PRs require human review and merge; they will not publish automatically.
- Do not add release changesets for private internal packages; only select `@moonshot-ai/kimi-code` and `@moonshot-ai/kimi-code-sdk`.
- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/kosong` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix.
- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/agent-core` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix.
- `@moonshot-ai/kimi-code` is the official CLI package name; after a global install it provides the `kimi` command.
- Make sure each publishable package on npm has a Trusted Publisher configured.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": minor
---
Allow expanding thinking during streaming with Ctrl-O

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix missing dynamically loaded MCP tools in the first conversation turn.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": patch
---
Update cross-tool imports for custom homes, instruction duplicate checks, project skills, and MCP settings; stop importing standalone `.claude/AGENTS.md`, project `.codex/AGENTS.md`, `.codex/CLAUDE.md`, and loose skill files, which can still be copied manually.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": minor
---
Sessions in web and desktop now unload from memory after 30 minutes idle and resume transparently on next access.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": minor
---
Plan mode now starts only when you turn it on, and every plan waits for your review: the agent can no longer enter Plan mode by itself, Never Ask no longer approves plans automatically, and `kimi -p` skips `default_plan_mode` and cannot approve the plan of a session that is already in Plan mode.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": minor
---
Add `/provider refresh [providerId]` to re-fetch provider model lists at runtime; unknown `/provider` subcommands now show a usage hint instead of opening the manager.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": patch
---
Speed up session resume for long conversations.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": minor
---
Raise Tower's default agent concurrency limit from 16 to 256, support the existing `max_running_tasks` limit and per-spawn model selection, and reduce lock contention during inbox, review, and status processing.

View file

@ -1,5 +0,0 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix the conversation view freezing at an old point after restarting the app when a turn had been interrupted; interrupted turns are now marked instead of shown as completed.

View file

@ -6,40 +6,24 @@ External PRs are accepted for approved bug fixes only: link an issue that a main
See https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md for more.
-->
## Requirement or Bug
## Related Issue
<!-- If there's an issue, write Resolve #(issue_number).
If it's a requirement, describe it briefly in plain language (under 100 characters). -->
<!-- Link the issue this change came from. External PRs must link an issue approved by a maintainer (an `/approve` comment) — PRs without one may be closed. -->
## Bug Reproduction Steps
Resolve #(issue_number)
<!-- Required for bug PRs only; write N/A for feature PRs.
Prefer writing in the issue and linking here; if no issue exists, write directly. -->
## Problem
## Root Cause
<!-- What user need or limitation does this address? If the linked issue already covers this, write "See linked issue". -->
<!-- Required for bug PRs only. Explain the root cause.
State whether this is a fundamental fix or a workaround. -->
## What changed
## Code Changes
<!-- Describe the code changes in plain, easy-to-understand language for the reviewer. -->
## Behavior Changes and Affected Users
<!-- List every observable behavior this PR changes, one row each, including behavior you
consider "unchanged" whose branch conditions moved, and prompt-text edits sentence by sentence.
| Behavior | Before | After | Who relies on the old behavior | Escape hatch |
Name affected users concretely (client, provider dialect, platform, config state, data written
by older versions, external scripts); see .agents/skills/review-pr/surfaces.md.
Write "None" with evidence if nothing observable changes.
Then list affected modules and the test coverage for each row. -->
<!-- What did you implement, and why does this approach fit Kimi Code? -->
## Checklist
- [ ] I have read the [CONTRIBUTING](https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md) document.
- [ ] I have linked a related issue (external PRs: issue must have a maintainer's `/approve`).
- [ ] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`).
- [ ] I have added tests that prove my feature works.
- [ ] The behavior-change table above is complete, and every removed behavior or flipped default is named in the changeset and either has an escape hatch or was explicitly approved by a maintainer in this PR.
- [ ] Ran `gen-changesets` skill, or this PR needs no changeset.
- [ ] Ran `gen-docs` skill, or this PR needs no doc update.

View file

@ -18,11 +18,6 @@ on:
required: false
type: boolean
default: false
sign-windows:
description: 'Whether to sign Windows builds with Azure Artifact Signing (requires AZURE_* secrets and AZURE_TRUSTED_SIGNING_* variables)'
required: false
type: boolean
default: false
secrets:
APPLE_CERTIFICATE_P12:
required: false
@ -34,12 +29,6 @@ on:
required: false
APPLE_NOTARIZATION_ISSUER_ID:
required: false
AZURE_TENANT_ID:
required: false
AZURE_CLIENT_ID:
required: false
AZURE_CLIENT_SECRET:
required: false
permissions:
contents: read
@ -107,14 +96,6 @@ jobs:
- name: Build native executable (local profile)
if: '!(runner.os == ''macOS'' && inputs.sign-macos)'
env:
KIMI_AZURE_TRUSTED_SIGNING: ${{ runner.os == 'Windows' && inputs.sign-windows && 'true' || 'false' }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ vars.AZURE_TRUSTED_SIGNING_ENDPOINT }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ vars.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: pnpm --filter @moonshot-ai/kimi-code run build:native:sea
- name: Notarize macOS binary

View file

@ -5,27 +5,11 @@ on:
push:
branches:
- main
- 'release/*'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}-${{ startsWith(github.ref, 'refs/heads/release/') && github.sha || '' }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
release-workflow-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- run: node --test scripts/release-trigger.test.mjs
build:
runs-on: ubuntu-latest
@ -157,10 +141,10 @@ jobs:
- run: pnpm install --frozen-lockfile
- name: Typecheck
run: |
pnpm exec tsc --version
pnpm dlx --package @typescript/native-preview@beta tsgo --version
for config in packages/*/tsconfig.json apps/kimi-code/tsconfig.json; do
echo "Typechecking ${config}"
pnpm exec tsc -p "${config}" --noEmit
pnpm dlx --package @typescript/native-preview@beta tsgo -p "${config}" --noEmit
done
- name: Typecheck VS Code extension
run: pnpm --filter kimi-code run typecheck

View file

@ -13,13 +13,9 @@ jobs:
upload-artifact-prefix: kimi-code-native
retention-days: 3
sign-macos: true
sign-windows: true
secrets:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}

View file

@ -2,88 +2,16 @@ name: Release
on:
push:
branches: ['release/*']
workflow_dispatch:
branches:
- main
concurrency:
group: release-${{ github.ref }}-${{ github.sha }}
cancel-in-progress: false
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
version:
if: github.repository_owner == 'MoonshotAI' && startsWith(github.ref, 'refs/heads/release/')
concurrency:
group: release-version-${{ github.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
actions: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: changesets/action@v1
id: version
with:
version: pnpm run version:release
commit: 'ci: release packages'
title: 'ci: release packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check version PR
if: steps.version.outputs.pullRequestNumber != ''
env:
GH_TOKEN: ${{ github.token }}
VERSION_BRANCH: changeset-release/${{ github.ref_name }}
run: gh workflow run ci.yml --ref "$VERSION_BRANCH"
candidate:
if: github.repository_owner == 'MoonshotAI' && github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/')
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
pull-requests: read
actions: read
checks: read
outputs:
publish: ${{ steps.trigger.outputs.publish }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Wait for merged version commit CI
id: trigger
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/release-trigger.mjs
vscode:
needs: candidate
if: needs.candidate.outputs.publish == 'true'
permissions:
contents: read
uses: ./.github/workflows/vscode-publish.yml
secrets:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
release:
name: Release
runs-on: ubuntu-latest
needs: candidate
if: needs.candidate.outputs.publish == 'true'
if: github.repository_owner == 'MoonshotAI'
outputs:
packages_published: ${{ steps.changesets.outputs.published }}
kimi_native_release: ${{ steps.kimi-release.outputs.should_publish }}
@ -143,6 +71,16 @@ jobs:
env:
CHANGESETS_PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }}
deploy-docs:
name: Deploy docs
needs: release
if: needs.release.outputs.packages_published == 'true'
uses: ./.github/workflows/docs-deploy.yml
permissions:
contents: read
pages: write
id-token: write
native-artifacts:
name: Native release artifact
needs: release
@ -152,16 +90,12 @@ jobs:
upload-artifact-prefix: kimi-code-native
retention-days: 7
sign-macos: true
sign-windows: true
secrets:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
publish-native-assets:
name: Publish native release assets
@ -194,15 +128,3 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.release.outputs.kimi_release_tag }}
run: gh release upload "$RELEASE_TAG" dist-native-release/* --clobber
deploy-docs:
needs: publish-native-assets
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Deploy existing main documentation
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh workflow run docs-deploy.yml --ref main

View file

@ -1,16 +1,35 @@
name: Publish VS Code extension
on:
workflow_call:
secrets:
VSCE_PAT:
required: false
OVSX_PAT:
required: false
# 把 0.7.3 手动发版流程固化成 CI 流水线:
#
# pnpm install → package:platform(全 6 平台)→ package:verify
# → extension host 冒烟(xvfb)→ publish:vsix → publish:ovsx
#
# 触发语义(参照 kimi-code-app 仓 desktop release.yml 的判定方式):
# 只在"本次 push 改动了 apps/vscode/package.json 的 version"时发布,
# 即 changesets 版本 PR("ci: release packages")合入后自动触发;
# 普通 PR 合入不会改变版本号,不会发布。版本号比较以 push 事件的
# before SHA 为基准(git show),可覆盖一次 push 含多个 commit 的情况,
# 不依赖工作区状态。workflow_dispatch 为手动兜底,
# 跳过版本号变化检查,直接走后续幂等闸门。
#
# 幂等(两道保险,可安全重跑,失败即停不自动重试):
# 1. 发布前逐平台核验 VS Marketplace(vsce show)与 Open VSX(REST)
# 线上版本:6 个 targetPlatform(darwin/linux/win32 × x64/arm64)
# 全部在线才跳过对应发布步骤;任一平台缺失(如上次部分发布失败)
# 即放行给发布步骤补齐,重跑可补齐;
# 2. 发布脚本自身带 --skip-duplicate(vsce)/ already-exists 跳过(ovsx)。
#
# 前置条件:仓库管理员需先配置 secrets VSCE_PAT / OVSX_PAT,
# 未配置时 publish 步骤会失败,属预期。
concurrency:
group: vscode-publish-${{ github.ref }}
cancel-in-progress: false
on:
push:
branches:
- main
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
@ -19,14 +38,13 @@ jobs:
publish:
name: Publish VSIX to marketplaces
runs-on: ubuntu-latest
if: github.repository_owner == 'MoonshotAI' && startsWith(github.ref, 'refs/heads/release/')
if: github.repository_owner == 'MoonshotAI'
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
fetch-depth: 0 # 需要按 push 事件的 before SHA 取旧版本号比较
- name: Setup pnpm
uses: pnpm/action-setup@v6
@ -44,6 +62,7 @@ jobs:
id: gate
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
@ -52,20 +71,27 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "extension version: ${version}"
prev=""
if [ -n "${BEFORE_SHA}" ] && [ -n "${BEFORE_SHA//0/}" ]; then
if git show "${BEFORE_SHA}:${pkg}" > /tmp/prev-vscode-pkg.json 2> /dev/null; then
prev="$(node -p "require('/tmp/prev-vscode-pkg.json').version")"
fi
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
echo "manual dispatch — skipping version-change check"
else
echo "push event has no usable before SHA ('${BEFORE_SHA}') — treating as a version change"
# 以 push 事件的 before SHA 为比较基准(覆盖多 commit 的单次 push)。
# before 为空或全 0(如新分支首推)时无法比较,prev 视为空 →
# 判定版本变化放行 —— 宁可多查一次线上闸门,不可漏发。
prev=""
if [ -n "${BEFORE_SHA}" ] && [ -n "${BEFORE_SHA//0/}" ]; then
if git show "${BEFORE_SHA}:${pkg}" > /tmp/prev-vscode-pkg.json 2> /dev/null; then
prev="$(node -p "require('/tmp/prev-vscode-pkg.json').version")"
fi
else
echo "push event has no usable before SHA ('${BEFORE_SHA}') — treating as a version change"
fi
if [ "${prev}" = "${version}" ]; then
echo "apps/vscode version unchanged in this push (${version}) — nothing to publish"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "version changed ${prev:-<none>} -> ${version} — release push"
fi
if [ "${prev}" = "${version}" ]; then
echo "apps/vscode version unchanged in this push (${version}) — nothing to publish"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "version changed ${prev:-<none>} -> ${version} — release push"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
# 线上闸门:逐平台核验,6 个 targetPlatform 全部在线才跳过对应市场;

View file

@ -1,18 +0,0 @@
import { existsSync } from 'node:fs';
if (
process.env.NODE_ENV === 'production' ||
process.env.CI === 'true' ||
process.env.npm_config_production === 'true' ||
!existsSync('.git')
) {
process.exit(0);
}
try {
const husky = (await import('husky')).default;
console.log(husky());
} catch (error) {
if (error && error.code === 'ERR_MODULE_NOT_FOUND') process.exit(0);
throw error;
}

View file

@ -1,4 +0,0 @@
set -e
node scripts/check-no-comments.mjs
pnpm lint-staged

View file

@ -14,18 +14,19 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
## Project Map
- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on engine packages. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- the browser web UI: **its source no longer lives in this repo.** It is developed in the code-app repo (`apps/web`) and shipped as the committed, prebuilt bundle `apps/kimi-code/dist-web` (gitignored, force-added), synced from code-app with `KIMI_CODE_REPO=<this checkout> pnpm run sync:web` — sync and commit the bundle in the same change whenever the web UI should ship differently. `apps/kimi-code/scripts/check-web-assets.mjs` guards packaging against a missing bundle. To hack on the web UI against this repo's server, run `pnpm dev:server` here and point code-app's `pnpm dev:web` at it via `KIMI_SERVER_URL`.
- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays.
- `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session transcript chat, per-scope Service panels, and the DI unit inspection view. See `apps/kimi-inspect/AGENTS.md`.
- `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler.
- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. See `packages/agent-core/AGENTS.md`.
- `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler. See `packages/agent-core-v2/AGENTS.md` and use the `agent-core-dev` skill (`.agents/skills/agent-core-dev/SKILL.md`) when developing here.
- `packages/node-sdk`: the public TypeScript SDK and harness.
- `packages/kosong`: the LLM / provider abstraction layer.
- `packages/kaos`: the execution environment and file/process abstractions.
- `packages/oauth`: Kimi OAuth and managed auth utilities.
- `packages/telemetry`: shared client-side telemetry infrastructure.
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth).
- `packages/remote-control`: the Kimi Remote Control tunnel client — registers this machine with the relay and forwards HTTP/WebSocket traffic to the local server, with a machine-wide single-instance lock; consumed by kap-server (the `/api/v1/remote-control` toggle) and by the CLI (`kimi web --remote-control`).
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract. See `packages/transcript/AGENTS.md`.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth). See `packages/kap-server/AGENTS.md`.
- `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 (`global.*` / `session(id).*` / `agent(id).*`, zod-validated); transport via subpath entry (`@moonshot-ai/klient/ipc|memory`, both return the same `Klient`); also hosts the e2e suites. See `packages/klient/AGENTS.md`.
- `packages/tree-sitter-bash`: a pure-TypeScript bash parser (no runtime deps, no wasm); `parse(source, { timeoutMs, maxNodes })` runs under a deterministic budget and returns a discriminated `ParseResult` — callers must treat aborted/hasError trees as "cannot analyze" and degrade. Parser only, no safety judgments; see the package README's "Known differences" section.
- `packages/minidb`: the embedded JSON document store (`MiniDb`) behind kap-server's search index — snapshot + WAL persistence with an exclusive write lock, a larger-than-RAM full-text layer, and persistent index generations. See `packages/minidb/AGENTS.md`.
@ -55,7 +56,6 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- YES: `interface Options { user?: User }`
- NO: `interface Options { user?: User | undefined }`
- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.
- Split functions only along abstraction levels: each function reads as one level of narrative (Step-down Rule), and a wrapper that adds no new abstraction level — especially one with a single call site — is inlined instead of extracted.
- Except for a package's `index.ts`, other `index.ts` files should prefer `export * from './module';`.
- Do not add too many new test files. Prefer adding tests to the existing test file of the corresponding component or module.
- When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug.
@ -64,7 +64,8 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
## Experimental Features
- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Precedence is per-flag env > `[experimental]` config > master env > the flag's `default`. Release by flipping the entry's `default` to `true`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time, then check it with `IFlagService.enabled(id)`.
- `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on).
## Where to Update Instructions
@ -73,17 +74,12 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- Project-map entries stay at 1–2 sentences; deep package docs live in the package's own `AGENTS.md`.
- Keep instruction updates focused and supported by code facts.
## Release Workflow
- `main` accumulates changesets; `release/*` maintains `ci: release packages` PRs. Merging the matching version PR authorizes publication after its merge-commit CI succeeds. Ordinary pushes do not publish.
- npm publishing and native artifacts retain the existing Changesets flow. CDN upload and routing remain separately approved operations. See `.changeset/README.md` for migration and main synchronization.
## Workflow Requirements
- Prefer `rg` / `rg --files` when reading code.
- When designing changes, follow existing boundaries and local patterns first.
- In public text and test data, replace real internal identifiers with neutral placeholders such as `example.com`, `example.test`, and `YOUR_API_KEY`. Before opening a PR, ask a read-only agent to audit the diff for context-specific internal identifiers.
- When creating a PR, use the `write-pr` skill (`.agents/skills/write-pr/SKILL.md`) to write the PR description. The PR title must follow Conventional Commit style, e.g. `chore: remove legacy format commands`.
- When creating a PR, the PR title must follow Conventional Commit style, e.g. `chore: remove legacy format commands`.
- When an AI agent opens or updates a PR, fill in `.github/pull_request_template.md` — link the related issue or explain the problem, then describe what changed. Do not leave placeholder text or submit a generic summary of the diff.
- Do not submit vague AI-generated PR text. The human author must understand the change well enough to explain the code, edge cases, and why the approach fits this repository.
- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules.
@ -94,39 +90,3 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- Agent working notes or handoff/summary documents (e.g. `HANDOVER-*.md`, `HANDOFF-*.md`, `handoff.md`).
- Throwaway UI/UX prototypes or design mockups (e.g. `*-designs.html`, `*-mockup.html`, `*-demo(s).html`) at the repo root or under a `design/` folder. The only tracked `.html` files should be Vite `index.html` entrypoints.
Before committing or opening a PR, run `git status` and `git diff --staged --stat` and remove anything matching these patterns. Put scratch work under `.tmp/` (gitignored) instead of the repo root or the source tree.
## Code Review Rules
These rules apply to every pull request review, automated or human. The user populations and contract files they refer to are listed in `.agents/skills/review-pr/surfaces.md`.
### Review scope: the PR's diff, not the repo
- Review only the PR's diff against its base. A problem in code the PR does not touch (pre-existing on base) is reported as a non-blocking note, never as a blocking finding — the sole exception is pre-existing code that the PR's behavior directly triggers or amplifies.
- Report a class of issues in one pass: when a finding generalizes to several locations in the diff, list all of them at once instead of raising them across successive rounds.
- No pure style or personal-preference findings. Every finding carries a severity: P0 blocking / P1 should-fix / P2 suggestion.
- Re-reviews are incremental: review only what changed since the previous review; do not restate findings on unchanged code or re-raise resolved threads. Before treating a resolved thread as settled, verify the current code: suppress a finding only when the problem is actually fixed or the finding no longer holds.
- Domain-specific rules keep their own canonical homes — changesets (`.agents/skills/gen-changesets/SKILL.md`), comment-free zones (General Coding Rules), PR text (`write-pr` skill); review against those, not ad-hoc taste.
### Enumerate changed behavior, not just bugs
Any input that worked before the change — a config key, env var, CLI flag, provider response shape, session written by an older version, client request, or hook payload — must behave the same after it unless the PR declares the change. For every deleted or narrowed branch, condition, default, or prompt sentence, ask who reached it before and where they go now; for every new condition, ask which existing inputs now match it first. Flag a PR that calls a path "unchanged" when its branch condition moved.
### A flipped default or removed behavior needs a named loss and an escape hatch
Everyone on the old default is affected. Require the changeset to name the behavior users lose, not only the new default; a config, env, or flag escape hatch or a maintainer's explicit sign-off in the PR; and a test that pins the old behavior for the population that keeps it.
### Prompt text is behavior
Editing or deleting sentences under `packages/agent-core-v2/src/**/*.md` (system prompt, tool descriptions, reminders, overlays, built-in skills) changes agent behavior for every user who receives that prompt. "No test references the sentence" is not evidence of no impact. Require the PR to name the population that receives the text (every session, plan mode, a flag-gated feature such as Tower), what the sentence enforced, who relied on it, and what enforces it now.
### Contract files are tripwires
The manifests under `packages/agent-core-v2/docs/` (`config-manifest.toml`, `wire-manifest.d.ts`, `state-manifest.d.ts`), `packages/kap-server/test/__snapshots__/apiSurface.snapshot.test.ts.snap`, `packages/node-sdk/src/index.ts`, `packages/agent-core-v2/src/features/externalHooks/`, `packages/acp-server/`, and `apps/kimi-code/src/cli/` are consumed outside this repository: desktop and web in code-app, the VS Code extension, ACP clients such as Zed, SDK users, hook scripts, and headless-output parsers. When they change, require the PR to name the consumers and how data and clients from the previous release keep working.
### Ports and refactors carry the old path's feature inventory
When a change replaces or bypasses an existing path, require a list of what the old path did — env vars honored, fallbacks, accepted inputs — and where each item lives in the new path. A silently dropped item is a regression, not a cleanup.
### Silent failure outranks a crash
A change that makes the product silently ignore configuration, silently approve or skip an action, or silently drop data is the most severe finding: users get no signal to report.

View file

@ -30,7 +30,7 @@ This is a pnpm monorepo. The most relevant entry points are:
- `apps/vis` — session debug visualizer
- `packages/node-sdk` — public TypeScript SDK (`@moonshot-ai/kimi-code-sdk`)
- `packages/agent-core-v2` — the agent engine (v2, DI Scope architecture); `packages/agent-core` is v1 and being phased out
- `packages/klient`, `kap-server`, `protocol`, `transcript`, `kosong`, `oauth`, `telemetry` — internal engine packages
- `packages/klient`, `kap-server`, `protocol`, `transcript`, `kosong`, `kaos`, `oauth`, `telemetry` — internal engine packages
- `docs/` — VitePress bilingual docs site
For the full project map, see [AGENTS.md](AGENTS.md).
@ -60,12 +60,12 @@ All commits and PR titles must follow [Conventional Commits](https://www.convent
| Type | Use for | Example |
|----------|---------------------------------------------|-------------------------------------------|
| feat | A new feature | feat(agent-core-v2): add tool dedup |
| feat | A new feature | feat(agent-core): add tool dedup |
| fix | A bug fix | fix(tui): correct status bar alignment |
| docs | Documentation only | docs: clarify install instructions |
| chore | Tooling / housekeeping | chore: bump dependencies |
| refactor | Internal refactor without behavior change | refactor(kosong): extract retry helper |
| test | Adding or improving tests | test(agent-core-v2): cover skill resolver |
| test | Adding or improving tests | test(agent-core): cover skill resolver |
| ci | CI / build pipeline changes | ci: cache pnpm store |
| build | Build system / artifact changes | build(native): add win32-arm64 target |
| perf | Performance improvement | perf(session): batch event flushes |

View file

@ -30,7 +30,7 @@ Kimi Code 对 CLI/TUI 行为、agent 工作流和公开 API 已有自己的主
- `apps/vis` — 会话调试可视化工具
- `packages/node-sdk` — 公开 TypeScript SDK(`@moonshot-ai/kimi-code-sdk`)
- `packages/agent-core-v2` — 当前的 agent 引擎(v2,DI Scope 架构);`packages/agent-core` 为 v1,正在逐步废弃
- `packages/klient`、`kap-server`、`protocol`、`transcript`、`kosong`、`oauth`、`telemetry` — 内部引擎包
- `packages/klient`、`kap-server`、`protocol`、`transcript`、`kosong`、`kaos`、`oauth`、`telemetry` — 内部引擎包
- `docs/` — VitePress 双语文档站
完整项目地图见 [AGENTS.md](AGENTS.md)。
@ -60,12 +60,12 @@ pnpm install
| 类型 | 用途 | 示例 |
|----------|------------------------------------------|----------------------------------------|
| feat | 新功能 | feat(agent-core-v2): add tool dedup |
| feat | 新功能 | feat(agent-core): add tool dedup |
| fix | bug 修复 | fix(tui): correct status bar alignment |
| docs | 仅文档 | docs: clarify install instructions |
| chore | 工具 / 杂务 | chore: bump dependencies |
| refactor | 无行为变更的内部重构 | refactor(kosong): extract retry helper |
| test | 新增或改进测试 | test(agent-core-v2): cover skill resolver |
| test | 新增或改进测试 | test(agent-core): cover skill resolver |
| ci | CI / 构建流水线变更 | ci: cache pnpm store |
| build | 构建系统 / 产物变更 | build(native): add win32-arm64 target |
| perf | 性能优化 | perf(session): batch event flushes |

View file

@ -42,7 +42,7 @@ Main directories:
- `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response.
- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly.
- `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`.
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import engine packages directly in app code.
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import `@moonshot-ai/agent-core` directly in app code.
## TUI Coding Conventions

View file

@ -1,531 +1,5 @@
# @moonshot-ai/kimi-code
## 2.3.1
### Patch Changes
- [#4348](https://github.com/MoonshotAI/kimi-code/pull/4348) [`c569bab`](https://github.com/MoonshotAI/kimi-code/commit/c569bab55d4d510b09f01205d760001d16117aff) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix workspace trust failures when Git Bash is unavailable on Windows.
- [#4348](https://github.com/MoonshotAI/kimi-code/pull/4348) [`c569bab`](https://github.com/MoonshotAI/kimi-code/commit/c569bab55d4d510b09f01205d760001d16117aff) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix sessions occasionally remaining occupied after an application crash on macOS and Linux.
- [#4356](https://github.com/MoonshotAI/kimi-code/pull/4356) [`0e3980e`](https://github.com/MoonshotAI/kimi-code/commit/0e3980e75e9af9d28955a5e93cff82d616b11353) Thanks [@7Sageer](https://github.com/7Sageer)! - Show the holding client's name in session-in-use errors.
- [#4348](https://github.com/MoonshotAI/kimi-code/pull/4348) [`c569bab`](https://github.com/MoonshotAI/kimi-code/commit/c569bab55d4d510b09f01205d760001d16117aff) Thanks [@liruifengv](https://github.com/liruifengv)! - Offer to take over a session that is still in use after a retry, so sessions left locked by an exited process can be used again, and add a `KIMI_CODE_SESSION_LOCK=0` switch that turns off the session lock.
- [#4348](https://github.com/MoonshotAI/kimi-code/pull/4348) [`c569bab`](https://github.com/MoonshotAI/kimi-code/commit/c569bab55d4d510b09f01205d760001d16117aff) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improved interactions and fixed known bugs.
## 2.3.0
### Minor Changes
- [#4294](https://github.com/MoonshotAI/kimi-code/pull/4294) [`3f80896`](https://github.com/MoonshotAI/kimi-code/commit/3f80896a79c5b17867898bd829b6d7a78f569f47) Thanks [@liruifengv](https://github.com/liruifengv)! - Add a confirmation dialog when switching models in a session with conversation history: start a new session, compact and continue, continue directly, or cancel. Shown only when enabled by cloud configuration.
- [#4188](https://github.com/MoonshotAI/kimi-code/pull/4188) [`3f0857e`](https://github.com/MoonshotAI/kimi-code/commit/3f0857eb7d612c4890c45dfa6acc8381f1e761c2) Thanks [@oocz](https://github.com/oocz)! - List plugin skills under their qualified `<pluginId>:<skillName>` name so same-named skills from different plugins all stay visible and invocable; invoking a plugin skill by its bare name no longer works, use the qualified name instead.
- [#4214](https://github.com/MoonshotAI/kimi-code/pull/4214) [`ef727e5`](https://github.com/MoonshotAI/kimi-code/commit/ef727e517923cb312cb48d42825c49dfacd528e7) Thanks [@sailist](https://github.com/sailist)! - Support streaming large requests and responses through remote control, raising the transfer limit from 10 MiB to 100 MiB, with concurrent transfers bounded to protect memory.
- [#4288](https://github.com/MoonshotAI/kimi-code/pull/4288) [`df0ff3c`](https://github.com/MoonshotAI/kimi-code/commit/df0ff3c5492e8f896927c4af9b4ae00b2b488607) Thanks [@github-actions](https://github.com/apps/github-actions)! - When the same session is open in multiple clients, sending from a second one now shows an occupancy prompt with retry/cancel, or reloads updates made elsewhere first and resends your message.
- [#4200](https://github.com/MoonshotAI/kimi-code/pull/4200) [`5bab748`](https://github.com/MoonshotAI/kimi-code/commit/5bab748dddaae4b2ac15822d72b83446e5fd7bf4) Thanks [@7Sageer](https://github.com/7Sageer)! - Automatically generate a session title after the first turn and show it as the terminal window title.
### Patch Changes
- [#4250](https://github.com/MoonshotAI/kimi-code/pull/4250) [`6e7cc90`](https://github.com/MoonshotAI/kimi-code/commit/6e7cc909da9084a424f822dd5a9aae5b98070b73) Thanks [@sailist](https://github.com/sailist)! - Setting `[permission] dangerous_command_guard = false` now turns off every built-in confirmation guard — dangerous commands, sensitive-file access, and git control-path access — instead of only the dangerous-command guard.
- [#4281](https://github.com/MoonshotAI/kimi-code/pull/4281) [`5710c54`](https://github.com/MoonshotAI/kimi-code/commit/5710c5496a213742ca0687f69dd8eeb5bde52dbf) Thanks [@sailist](https://github.com/sailist)! - Fix session events stopping after a session is archived and restored.
- [#4201](https://github.com/MoonshotAI/kimi-code/pull/4201) [`83a39b4`](https://github.com/MoonshotAI/kimi-code/commit/83a39b49ed62627c7a604b39e26d8e9d17ade296) Thanks [@Grapedge](https://github.com/Grapedge)! - Stop blocking the terminal while the footer reads git status.
- [#4230](https://github.com/MoonshotAI/kimi-code/pull/4230) [`b260326`](https://github.com/MoonshotAI/kimi-code/commit/b2603264ec4687cee766cf5d5e6d4976e432961c) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Report a clear error when thinking effort is set to off for Gemini 4 models.
- [#4208](https://github.com/MoonshotAI/kimi-code/pull/4208) [`20b5991`](https://github.com/MoonshotAI/kimi-code/commit/20b59915bb11fc56ec389aa96720f1b7daac0862) Thanks [@SeleneXX](https://github.com/SeleneXX)! - Fix Gemini 4 requests always failing with a 400 error.
- [#4242](https://github.com/MoonshotAI/kimi-code/pull/4242) [`c1459a6`](https://github.com/MoonshotAI/kimi-code/commit/c1459a61845056832b85a0a220ee11a2bc5132f8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Run hooks such as `PreToolUse` and `Stop` in the session's project directory, as documented, instead of the directory the server was started from.
- [#4210](https://github.com/MoonshotAI/kimi-code/pull/4210) [`f28c51c`](https://github.com/MoonshotAI/kimi-code/commit/f28c51c01f025bba9f0d0f58f6309e7517a8cd02) Thanks [@sailist](https://github.com/sailist)! - Add the [mcp] default_deferred config key: when the experimental tool-select flag is on, set default_deferred = true in config.toml to load every MCP server's tools on demand by default; a server can opt out with deferred: false.
- [#4171](https://github.com/MoonshotAI/kimi-code/pull/4171) [`0327e8e`](https://github.com/MoonshotAI/kimi-code/commit/0327e8e9a875949fd40141ef79f902d756dfd3db) Thanks [@7Sageer](https://github.com/7Sageer)! - Support custom-registry model upgrade metadata in model configuration.
- [#4188](https://github.com/MoonshotAI/kimi-code/pull/4188) [`3f0857e`](https://github.com/MoonshotAI/kimi-code/commit/3f0857eb7d612c4890c45dfa6acc8381f1e761c2) Thanks [@oocz](https://github.com/oocz)! - Allow uppercase letters, dot-separated segments, and @ suffixes in plugin names when installing plugins.
- [#4294](https://github.com/MoonshotAI/kimi-code/pull/4294) [`3f80896`](https://github.com/MoonshotAI/kimi-code/commit/3f80896a79c5b17867898bd829b6d7a78f569f47) Thanks [@liruifengv](https://github.com/liruifengv)! - Queue /compact while the agent is busy: the command now joins the message queue and runs in order instead of failing with an error; it is skipped if the context was auto-compacted while it waited.
- [#4190](https://github.com/MoonshotAI/kimi-code/pull/4190) [`e4c99a3`](https://github.com/MoonshotAI/kimi-code/commit/e4c99a3bad19b00bd1939b1cf254f427b50061f0) Thanks [@starquakee](https://github.com/starquakee)! - Clarify unavailable tools, announce when no tools can be loaded, and discourage repeated failed tool selections.
- [#4272](https://github.com/MoonshotAI/kimi-code/pull/4272) [`ce0834a`](https://github.com/MoonshotAI/kimi-code/commit/ce0834a8f301b89cc4523499a3e074c36851de8f) Thanks [@Grapedge](https://github.com/Grapedge)! - Move the session rating opt-out from tui.toml to config.toml.
- [#4066](https://github.com/MoonshotAI/kimi-code/pull/4066) [`85077a4`](https://github.com/MoonshotAI/kimi-code/commit/85077a419001285e49ede3574924f001935aed62) Thanks [@7Sageer](https://github.com/7Sageer)! - Keep subagent tool descriptions aligned with currently available tools.
- [#3799](https://github.com/MoonshotAI/kimi-code/pull/3799) [`7e58f2d`](https://github.com/MoonshotAI/kimi-code/commit/7e58f2d2410d2b541ffd57f4501c0436bba4ccbd) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix the latest reply disappearing from the transcript after a background task completion notification arrives; the fold boundary is now anchored to the notification turn itself instead of the asynchronously mounted task card.
- [#4239](https://github.com/MoonshotAI/kimi-code/pull/4239) [`da5918c`](https://github.com/MoonshotAI/kimi-code/commit/da5918c0c861a1d670f4523a9f9dd968647fe4cb) Thanks [@RealKai42](https://github.com/RealKai42)! - Remind the agent to update its todo list after 5 steps without an update instead of 10, only while the list still has unfinished items, and ask it to bring the list up to date before its final reply.
- [#4248](https://github.com/MoonshotAI/kimi-code/pull/4248) [`8fcd332`](https://github.com/MoonshotAI/kimi-code/commit/8fcd3326e7488e5e0d4c595115ef74a10c340d5b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Show a single overflowing preview line in full instead of swapping it for a same-height expand hint.
- [#4304](https://github.com/MoonshotAI/kimi-code/pull/4304) [`d36071e`](https://github.com/MoonshotAI/kimi-code/commit/d36071ede6ae3cbdc08cbb8a0f7daf7bc6d06f2d) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Improved interactions and fixed known bugs.
## 2.2.0
### Minor Changes
- [#4159](https://github.com/MoonshotAI/kimi-code/pull/4159) [`0a3a2fb`](https://github.com/MoonshotAI/kimi-code/commit/0a3a2fbafab473df9e9f6609f825af78d2c1ce6d) Thanks [@RealKai42](https://github.com/RealKai42)! - Add an opt-in NotifyUser progress update feature (off by default): new sessions can show the agent's mid-turn progress with an optional one-line title, enabled in the TUI `/experiments` or the web/Desktop settings' Lab section; set `[experimental] notify_user = true` or `KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1` to turn it on.
- [#4145](https://github.com/MoonshotAI/kimi-code/pull/4145) [`242ac23`](https://github.com/MoonshotAI/kimi-code/commit/242ac230006447f8601e1678c49182bfbae89fe9) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add `kimi app [path]` to open a new desktop chat in the current or specified directory.
- [#4061](https://github.com/MoonshotAI/kimi-code/pull/4061) [`20a2cea`](https://github.com/MoonshotAI/kimi-code/commit/20a2cea72f5d2c4be3a3845e4ff24ce302a6630e) Thanks [@chengluyu](https://github.com/chengluyu)! - Cap the agent's in-turn wait for background tasks at 90 seconds (longer timeouts are no longer accepted), discourage repeated waits, and let a new message end the wait immediately.
- [#3998](https://github.com/MoonshotAI/kimi-code/pull/3998) [`09af3b4`](https://github.com/MoonshotAI/kimi-code/commit/09af3b483faac51a767aee50adb1bc1a8de4cc53) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: a message sent while the agent is running aborts the in-progress turn and runs as its own turn.
- [#4031](https://github.com/MoonshotAI/kimi-code/pull/4031) [`33e368a`](https://github.com/MoonshotAI/kimi-code/commit/33e368a1eff8d958856af8e2fb64f4db122b2247) Thanks [@Grapedge](https://github.com/Grapedge)! - Add sticky user message summaries in fullscreen mode; click a summary to jump to the original message.
- [#4074](https://github.com/MoonshotAI/kimi-code/pull/4074) [`c9f01f0`](https://github.com/MoonshotAI/kimi-code/commit/c9f01f07388abc2cdca40c9c5267f58c65086710) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: deterministic orchestration with TowerRebase and LLM-free status, goal/tower mutual exclusion, and workspace protection.
- [#4059](https://github.com/MoonshotAI/kimi-code/pull/4059) [`1f6f0b1`](https://github.com/MoonshotAI/kimi-code/commit/1f6f0b1fa2117cc9374c1be62c534b4a4d60e412) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the KIMI_CODE_TRUST_WORKSPACE environment variable to mark the workspace as trusted.
- [#4165](https://github.com/MoonshotAI/kimi-code/pull/4165) [`afd95f6`](https://github.com/MoonshotAI/kimi-code/commit/afd95f64a5444b649a4cc33504513bf88e4c54ba) Thanks [@Grapedge](https://github.com/Grapedge)! - Replace user skill cards with highlighted prompts and support Ctrl-S for skill prompts.
### Patch Changes
- [#4183](https://github.com/MoonshotAI/kimi-code/pull/4183) [`c15638b`](https://github.com/MoonshotAI/kimi-code/commit/c15638b0a1234e5aa97659fbc625d7dc4d60d93f) Thanks [@RealKai42](https://github.com/RealKai42)! - Cancel in-flight video uploads when a media read is interrupted.
- [#4081](https://github.com/MoonshotAI/kimi-code/pull/4081) [`06ebfc8`](https://github.com/MoonshotAI/kimi-code/commit/06ebfc821e304bd4ca3f828ae739954166463d65) Thanks [@sailist](https://github.com/sailist)! - Fix shifted turns and lost replies when a UserPromptSubmit hook injects text; hook JSON output without a "message" field is no longer added to the conversation.
- [#4154](https://github.com/MoonshotAI/kimi-code/pull/4154) [`419aced`](https://github.com/MoonshotAI/kimi-code/commit/419aced0e97fa04b75f8f71b089e1667f6de6d0a) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix media file reading on the first turn after resuming a session.
- [#4083](https://github.com/MoonshotAI/kimi-code/pull/4083) [`f409caa`](https://github.com/MoonshotAI/kimi-code/commit/f409caa21e71ce7beb158d29ffca1fed76216a64) Thanks [@sailist](https://github.com/sailist)! - Cron tasks from the source session no longer fire inside a forked session.
- [#4152](https://github.com/MoonshotAI/kimi-code/pull/4152) [`f0bc988`](https://github.com/MoonshotAI/kimi-code/commit/f0bc988bbc7db08d650f37ae0eaa69bcc6e90696) Thanks [@7Sageer](https://github.com/7Sageer)! - Forked subagents now inherit the parent's available agent types, including custom agents.
- [#4057](https://github.com/MoonshotAI/kimi-code/pull/4057) [`a940f2f`](https://github.com/MoonshotAI/kimi-code/commit/a940f2ff049ed57abce68c59526de7a6628a4112) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix the permission mode switch in the goal-start approval prompt not taking effect on the v2 engine.
- [#4081](https://github.com/MoonshotAI/kimi-code/pull/4081) [`06ebfc8`](https://github.com/MoonshotAI/kimi-code/commit/06ebfc821e304bd4ca3f828ae739954166463d65) Thanks [@sailist](https://github.com/sailist)! - Fix messages rendering out of place in sessions with a UserPromptSubmit hook.
- [#4153](https://github.com/MoonshotAI/kimi-code/pull/4153) [`7091138`](https://github.com/MoonshotAI/kimi-code/commit/7091138f5e66289d04b9b7a5cd77f94af9308be3) Thanks [@RealKai42](https://github.com/RealKai42)! - Let the agent write one- or two-sentence progress notes between tool calls instead of bare status lines; in the experimental Updates panel (`KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1`), stop asking the agent to repost its mid-turn text as an update and remind it to post updates less often.
- [#4054](https://github.com/MoonshotAI/kimi-code/pull/4054) [`4fbe065`](https://github.com/MoonshotAI/kimi-code/commit/4fbe065442179435c43d3c3dc8d11bb408b3fd30) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop prompting the agent to send NotifyUser updates when a session is opened in a host that does not display them.
- [#4091](https://github.com/MoonshotAI/kimi-code/pull/4091) [`21406fb`](https://github.com/MoonshotAI/kimi-code/commit/21406fb4c805cc8c715e6d1f16ad3fb5f25f4fe3) Thanks [@sailist](https://github.com/sailist)! - Omit max_tokens for models without an explicit output limit, fixing repeated 400 errors on strict serving stacks (e.g. bare vLLM) when the model's max output size is unknown.
- [#4184](https://github.com/MoonshotAI/kimi-code/pull/4184) [`cbd7540`](https://github.com/MoonshotAI/kimi-code/commit/cbd754049a114ee36d1b27675c82819e4963c692) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove the never-implemented `executor = "kaos"` option from MCP stdio server configuration; configs using it now fail validation at load time.
- [#2919](https://github.com/MoonshotAI/kimi-code/pull/2919) [`0f052fe`](https://github.com/MoonshotAI/kimi-code/commit/0f052fee1399f34086bb6343a56867d7df9b70b5) Thanks [@duyphan154](https://github.com/duyphan154)! - Sanitize foreground Bash tool output so captured terminal escape sequences no longer change the host terminal state.
- [#4164](https://github.com/MoonshotAI/kimi-code/pull/4164) [`3395f5b`](https://github.com/MoonshotAI/kimi-code/commit/3395f5bea6c77feb6a835d1ca3f4935970f96c6b) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Show a "Shutting down" status hint while shutdown hooks run during exit, and let a Ctrl+C during that window force an immediate exit instead of waiting for the hook timeout.
- [#4033](https://github.com/MoonshotAI/kimi-code/pull/4033) [`eec9bbd`](https://github.com/MoonshotAI/kimi-code/commit/eec9bbd94958248034f5dbf109a639970308f05e) Thanks [@Grapedge](https://github.com/Grapedge)! - Feedback surveys now appear less often.
- [#4160](https://github.com/MoonshotAI/kimi-code/pull/4160) [`72fc1d0`](https://github.com/MoonshotAI/kimi-code/commit/72fc1d0a3375b2a87dff602a1d91726a99f360e0) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Show "Working…" in the status spinner while thinking.
- [#4056](https://github.com/MoonshotAI/kimi-code/pull/4056) [`395d537`](https://github.com/MoonshotAI/kimi-code/commit/395d537237d737de0239159b60d1cc160bdf04e6) Thanks [@7Sageer](https://github.com/7Sageer)! - Replace inline MCP commands in the workspace trust prompt with activation summaries and configuration paths for manual inspection.
- [#4076](https://github.com/MoonshotAI/kimi-code/pull/4076) [`e3bf50c`](https://github.com/MoonshotAI/kimi-code/commit/e3bf50c0831d91ceb3656acf1b7a3f480eaada56) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove interruption reminders from the conversation when their interrupted turns are undone.
- [#4031](https://github.com/MoonshotAI/kimi-code/pull/4031) [`33e368a`](https://github.com/MoonshotAI/kimi-code/commit/33e368a1eff8d958856af8e2fb64f4db122b2247) Thanks [@Grapedge](https://github.com/Grapedge)! - Replace the user message bullet ✨ with ❯.
- [#4221](https://github.com/MoonshotAI/kimi-code/pull/4221) [`2e6821d`](https://github.com/MoonshotAI/kimi-code/commit/2e6821dcbd8034f128b2326a73d4704201b7c108) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Improved interactions and fixed known bugs.
## 2.1.1
### Patch Changes
- [#4013](https://github.com/MoonshotAI/kimi-code/pull/4013) [`929403b`](https://github.com/MoonshotAI/kimi-code/commit/929403b6db219eae6099dd23eae2f0efa2bb2823) Thanks [@7Sageer](https://github.com/7Sageer)! - Roll back some of the overly defensive changes in 2.1.0
- [#4015](https://github.com/MoonshotAI/kimi-code/pull/4015) [`c7dd841`](https://github.com/MoonshotAI/kimi-code/commit/c7dd84124a00d2dc1a68fbbc3e54b1095ee9ac23) Thanks [@sailist](https://github.com/sailist)! - Revert filesystem watchers for config and workspace files to on by default. Set `[watch] enabled` to `false` or `KIMI_CODE_WATCH=0` to keep them off.
## 2.1.0
### Minor Changes
- [#3990](https://github.com/MoonshotAI/kimi-code/pull/3990) [`32000d0`](https://github.com/MoonshotAI/kimi-code/commit/32000d08afcd019016fa60db0d68eaf21f3e35a6) Thanks [@Grapedge](https://github.com/Grapedge)! - In fullscreen, click a folded block to open or close it.
- [#4004](https://github.com/MoonshotAI/kimi-code/pull/4004) [`e17ff67`](https://github.com/MoonshotAI/kimi-code/commit/e17ff679281e340fd328bc2ebaba1237e79a9ad3) Thanks [@Grapedge](https://github.com/Grapedge)! - Add a TUI mode setting to /settings with an experimental fullscreen layout; switching modes takes effect after restarting Kimi Code. You can also set `tui_mode` in `~/.kimi-code/tui.toml` directly: `"regular"` renders into the terminal scrollback, while `"fullscreen"` makes parts of the UI mouse-interactive for a smoother experience.
### Patch Changes
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Block file tools from accessing files outside the working directory through symlinks.
- [#3975](https://github.com/MoonshotAI/kimi-code/pull/3975) [`bb96d80`](https://github.com/MoonshotAI/kimi-code/commit/bb96d80748921618182369a40a6610f38fa16acb) Thanks [@RealKai42](https://github.com/RealKai42)! - Reduce the CLI's startup time and memory usage.
- [#3957](https://github.com/MoonshotAI/kimi-code/pull/3957) [`6a214b8`](https://github.com/MoonshotAI/kimi-code/commit/6a214b85e53e58a9ef6480f27bcb7b0103c0e34e) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix a "Maximum call stack size exceeded" error when opening large sessions.
- [#3974](https://github.com/MoonshotAI/kimi-code/pull/3974) [`994287a`](https://github.com/MoonshotAI/kimi-code/commit/994287acb3a7428b580c09cabd5aba1b5a818aaa) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix forked sessions getting their titles overwritten by auto-generated titles.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Apply project-local configuration only after the workspace is trusted.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Block repository git configuration from executing commands during background git operations.
- [#3969](https://github.com/MoonshotAI/kimi-code/pull/3969) [`b3212fd`](https://github.com/MoonshotAI/kimi-code/commit/b3212fd9abc1f4670fb47df16f0137957aed6f2d) Thanks [@sailist](https://github.com/sailist)! - Skip cloud telemetry on the global login until its collector is live, and keep shutdown from aborting TUI exit.
- [#3979](https://github.com/MoonshotAI/kimi-code/pull/3979) [`e796bb5`](https://github.com/MoonshotAI/kimi-code/commit/e796bb5d482ecb7976c25dad0563b74cc9ec2c68) Thanks [@sailist](https://github.com/sailist)! - Fix MCP OAuth not requesting the offline_access scope, which caused hourly browser re-authorization with providers like Vercel.
- [#3969](https://github.com/MoonshotAI/kimi-code/pull/3969) [`b3212fd`](https://github.com/MoonshotAI/kimi-code/commit/b3212fd9abc1f4670fb47df16f0137957aed6f2d) Thanks [@sailist](https://github.com/sailist)! - Fix Remote Control failing to start after `kimi login --region global`.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Reject additional directories that resolve to the home directory or filesystem root.
- [#3995](https://github.com/MoonshotAI/kimi-code/pull/3995) [`a54e6f6`](https://github.com/MoonshotAI/kimi-code/commit/a54e6f6a9b36bc1346e41da576d96ff54aa557b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Turn off repeat reminders and forced stops with `KIMI_CODE_REPEAT_BREAKER=0`.
- [#3970](https://github.com/MoonshotAI/kimi-code/pull/3970) [`895e9d9`](https://github.com/MoonshotAI/kimi-code/commit/895e9d9b868cf9899e444784130fa1946c6cef6c) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix missing Swarm members after reopening an interrupted conversation.
- [#3989](https://github.com/MoonshotAI/kimi-code/pull/3989) [`a268ad7`](https://github.com/MoonshotAI/kimi-code/commit/a268ad775d9451d008d3c73de053655f6ab0772a) Thanks [@Grapedge](https://github.com/Grapedge)! - Show a Thinking… spinner and keep the status row height steady during streaming.
- [#3989](https://github.com/MoonshotAI/kimi-code/pull/3989) [`a268ad7`](https://github.com/MoonshotAI/kimi-code/commit/a268ad775d9451d008d3c73de053655f6ab0772a) Thanks [@Grapedge](https://github.com/Grapedge)! - Add a clickable "Jump to bottom" indicator to the fullscreen TUI.
- [#3976](https://github.com/MoonshotAI/kimi-code/pull/3976) [`f7012aa`](https://github.com/MoonshotAI/kimi-code/commit/f7012aa23b38d90d1a643166e2e78f706a606a34) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: smaller missions, stricter completion and review checks, and review history in rework briefings.
- [#3847](https://github.com/MoonshotAI/kimi-code/pull/3847) [`65ae3e3`](https://github.com/MoonshotAI/kimi-code/commit/65ae3e368c7cfa096242cacc12eeeb661e682977) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: mission titles must be ASCII, and task cards show the mission id.
- [#3931](https://github.com/MoonshotAI/kimi-code/pull/3931) [`f17a22e`](https://github.com/MoonshotAI/kimi-code/commit/f17a22ebf4e21b8196bfaaa9a2490707676bf519) Thanks [@sailist](https://github.com/sailist)! - Turn off filesystem watchers for config and workspace files by default. Set `[watch] enabled` to `true` or `KIMI_CODE_WATCH=1` to turn them back on.
- [#4005](https://github.com/MoonshotAI/kimi-code/pull/4005) [`ba41d07`](https://github.com/MoonshotAI/kimi-code/commit/ba41d07810cae37a9e529cad6b0f8e8abc8d7aaf) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improved interactions and fixed known bugs.
## 2.0.2
### Patch Changes
- [#3922](https://github.com/MoonshotAI/kimi-code/pull/3922) [`9df7a9c`](https://github.com/MoonshotAI/kimi-code/commit/9df7a9ccfb0d7d0713b51ae1ac165d0634fc40d2) Thanks [@sailist](https://github.com/sailist)! - Fix new messages occasionally landing at an old position in the conversation after resuming a session.
- [#3911](https://github.com/MoonshotAI/kimi-code/pull/3911) [`88a7d93`](https://github.com/MoonshotAI/kimi-code/commit/88a7d932f1cda95c78bf5488d34ff974a160fb08) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix compaction failing after switching to a model with a smaller context window.
- [#3929](https://github.com/MoonshotAI/kimi-code/pull/3929) [`6ffdf0d`](https://github.com/MoonshotAI/kimi-code/commit/6ffdf0d579fa1a9f30c2d00c437c6c8de0c03997) Thanks [@7Hanrui](https://github.com/7Hanrui)! - The agent no longer assumes the current working directory is the project root.
- [#3933](https://github.com/MoonshotAI/kimi-code/pull/3933) [`9721259`](https://github.com/MoonshotAI/kimi-code/commit/97212596f7d694b9683046ec7aad4d1247d29468) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix a message sent while the agent was running sometimes appearing twice in the chat.
- [#3934](https://github.com/MoonshotAI/kimi-code/pull/3934) [`2e605b1`](https://github.com/MoonshotAI/kimi-code/commit/2e605b10be906fb627c1f3aeaaa066b14ddbdbba) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improved interactions and fixed known bugs.
## 2.0.1
### Patch Changes
- [#3878](https://github.com/MoonshotAI/kimi-code/pull/3878) [`f233f9d`](https://github.com/MoonshotAI/kimi-code/commit/f233f9de04d71c42d27df870b18e1bc1a6605f16) Thanks [@7Sageer](https://github.com/7Sageer)! - Stop listing unavailable media tools in subagent menus and clarify Read errors for images and videos.
- [#3762](https://github.com/MoonshotAI/kimi-code/pull/3762) [`c5ad17f`](https://github.com/MoonshotAI/kimi-code/commit/c5ad17f06aa12702d2b30dff63acba9ed08f688d) Thanks [@7Sageer](https://github.com/7Sageer)! - Providers can read their API key from a named environment variable (`api_key_env` in `config.toml` or the provider API).
- [#3896](https://github.com/MoonshotAI/kimi-code/pull/3896) [`73ebe9a`](https://github.com/MoonshotAI/kimi-code/commit/73ebe9ab2fbf9bf0ffc8fe5a5375060ce8e984b5) Thanks [@sailist](https://github.com/sailist)! - Keep follow-up messages sent during a turn from appearing twice after a session reload.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Compact the session index on startup when many entries point to deleted sessions.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Start the CLI faster by removing a duplicate skills directory scan.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Open the session list and resume sessions faster on workspaces with many sessions.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Resume long sessions faster.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Index sessions for search in capped, incremental background passes, tunable via the [database] searchSyncSessionCap and searchSyncDebounceMs settings.
- [#3864](https://github.com/MoonshotAI/kimi-code/pull/3864) [`25dd4ce`](https://github.com/MoonshotAI/kimi-code/commit/25dd4ce97345c7ebfd9c036898e5eef955c45ea8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Rename the kimi install-app subcommand to kimi install-desktop; the old name keeps working as a hidden alias.
- [#3910](https://github.com/MoonshotAI/kimi-code/pull/3910) [`7dc253c`](https://github.com/MoonshotAI/kimi-code/commit/7dc253c5cefac7c277e5fc7071acf3e855fd503e) Thanks [@sailist](https://github.com/sailist)! - Restore thinking display for OpenAI-compatible providers that send a reasoning string next to an unrecognized reasoning_details array.
- [#3875](https://github.com/MoonshotAI/kimi-code/pull/3875) [`c1047a6`](https://github.com/MoonshotAI/kimi-code/commit/c1047a6bbe0e37d097e5da54d92dd3fb1b7a28da) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix `kimi -p` exiting early and cancelling the active turn when a cron task fires.
- [#3894](https://github.com/MoonshotAI/kimi-code/pull/3894) [`910aba2`](https://github.com/MoonshotAI/kimi-code/commit/910aba273e2bee3a8815666a3a9c4fff609f2366) Thanks [@7Sageer](https://github.com/7Sageer)! - Drop literal decision clauses from the system prompt and the auto permission mode reminder.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Restore sessions with large file-edit histories faster.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Serve session transcript APIs faster on repeated calls.
- [#3885](https://github.com/MoonshotAI/kimi-code/pull/3885) [`5acc863`](https://github.com/MoonshotAI/kimi-code/commit/5acc863ae60c092457bd9ecdde55266b5fad7040) Thanks [@starquakee](https://github.com/starquakee)! - select_tools now points out already-available tools to call directly and suggests announced names for near-miss selections.
- [#3887](https://github.com/MoonshotAI/kimi-code/pull/3887) [`e3f48a2`](https://github.com/MoonshotAI/kimi-code/commit/e3f48a225bbedbe167b30510e67d66ee67fcb0ec) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix deleting or archiving a session sometimes never finishing.
- [#3891](https://github.com/MoonshotAI/kimi-code/pull/3891) [`53e5e3f`](https://github.com/MoonshotAI/kimi-code/commit/53e5e3fca6bf377662bc4d61d220dde4ded8e7b7) Thanks [@sailist](https://github.com/sailist)! - Keep follow-up messages sent during a turn attached to that turn in the conversation history.
- [#3906](https://github.com/MoonshotAI/kimi-code/pull/3906) [`60f2a63`](https://github.com/MoonshotAI/kimi-code/commit/60f2a63278f28b77d9358cf589b2da6193bd2614) Thanks [@sailist](https://github.com/sailist)! - Keep a follow-up sent during a turn as one message, and undo the host turn instead of the follow-up alone.
- [#3879](https://github.com/MoonshotAI/kimi-code/pull/3879) [`1fddc16`](https://github.com/MoonshotAI/kimi-code/commit/1fddc16e3ea2de4c26a18acd764380adf9e2ed64) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove the system-prompt rule that forbade all file access outside the working directory.
- [#3892](https://github.com/MoonshotAI/kimi-code/pull/3892) [`a80fe31`](https://github.com/MoonshotAI/kimi-code/commit/a80fe31cff0ebae686e60d7d5d0559ad2d2e843d) Thanks [@sailist](https://github.com/sailist)! - Stop workspace file watchers from scanning an unbounded project root, and add `[watch] enabled` / `KIMI_CODE_WATCH` to disable watching entirely.
- [#3869](https://github.com/MoonshotAI/kimi-code/pull/3869) [`86e0820`](https://github.com/MoonshotAI/kimi-code/commit/86e08208e3445171f6500c948234d7e3a0684324) Thanks [@sailist](https://github.com/sailist)! - Stop asking for approval of bash commands that cannot be statically analyzed in Ask When Needed permission mode.
## 2.0.0
### Major Changes
- [#3849](https://github.com/MoonshotAI/kimi-code/pull/3849) [`34ec5d2`](https://github.com/MoonshotAI/kimi-code/commit/34ec5d2a1700e540c39d2a1b6c905c9fa96af2db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the /desktop slash command (alias /install-desktop) and the kimi install-app subcommand to open the Kimi Code desktop app page in the browser.
### Minor Changes
- [#3851](https://github.com/MoonshotAI/kimi-code/pull/3851) [`faec32c`](https://github.com/MoonshotAI/kimi-code/commit/faec32c7e7c1df5d27b7060f75a2a665efb59268) Thanks [@Grapedge](https://github.com/Grapedge)! - Render mermaid code blocks as diagrams in the terminal; turn it off under /settings → Mermaid diagrams, or set `mermaid = "off"` in the `[markdown]` section of tui.toml.
### Patch Changes
- [#3838](https://github.com/MoonshotAI/kimi-code/pull/3838) [`f4e5822`](https://github.com/MoonshotAI/kimi-code/commit/f4e5822164800ec138f77b0f323e2374963bfe19) Thanks [@sailist](https://github.com/sailist)! - Fix an occasional crash when a running turn is canceled.
- [#3802](https://github.com/MoonshotAI/kimi-code/pull/3802) [`c72a202`](https://github.com/MoonshotAI/kimi-code/commit/c72a20270e5078987a65e63b3741d8c778f4e02e) Thanks [@Grapedge](https://github.com/Grapedge)! - Highlight diff code blocks.
- [#3844](https://github.com/MoonshotAI/kimi-code/pull/3844) [`0cf413f`](https://github.com/MoonshotAI/kimi-code/commit/0cf413f7759c314766ba30250a9875ac1af1185e) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix a steered message appearing twice after interrupting the turn.
- [#3848](https://github.com/MoonshotAI/kimi-code/pull/3848) [`b4d0b8a`](https://github.com/MoonshotAI/kimi-code/commit/b4d0b8a3889c1a6f0115c527341ccd2066bd180a) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix @ file mentions duplicating or dropping the path when accepting a suggestion while still typing.
- [#3828](https://github.com/MoonshotAI/kimi-code/pull/3828) [`2bb6e12`](https://github.com/MoonshotAI/kimi-code/commit/2bb6e12c7e3ecc19521d21fcffc832075ed13200) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix duplicate user messages when steering an ongoing conversation.
- [#3832](https://github.com/MoonshotAI/kimi-code/pull/3832) [`1c7e996`](https://github.com/MoonshotAI/kimi-code/commit/1c7e996aa8bdd7a6f32447f1d89e43c0646b41bc) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix files attached while steering an ongoing conversation appearing only after a reload.
- [#3853](https://github.com/MoonshotAI/kimi-code/pull/3853) [`bd06178`](https://github.com/MoonshotAI/kimi-code/commit/bd06178913d2cc4ebd229ecee6714081105e5ae1) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Keep the original prompt visible when steering a running turn, instead of replacing it with the steered text.
- [#3832](https://github.com/MoonshotAI/kimi-code/pull/3832) [`1c7e996`](https://github.com/MoonshotAI/kimi-code/commit/1c7e996aa8bdd7a6f32447f1d89e43c0646b41bc) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix slash commands sent while steering an ongoing conversation missing from the transcript.
- [#3837](https://github.com/MoonshotAI/kimi-code/pull/3837) [`cafd9b5`](https://github.com/MoonshotAI/kimi-code/commit/cafd9b5a65509fe57332cbc0eb1fb19f0443fb0e) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix missing and reappearing chat messages after undoing a steered message.
- [#3818](https://github.com/MoonshotAI/kimi-code/pull/3818) [`19ce4b3`](https://github.com/MoonshotAI/kimi-code/commit/19ce4b3f0600c846e2f28828d8169829630fc666) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix /usage showing a wrong error message when no session has been created yet.
- [#3784](https://github.com/MoonshotAI/kimi-code/pull/3784) [`5653c73`](https://github.com/MoonshotAI/kimi-code/commit/5653c739b9c0dbafd1e4b3d36edeb0fe44ec71f4) Thanks [@7Sageer](https://github.com/7Sageer)! - Images sent to Kimi models are uploaded as file references instead of inline data, and a warning is shown when media are dropped from a retried request.
- [#3778](https://github.com/MoonshotAI/kimi-code/pull/3778) [`7d174ac`](https://github.com/MoonshotAI/kimi-code/commit/7d174ac93352ccfc835d186ac7b8185d0194e6b5) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Limit memory growth from finished subagents.
- [#3778](https://github.com/MoonshotAI/kimi-code/pull/3778) [`7d174ac`](https://github.com/MoonshotAI/kimi-code/commit/7d174ac93352ccfc835d186ac7b8185d0194e6b5) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Report background subagents that time out or are stopped as cancelled instead of failed.
- [#3846](https://github.com/MoonshotAI/kimi-code/pull/3846) [`31f1b68`](https://github.com/MoonshotAI/kimi-code/commit/31f1b6824d1900aeea71b69c824c6354a0dd9019) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix OAuth login never triggering for MCP servers that allow anonymous tool discovery but reject tool calls with 401.
- [#3784](https://github.com/MoonshotAI/kimi-code/pull/3784) [`5653c73`](https://github.com/MoonshotAI/kimi-code/commit/5653c739b9c0dbafd1e4b3d36edeb0fe44ec71f4) Thanks [@7Sageer](https://github.com/7Sageer)! - When accumulated images and videos exceed the request size budget, the oldest media are omitted from requests with a warning instead of failing.
- [#3778](https://github.com/MoonshotAI/kimi-code/pull/3778) [`7d174ac`](https://github.com/MoonshotAI/kimi-code/commit/7d174ac93352ccfc835d186ac7b8185d0194e6b5) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Report tools and subagents interrupted by the user as cancelled instead of aborted or failed.
- [#3803](https://github.com/MoonshotAI/kimi-code/pull/3803) [`39a7455`](https://github.com/MoonshotAI/kimi-code/commit/39a74556a662a96293e9838663e91b31dbdc7a0b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - The built-in browser plugin now appears as "Kimi Browser Extension" in the plugins panel, marketplace catalog, and docs, matching the product rename.
- [#3858](https://github.com/MoonshotAI/kimi-code/pull/3858) [`95d4a9d`](https://github.com/MoonshotAI/kimi-code/commit/95d4a9d0411b18f468345717c01c40f5ac996cdc) Thanks [@liruifengv](https://github.com/liruifengv)! - Sign the Windows CLI executable with a trusted publisher certificate.
- [#3795](https://github.com/MoonshotAI/kimi-code/pull/3795) [`a7bdabb`](https://github.com/MoonshotAI/kimi-code/commit/a7bdabbe82a089cd8631f6d613c252d96cbab639) Thanks [@Grapedge](https://github.com/Grapedge)! - Feedback surveys now appear at better times in long conversations.
- [#3833](https://github.com/MoonshotAI/kimi-code/pull/3833) [`8660a07`](https://github.com/MoonshotAI/kimi-code/commit/8660a07b3a0b7984971371d77f316e802e5e83fe) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix tower worker cards wrongly showing "completed" during rework.
- [#3840](https://github.com/MoonshotAI/kimi-code/pull/3840) [`9c5e9b4`](https://github.com/MoonshotAI/kimi-code/commit/9c5e9b48634be1dfae921fb48f9afc3a23ffd1ad) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix a Windows crash when a project or config folder is opened through a short 8.3 path.
## 0.43.1
### Patch Changes
- [#3780](https://github.com/MoonshotAI/kimi-code/pull/3780) [`486dcd2`](https://github.com/MoonshotAI/kimi-code/commit/486dcd26c76f2854fc351515a45d8f0fc2d31ed6) Thanks [@Grapedge](https://github.com/Grapedge)! - Update the terminal UI engine, fixing link colors in wrapped markdown tables and `@` file-completion ordering, and adding native clipboard support on Linux X11.
- [#3776](https://github.com/MoonshotAI/kimi-code/pull/3776) [`48fad6e`](https://github.com/MoonshotAI/kimi-code/commit/48fad6e7d9cd00511c6118e84ae6d22dafb96be9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix memory not being released when subagent scopes are disposed.
- [#3777](https://github.com/MoonshotAI/kimi-code/pull/3777) [`dc76b0c`](https://github.com/MoonshotAI/kimi-code/commit/dc76b0caaa6e0aa406bf87957e6b5f8aca722221) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix progressively slower rendering on each round of large agent swarm runs.
- [#3631](https://github.com/MoonshotAI/kimi-code/pull/3631) [`1336be3`](https://github.com/MoonshotAI/kimi-code/commit/1336be38777959cc558fed2b51dc53caa07da5db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Stop returning deleted sessions from global search before the search index catches up.
- [#3775](https://github.com/MoonshotAI/kimi-code/pull/3775) [`f248624`](https://github.com/MoonshotAI/kimi-code/commit/f2486241fb5e268d7a43cb589b5c2d59af8db571) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Reduce event-loop stalls and GC churn in sessions with many concurrent subagents.
- [#3779](https://github.com/MoonshotAI/kimi-code/pull/3779) [`82ec469`](https://github.com/MoonshotAI/kimi-code/commit/82ec469e47eed52feb664fd361e3d8dcb774596e) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix pressing Ctrl+C while subagents are running exiting the whole CLI instead of just interrupting the subagents.
- [#3752](https://github.com/MoonshotAI/kimi-code/pull/3752) [`0725ef1`](https://github.com/MoonshotAI/kimi-code/commit/0725ef1ce0a7f9c15db7ffc042f57308657b3ee6) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Fix tower mode mistaking newly spawned agents for previous sessions' roster entries.
## 0.43.0
### Minor Changes
- [#3749](https://github.com/MoonshotAI/kimi-code/pull/3749) [`6126472`](https://github.com/MoonshotAI/kimi-code/commit/6126472c7af421070f09c470c5750455dd277657) Thanks [@liruifengv](https://github.com/liruifengv)! - web: AI session titles are now always on — a title is generated after the first turn and can be regenerated from the rename field, with no experimental flag required.
- [#3670](https://github.com/MoonshotAI/kimi-code/pull/3670) [`a9efbe0`](https://github.com/MoonshotAI/kimi-code/commit/a9efbe053f966104d4b51090118da5135c084e22) Thanks [@Grapedge](https://github.com/Grapedge)! - Delete sessions from the session picker: press Ctrl+X on a session, then y to confirm.
### Patch Changes
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: add user agreement and privacy policy entries to the Settings → About page.
- [#3750](https://github.com/MoonshotAI/kimi-code/pull/3750) [`775a6c3`](https://github.com/MoonshotAI/kimi-code/commit/775a6c35de40e6dd8cc399cc848f95818733ee7c) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the `loop_control.compaction_max_attempts` config option to set the maximum total attempts for a failing compaction request (default 5).
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add the dynamically_loaded_tools capability to official Kimi Code models when the service declares support for message-level tool declarations.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix sessions containing many brackets or backslashes getting stuck while loading.
- [#3734](https://github.com/MoonshotAI/kimi-code/pull/3734) [`ee2cac1`](https://github.com/MoonshotAI/kimi-code/commit/ee2cac102b835fcd7adb3d4b9bc3d62b0b71cdfd) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix a crash that killed the process when a retried LLM request had streamed a partial tool call before disconnecting.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix persistent UI stuttering while streaming in long sessions.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix certain formulas, links, tool results, and log contents causing the UI to stutter or become unresponsive.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the send button staying disabled when starting a new session.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix previous sessions being wrongly marked as unread and triggering "turn complete" notifications after starting a new conversation.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: render Markdown frontmatter metadata as key-value cards and tag lists.
- [#3688](https://github.com/MoonshotAI/kimi-code/pull/3688) [`9f7e68e`](https://github.com/MoonshotAI/kimi-code/commit/9f7e68e8bdb70d3e5cde5a924740e357ede2fc40) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve MCP attachments that cannot be delivered directly to the model.
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add a per-server `deferred` field to MCP server configuration: when the model supports dynamic tool loading (experimental `tool-select` flag), set `deferred: true` to keep a server's tools out of the top-level tool list and load them on demand via `select_tools`; servers are exposed inline by default.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: drop the Changes entry from the panel new-tab menu while a diff tab is open.
- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Exclude time spent with the session closed from goal time budgets.
- [#3728](https://github.com/MoonshotAI/kimi-code/pull/3728) [`b180725`](https://github.com/MoonshotAI/kimi-code/commit/b1807253c34e12b0ecf60c9b4da3890d0c80ce72) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Add the `KIMI_CODE_PERMISSION_MODE_REMINDER` environment variable: set it to `0` to stop injecting the auto permission-mode reminders into the model context.
- [#3696](https://github.com/MoonshotAI/kimi-code/pull/3696) [`d3dc594`](https://github.com/MoonshotAI/kimi-code/commit/d3dc5945548d3353a5e6cfc457a3a8cf84d7da71) Thanks [@sailist](https://github.com/sailist)! - Include the server token in the Remote Control Local UI link so it opens already signed in.
- [#3709](https://github.com/MoonshotAI/kimi-code/pull/3709) [`306f6f6`](https://github.com/MoonshotAI/kimi-code/commit/306f6f69251d65fda434463433c0761758eacb16) Thanks [@sailist](https://github.com/sailist)! - Fix Remote Control uploads larger than ~3.5MB always failing with a 400 error.
- [#3718](https://github.com/MoonshotAI/kimi-code/pull/3718) [`ab9e688`](https://github.com/MoonshotAI/kimi-code/commit/ab9e688cd0c753bd3cea3406a9e9194c73ab2239) Thanks [@sailist](https://github.com/sailist)! - Reuse unchanged Remote Control assets across page loads instead of retransferring them.
- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Remove the 24-hour limit on goal time budgets.
- [#3714](https://github.com/MoonshotAI/kimi-code/pull/3714) [`565093f`](https://github.com/MoonshotAI/kimi-code/commit/565093f727eee07e2aa05f43859fa12b9e977989) Thanks [@sailist](https://github.com/sailist)! - Skip the confirmation prompt for rm -rf commands that target only /tmp or /temp paths.
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Fix the select_tools tool never being registered because agent profiles do not list it in their tool allowlists.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: restructure the Settings pages — Account moves right after General, Agent is renamed to Agents & Sessions and now includes message folding, Advanced is renamed to About, and data & privacy settings move into General.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the selected segment background flashing and shifting while the Settings segmented control loads.
- [#3697](https://github.com/MoonshotAI/kimi-code/pull/3697) [`5e452fc`](https://github.com/MoonshotAI/kimi-code/commit/5e452fc2ddf5a0d9feedfbf3c7cdde4246fc44b6) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow steering messages to interrupt waits for background tasks.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the switch thumb deforming at both ends when stretched on hover.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix tooltips popping up even when the mouse has not moved.
- [#3648](https://github.com/MoonshotAI/kimi-code/pull/3648) [`2da4aa2`](https://github.com/MoonshotAI/kimi-code/commit/2da4aa23b0d484312cc068b2d4b2a62e694a2bfe) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode reliability fixes across messaging, worktrees, and the review-to-merge gate.
- [#3707](https://github.com/MoonshotAI/kimi-code/pull/3707) [`e409bc8`](https://github.com/MoonshotAI/kimi-code/commit/e409bc8ba582c9f390146eca08812d22790c81f8) Thanks [@sailist](https://github.com/sailist)! - Compress Remote Control tunnel responses with gzip.
- [#3702](https://github.com/MoonshotAI/kimi-code/pull/3702) [`42998cf`](https://github.com/MoonshotAI/kimi-code/commit/42998cfc13cb03914256587f0b702ec95e2d156c) Thanks [@sailist](https://github.com/sailist)! - Add `-y, --yes` to `kimi upgrade` (alias `kimi update`) to skip the confirmation prompt and install the update directly.
- [#3681](https://github.com/MoonshotAI/kimi-code/pull/3681) [`1306a9a`](https://github.com/MoonshotAI/kimi-code/commit/1306a9a8dbd5acd9532182c36edcabbc7e8de807) Thanks [@7Sageer](https://github.com/7Sageer)! - Warn at startup when a [models] entry in config.toml is missing the model field and cannot be used.
## 0.42.0
### Minor Changes
- [#3613](https://github.com/MoonshotAI/kimi-code/pull/3613) [`d4d20d2`](https://github.com/MoonshotAI/kimi-code/commit/d4d20d21d733a7942056c15ffcc2c74cce51f7d5) Thanks [@liukx0205](https://github.com/liukx0205)! - Add read-only tools to the /btw side agent.
- [#3524](https://github.com/MoonshotAI/kimi-code/pull/3524) [`f6a9c39`](https://github.com/MoonshotAI/kimi-code/commit/f6a9c39e22a63b2a231684ed8426bfe4ec406cf7) Thanks [@RealKai42](https://github.com/RealKai42)! - Add an experimental Updates panel with paginated progress messages from the main agent and subagents; enable it with `KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1`.
- [#3634](https://github.com/MoonshotAI/kimi-code/pull/3634) [`e831fd1`](https://github.com/MoonshotAI/kimi-code/commit/e831fd1ea9488ad5192bcc9d96579470cf0c4442) Thanks [@7Sageer](https://github.com/7Sageer)! - The subagent model pool (`[secondary_model]`) is now always on; the experimental secondary-model flag and the `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` opt-out have been removed.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: support permanently deleting sessions from the session row context menu, with a confirmation prompt.
- [#3526](https://github.com/MoonshotAI/kimi-code/pull/3526) [`55685c5`](https://github.com/MoonshotAI/kimi-code/commit/55685c58b5dbff692e16c66efa65fa6d32222f61) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop reminding the model of its context budget before automatic compaction.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: preview images and videos in a reorderable media rail in the composer, mention them in the text on demand, and keep the previews after queueing and sending.
### Patch Changes
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the conversation scrollbar being too thin to click and drag easily.
- [#3539](https://github.com/MoonshotAI/kimi-code/pull/3539) [`34ad513`](https://github.com/MoonshotAI/kimi-code/commit/34ad5137ce36f6eb03c4a42145e9bdcc8d607d87) Thanks [@RealKai42](https://github.com/RealKai42)! - Collapse finished tool calls in the transcript to a header plus one marked outcome row: short output is shown whole, hidden output is counted (`N more lines`, `+N more`) and revealed by `Ctrl+O`, which the footer advertises while it is available.
- [#3537](https://github.com/MoonshotAI/kimi-code/pull/3537) [`f12d59e`](https://github.com/MoonshotAI/kimi-code/commit/f12d59e089e2531a33fbca30b26ffeabd5862b45) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the agent resuming the wrong request after automatic context compaction in long sessions.
- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - The minidb session-index read model and global search worker are now always on; the experimental flags have been replaced by the `[database]` config section and the `KIMI_CODE_PERSISTENCE_MINIDB_READMODEL` / `KIMI_CODE_SEARCH_WORKER` env vars.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix background task notifications lingering at the bottom of the session during subsequent tool calls.
- [#3658](https://github.com/MoonshotAI/kimi-code/pull/3658) [`ff7371b`](https://github.com/MoonshotAI/kimi-code/commit/ff7371b70a7ae80ec70712b610f73e6d6f29edc0) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow file searches to retrieve matches beyond the first 100 results.
- [#3652](https://github.com/MoonshotAI/kimi-code/pull/3652) [`7f5debf`](https://github.com/MoonshotAI/kimi-code/commit/7f5debfa71ac9e4a23b5dab1a511aa3672677381) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images on a session's first prompt when the configured default model is served by Kimi and no model has been selected yet.
- [#3649](https://github.com/MoonshotAI/kimi-code/pull/3649) [`80480c0`](https://github.com/MoonshotAI/kimi-code/commit/80480c01d27a921d4660ad043e78c8b61167f82b) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images in ReadMediaFile and prompt attachments when the model is served by Kimi.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix page jank caused by hundreds of simultaneous requests when reloading a session with many background tasks.
- [#3654](https://github.com/MoonshotAI/kimi-code/pull/3654) [`6de0cec`](https://github.com/MoonshotAI/kimi-code/commit/6de0cec174ae3af44fc9f9f74b2aa8599b928fdf) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve distinct structured data in MCP tool results.
- [#3669](https://github.com/MoonshotAI/kimi-code/pull/3669) [`f8c606e`](https://github.com/MoonshotAI/kimi-code/commit/f8c606e7d7b33a43190721d47ec6c8a2076eaec6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Support compressed downloads from updated native release manifests.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: reduce jank when opening and scrolling back through long conversations, while preserving message and tool expansion state.
- [#3548](https://github.com/MoonshotAI/kimi-code/pull/3548) [`baf17a8`](https://github.com/MoonshotAI/kimi-code/commit/baf17a8fcc289f20fa6c8d85dd8f93eeb3ff0cbc) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve image and video filenames in session history.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Add configurable character limits and resumable long-line file reads without repeated output truncation.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Read malformed UTF-16 files with an explicit lossy-decoding warning.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Avoid repeated scanning for common tail reads and report file changes detected during tail rereads.
- [#3616](https://github.com/MoonshotAI/kimi-code/pull/3616) [`260ac3f`](https://github.com/MoonshotAI/kimi-code/commit/260ac3faad1cf0ca84dca26173ddf66a09220841) Thanks [@Grapedge](https://github.com/Grapedge)! - Upgrade the default thinking effort to the recommended level for eligible users.
- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - Remote Control is now always on; the experimental `KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL` flag has been removed.
- [#3618](https://github.com/MoonshotAI/kimi-code/pull/3618) [`75682b0`](https://github.com/MoonshotAI/kimi-code/commit/75682b0ef130e0340a9d16421312e2766c327be1) Thanks [@sailist](https://github.com/sailist)! - Fix recent sessions missing from the session list when the sessions directory contains stray files.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix skills created mid-session not appearing in the slash list until the app restarts.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix trailing backticks briefly flashing at the end of code blocks while they stream.
- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - The /tasks panel now shows each background agent's model under its task row.
- [#3607](https://github.com/MoonshotAI/kimi-code/pull/3607) [`5eea890`](https://github.com/MoonshotAI/kimi-code/commit/5eea89016511d113480755907619defafaf025cf) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix the streaming debug timing attributing client-side busy time to the server.
- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower worker and reviewer briefings now carry the full mission context, and tower agent timeouts follow the subagent timeout setting (`[subagent] timeout_ms` or `KIMI_SUBAGENT_TIMEOUT_MS`), defaulting to 2 hours. Fix the /tasks list not showing the model for tower-spawned agents.
- [#3593](https://github.com/MoonshotAI/kimi-code/pull/3593) [`00cfbb0`](https://github.com/MoonshotAI/kimi-code/commit/00cfbb0547cee6ec2650fd3fc52e15d2072c41aa) Thanks [@7Sageer](https://github.com/7Sageer)! - Print a warning in `kimi -p` when project-level MCP servers are skipped because the folder is not trusted.
- [#3608](https://github.com/MoonshotAI/kimi-code/pull/3608) [`fb0353a`](https://github.com/MoonshotAI/kimi-code/commit/fb0353a8ba5ceb7e8ae4e27f3260b3c8c8d80784) Thanks [@liukx0205](https://github.com/liukx0205)! - Watch the user-level skill roots (`~/.kimi-code/skills` and `~/.agents/skills`) so the workspace skill catalog refreshes automatically when skills are created, modified, or deleted while the daemon is running — no restart or manual reload needed.
- [#3560](https://github.com/MoonshotAI/kimi-code/pull/3560) [`af81bb9`](https://github.com/MoonshotAI/kimi-code/commit/af81bb92215dca2f933579ce0119f7add452bc96) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Open the browser on localhost instead of the wildcard bind address for `kimi web --host 0.0.0.0`.
- [#3605](https://github.com/MoonshotAI/kimi-code/pull/3605) [`f1e9152`](https://github.com/MoonshotAI/kimi-code/commit/f1e915277655c68213cf91fe5010413bfde3cb65) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix slow response streaming and rendering after resuming sessions with many scheduled cron turns.
## 0.41.0
### Minor Changes
- [#3423](https://github.com/MoonshotAI/kimi-code/pull/3423) [`b199e33`](https://github.com/MoonshotAI/kimi-code/commit/b199e3326de28d5094ca5f18816a74ab336c51e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Remind the model of its context budget before automatic compaction, and after compaction point it at the session's event log for exact details.
- [#3525](https://github.com/MoonshotAI/kimi-code/pull/3525) [`eba23ed`](https://github.com/MoonshotAI/kimi-code/commit/eba23edb93ec00aa11ffa288e2b89ebbf3d77a70) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Turn-level file history is now always on; the experimental file-history flag has been removed.
- [#3516](https://github.com/MoonshotAI/kimi-code/pull/3516) [`6013658`](https://github.com/MoonshotAI/kimi-code/commit/60136588a4218bc2423b93467c4fae8c6554219c) Thanks [@Grapedge](https://github.com/Grapedge)! - Add an occasional session rating prompt above the input box.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: add tower multi-agent collaboration mode (experimental), enabled via the /tower command or the composer plus menu; /tower supports specifying a base branch (e.g. /tower add-new-feature).
### Patch Changes
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix background agent message cards incorrectly showing a running indicator.
- [#3529](https://github.com/MoonshotAI/kimi-code/pull/3529) [`b184b31`](https://github.com/MoonshotAI/kimi-code/commit/b184b31497949ac019ac43d5a3dfe2ab87dbebc4) Thanks [@sailist](https://github.com/sailist)! - Stop blocking dangerous commands and commands that cannot be statically analyzed in auto permission mode.
- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Deliver background question answers to the agent directly instead of via a saved output file.
- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix background questions being cancelled as soon as the agent finishes its turn.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: polish the style and interaction of Bash commands in the right-side panel.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection comments and quote-to-chat in the diff and per-turn changes panels.
- [#3459](https://github.com/MoonshotAI/kimi-code/pull/3459) [`b6b9b37`](https://github.com/MoonshotAI/kimi-code/commit/b6b9b374dd5a3cda4257081b194760d7998e73f0) Thanks [@RealKai42](https://github.com/RealKai42)! - Subagent final messages are no longer bounced back for expansion when they are under 200 characters.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix file change previews showing added/removed lines that never existed when the same file is edited multiple times in one turn.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: pressing Esc no longer closes the right detail panel.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the default thinking effort in settings not being settable to the highest level (Max).
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rename the three permission modes to Always Ask / Ask When Needed / Never Ask and update their Chinese and English descriptions.
- [#3473](https://github.com/MoonshotAI/kimi-code/pull/3473) [`d567a6a`](https://github.com/MoonshotAI/kimi-code/commit/d567a6a4fdaeb052deae9d9b313df0d922105799) Thanks [@7Sageer](https://github.com/7Sageer)! - Show a warning after switching to Ask When Needed or Never Ask mode.
- [#3498](https://github.com/MoonshotAI/kimi-code/pull/3498) [`a3b48a7`](https://github.com/MoonshotAI/kimi-code/commit/a3b48a7272880dafb64e4c403006d94dc781d05c) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix print mode (`kimi -p`) ignoring the `KIMI_DISABLE_TELEMETRY` environment variable.
- [#3531](https://github.com/MoonshotAI/kimi-code/pull/3531) [`51bd52a`](https://github.com/MoonshotAI/kimi-code/commit/51bd52a589089f99a941e0d3285b0cf638e5b1a0) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix print mode (`kimi -p`) losing session records when the run exits on an error or a termination signal.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restyle selection quote pills from blue to the same neutral ink as other mentions, with a vertical bar separating the quote and comment.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: shorten selection quote pill excerpts to at most 12 characters.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restore selection quoting — after selecting text in a message or file preview, you can add a comment or quote it into the chat.
- [#3478](https://github.com/MoonshotAI/kimi-code/pull/3478) [`052e98e`](https://github.com/MoonshotAI/kimi-code/commit/052e98ec17ac8d931873ef892fb0e1912aa401e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Resuming a subagent by its agent id now works after the session is reopened in a new process; the resumed subagent follows the current permission mode and is matched by its own profile in permission rules.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: selection comment bubbles near the bottom of the page now pop above the selection and grow upward, eliminating double scrollbars.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix selection bubbles displayed over the panel header being unclickable and lacking a hover state.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the comment bubble popping up before mouse release when selecting text with a slow drag.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rework selection comment bubble actions into right-aligned Cancel / Add to chat below the input, with an Enter hint on the confirm button.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: revamp the selection comment bubble — the comment box supports multi-line input and auto-grows, and the confirm button now matches the chat composer send button.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection quoting in the terminal — select text to add a comment or quote it into the chat.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: lengthen the hover delay of quote and mention preview cards so merely passing the mouse over them no longer triggers them.
- [#3461](https://github.com/MoonshotAI/kimi-code/pull/3461) [`8057d30`](https://github.com/MoonshotAI/kimi-code/commit/8057d30afbd2942f7c2e647f33a9dc7401af2b43) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode (experimental, `KIMI_CODE_EXPERIMENTAL_TOWER=1`): fix tower mode never starting when enabled through `[experimental] tower = true` in `config.toml` instead of the environment variable. When tower mode cannot be enabled, the error now names the actual blocker — the disabled experiment, a required restart, or the owning session. When another live session owns the workspace tower, the message also names the owning session's title alongside its id. /tower now also works in a directory that is not a git repository — it runs git init and commits what is there (an empty initial commit for empty directories).
- [#3521](https://github.com/MoonshotAI/kimi-code/pull/3521) [`744b718`](https://github.com/MoonshotAI/kimi-code/commit/744b718b672199f407786f3b5f789fb3caf4b2a7) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Remove the /dance Easter egg hint from the TUI tips rotation.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix inaccurate added/removed line counts in per-turn file change summaries after the same file is edited or overwritten multiple times.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: per-turn file change cards now show only exact line statistics, and the card no longer appears when statistics are unavailable.
## 0.40.1
### Patch Changes

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{bQ as H,M as J,aU as p,bE as S,az as O,aL as Q,s as F,v as I,I as K,bJ as W,bL as Z,aw as X,H as A,bb as ee,bB as te,g as le,au as ae,T as ne,as as Y,bR as oe}from"./index-8y9QBU_r.js";var k=(d,x,e)=>new Promise((n,s)=>{var r=i=>{try{c(e.next(i))}catch(v){s(v)}},y=i=>{try{c(e.throw(i))}catch(v){s(v)}},c=i=>i.done?n(i.value):Promise.resolve(i.value).then(r,y);c((e=e.apply(d,x)).next())});const ie=["id"],re=["data-placement"],se=H(J({__name:"Tooltip",props:{visible:{type:Boolean},anchorEl:{},content:{},placement:{},offset:{},originX:{},originY:{},id:{},isDark:{type:[Boolean,null]}},setup(d){var x;const e=d,n=p(null),s=p(null),r=p({transform:"translate3d(0px, 0px, 0px)",left:"0px",top:"0px"}),y=p({}),c=p((x=e.placement)!=null?x:"top"),i=p(!1),v=p(!1);let E=null,M=null,R=null,P=null,w=null,f=0;function N(){return P?Promise.resolve(P):(w||(w=oe(()=>import("./floating-ui.dom-xGUaHE3m.js"),[]).then(a=>(P=a,a)).catch(a=>{throw w=null,a})),w)}function T(){E&&(E(),E=null),M=null,R=null}function D(a){return k(this,null,function*(){const t=e.anchorEl,l=n.value;if(!e.visible||!t||!l||M===t&&R===l)return;const{autoUpdate:o}=yield N();a()&&e.visible&&e.anchorEl===t&&n.value===l&&(T(),M=t,R=l,E=o(t,l,()=>{$().catch(()=>{B()})}))})}function $(){return k(this,null,function*(){var a,t;const l=e.anchorEl,o=n.value;if(!e.visible||!l||!o)return!1;const{arrow:u,computePosition:C,flip:h,offset:m,shift:b}=yield N();if(!e.visible||e.anchorEl!==l||n.value!==o)return!1;const g=[m((a=e.offset)!=null?a:6),h(),b({padding:6}),...s.value?[u({element:s.value,padding:4})]:[]],{x:j,y:q,placement:U,middlewareData:V}=yield C(l,o,{placement:(t=e.placement)!=null?t:"top",middleware:g,strategy:"fixed"});if(!e.visible||e.anchorEl!==l||n.value!==o)return!1;if(r.value.transform=`translate3d(${Math.round(j)}px, ${Math.round(q)}px, 0)`,r.value.left="0px",r.value.top="0px",c.value=U,V.arrow&&s.value){const{x:z,y:L}=V.arrow,G={top:"bottom",bottom:"top",left:"right",right:"left"}[U.split("-")[0]];y.value={left:z!=null?`${z}px`:"",top:L!=null?`${L}px`:"",[G]:"-3px"}}return!0})}function B(){var a,t;const l=e.anchorEl,o=n.value;if(!l||!o)return!1;const u=l.getBoundingClientRect(),C=o.getBoundingClientRect(),h=(a=e.offset)!=null?a:6,m=(t=e.placement)!=null?t:"top";let b=u.left,g=u.top;return m==="bottom"?g=u.bottom+h:m==="left"?b=u.left-C.width-h:m==="right"?b=u.right+h:g=u.top-C.height-h,r.value.transform=`translate3d(${Math.round(Math.max(0,b))}px, ${Math.round(Math.max(0,g))}px, 0)`,r.value.left="0px",r.value.top="0px",c.value=m,y.value={},!0}S(()=>e.visible,a=>k(null,null,function*(){const t=++f;if(a){if(i.value=!1,yield Y(),t!==f||!e.visible)return;if(e.anchorEl&&n.value)try{const l=e.anchorEl,o=n.value,u=l.getBoundingClientRect();if(!(yield $())||t!==f||!e.visible||e.anchorEl!==l||n.value!==o)return;v.value=!1,i.value=!0,yield D(()=>t===f)}catch{if(t!==f||!e.visible)return;if(i.value=B(),e.anchorEl&&n.value)try{yield D(()=>t===f)}catch{}}else i.value=!0}else T()}));let _=0;return S([()=>e.anchorEl,()=>e.placement,()=>e.content],([a],[t])=>k(null,null,function*(){v.value=!!(a&&t&&a!==t&&Math.hypot(a.getBoundingClientRect().left-t.getBoundingClientRect().left,a.getBoundingClientRect().top-t.getBoundingClientRect().top)<=120);const l=++_;if(e.visible&&e.anchorEl&&n.value){if(yield Y(),l!==_||!e.visible||!e.anchorEl||!n.value)return;try{const o=yield $();if(l!==_||!e.visible||!e.anchorEl||!n.value)return;o||B()}catch{B()}yield D(()=>l===_)}})),O(()=>{f+=1,T()}),(a,t)=>(Q(),F(ne,{to:"body"},[I("div",{class:ae(["markstream-vue",{dark:d.isDark}])},[K(le,{name:"tooltip",appear:""},{default:W(()=>[Z(I("div",{id:e.id,ref_key:"tooltip",ref:n,style:X({position:"fixed",transitionProperty:v.value?"opacity, transform":"opacity",left:r.value.left,top:r.value.top,transform:r.value.transform,visibility:i.value?"visible":"hidden",pointerEvents:i.value?void 0:"none"}),class:"tooltip-element",role:"tooltip"},[A(ee(d.content)+" ",1),I("div",{ref_key:"arrowEl",ref:s,class:"tooltip-arrow","data-placement":c.value,style:X(y.value)},null,12,re)],12,ie),[[te,d.visible]])]),_:1})],2)]))}}),[["__scopeId","data-v-c606ee4c"]]);export{se as default};

View file

@ -0,0 +1 @@
import{bQ as A,M as H,aU as b,bE as V,az as J,aL as O,s as Q,v as R,I as F,bJ as G,bL as K,aw as L,H as W,bb as Z,bB as ee,g as te,au as le,T as ae,as as B,bR as ne}from"./index--0t1wzw_.js";var k=(h,E,e)=>new Promise((o,p)=>{var i=a=>{try{d(e.next(a))}catch(c){p(c)}},y=a=>{try{d(e.throw(a))}catch(c){p(c)}},d=a=>a.done?o(a.value):Promise.resolve(a.value).then(i,y);d((e=e.apply(h,E)).next())});const oe=["id"],ie=["data-placement"],ue=A(H({__name:"Tooltip",props:{visible:{type:Boolean},anchorEl:{},content:{},placement:{},offset:{},originX:{},originY:{},id:{},isDark:{type:[Boolean,null]}},setup(h){var E;const e=h,o=b(null),p=b(null),i=b({transform:"translate3d(0px, 0px, 0px)",left:"0px",top:"0px"}),y=b({}),d=b((E=e.placement)!=null?E:"top"),a=b(!1);let c=null,$=null,T=null,C=null,w=null,s=0;function X(){return C?Promise.resolve(C):(w||(w=ne(()=>import("./floating-ui.dom-xGUaHE3m.js"),[]).then(l=>(C=l,l)).catch(l=>{throw w=null,l})),w)}function D(){c&&(c(),c=null),$=null,T=null}function P(l){return k(this,null,function*(){const t=e.anchorEl,n=o.value;if(!e.visible||!t||!n||$===t&&T===n)return;const{autoUpdate:r}=yield X();l()&&e.visible&&e.anchorEl===t&&o.value===n&&(D(),$=t,T=n,c=r(t,n,()=>{N().catch(()=>{_()})}))})}function N(){return k(this,null,function*(){var l,t;const n=e.anchorEl,r=o.value;if(!e.visible||!n||!r)return!1;const{arrow:u,computePosition:m,flip:v,offset:f,shift:x}=yield X();if(!e.visible||e.anchorEl!==n||o.value!==r)return!1;const g=[f((l=e.offset)!=null?l:6),v(),x({padding:6}),...p.value?[u({element:p.value,padding:4})]:[]],{x:S,y:j,placement:Y,middlewareData:z}=yield m(n,r,{placement:(t=e.placement)!=null?t:"top",middleware:g,strategy:"fixed"});if(!e.visible||e.anchorEl!==n||o.value!==r)return!1;if(i.value.transform=`translate3d(${Math.round(S)}px, ${Math.round(j)}px, 0)`,i.value.left="0px",i.value.top="0px",d.value=Y,z.arrow&&p.value){const{x:I,y:U}=z.arrow,q={top:"bottom",bottom:"top",left:"right",right:"left"}[Y.split("-")[0]];y.value={left:I!=null?`${I}px`:"",top:U!=null?`${U}px`:"",[q]:"-3px"}}return!0})}function _(){var l,t;const n=e.anchorEl,r=o.value;if(!n||!r)return!1;const u=n.getBoundingClientRect(),m=r.getBoundingClientRect(),v=(l=e.offset)!=null?l:6,f=(t=e.placement)!=null?t:"top";let x=u.left,g=u.top;return f==="bottom"?g=u.bottom+v:f==="left"?x=u.left-m.width-v:f==="right"?x=u.right+v:g=u.top-m.height-v,i.value.transform=`translate3d(${Math.round(Math.max(0,x))}px, ${Math.round(Math.max(0,g))}px, 0)`,i.value.left="0px",i.value.top="0px",d.value=f,y.value={},!0}V(()=>e.visible,l=>k(null,null,function*(){const t=++s;if(l){if(a.value=!1,yield B(),t!==s||!e.visible)return;if(e.anchorEl&&o.value)try{const n=e.anchorEl,r=o.value,u=n.getBoundingClientRect();if(!(yield N())||t!==s||!e.visible||e.anchorEl!==n||o.value!==r)return;const m=i.value.transform;if(e.originX!=null&&e.originY!=null){const v=Math.abs(Number(e.originX)-u.left),f=Math.abs(Number(e.originY)-u.top);if(Math.hypot(v,f)>120){if(i.value.transform=`translate3d(${Math.round(e.originX)}px, ${Math.round(e.originY)}px, 0)`,yield B(),t!==s||!e.visible||(a.value=!0,yield B(),t!==s||!e.visible))return;i.value.transform=m}else a.value=!0}else a.value=!0;yield P(()=>t===s)}catch{if(t!==s||!e.visible)return;if(a.value=_(),e.anchorEl&&o.value)try{yield P(()=>t===s)}catch{}}else a.value=!0}else a.value=!1,D()}));let M=0;return V([()=>e.anchorEl,()=>e.placement,()=>e.content],()=>k(null,null,function*(){const l=++M;if(e.visible&&e.anchorEl&&o.value){if(yield B(),l!==M||!e.visible||!e.anchorEl||!o.value)return;try{const t=yield N();if(l!==M||!e.visible||!e.anchorEl||!o.value)return;t||_()}catch{_()}yield P(()=>l===M)}})),J(()=>{s+=1,D()}),(l,t)=>(O(),Q(ae,{to:"body"},[R("div",{class:le(["markstream-vue",{dark:h.isDark}])},[F(te,{name:"tooltip",appear:""},{default:G(()=>[K(R("div",{id:e.id,ref_key:"tooltip",ref:o,style:L({position:"fixed",left:i.value.left,top:i.value.top,transform:i.value.transform,visibility:a.value?"visible":"hidden",pointerEvents:a.value?void 0:"none"}),class:"tooltip-element",role:"tooltip"},[W(Z(h.content)+" ",1),R("div",{ref_key:"arrowEl",ref:p,class:"tooltip-arrow","data-placement":d.value,style:L(y.value)},null,12,ie)],12,oe),[[ee,h.visible]])]),_:1})],2)]))}}),[["__scopeId","data-v-c606ee4c"]]);export{ue as default};

View file

@ -0,0 +1 @@
function e(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}export{e as g};

View file

@ -0,0 +1 @@
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-DcgqWRh_.js";import{p as f}from"./chunk-JWPE2WC7-C7S97jLg.js";import{_ as n,l as o}from"./mermaid.core-CqiFQExc.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},F={parser:R,db:a,renderer:u,styles:p};export{F as diagram};

View file

@ -1 +0,0 @@
import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-CClISXJW.js";import{p as f}from"./chunk-JWPE2WC7-Bgwiccxa.js";import{_ as n,l as o}from"./mermaid.core-DY8EMjrF.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-rK0CO-KU.js";import"./index-8y9QBU_r.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};

View file

@ -0,0 +1 @@
import{P as ln,Q as an,R as j,S,T as W,V as un,W as y,X as tn,Y as C,$ as _,a0 as rn,a1 as o,a2 as on,a3 as sn,a4 as fn}from"./mermaid.core-CqiFQExc.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,E,q,v,R,X,a){var I=E-l,i=q-h,n=X-v,d=a-R,u=d*I-n*i;if(!(u*u<y))return u=(n*(h-R)-d*(l-v))/u,[l+u*I,h+u*i]}function L(l,h,E,q,v,R,X){var a=l-E,I=h-q,i=(X?R:-R)/C(a*a+I*I),n=i*I,d=-i*a,u=l+n,s=h+d,f=E+n,c=q+d,Y=(u+f)/2,t=(s+c)/2,m=f-u,g=c-s,A=m*m+g*g,T=v-R,P=u*c-f*s,O=(g<0?-1:1)*C(on(0,T*T*A-P*P)),Q=(P*g-m*O)/A,V=(-P*m-g*O)/A,w=(P*g+m*O)/A,p=(-P*m+g*O)/A,x=Q-Y,e=V-t,r=w-Y,$=p-t;return x*x+e*e>r*r+$*$&&(Q=w,V=p),{cx:Q,cy:V,x01:-n,y01:-d,x11:Q*(v/T-1),y11:V*(v/T-1)}}function hn(){var l=cn,h=yn,E=W(0),q=null,v=gn,R=dn,X=mn,a=null,I=ln(i);function i(){var n,d,u=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-un,c=R.apply(this,arguments)-un,Y=rn(c-f),t=c>f;if(a||(a=n=I()),s<u&&(d=s,s=u,u=d),!(s>y))a.moveTo(0,0);else if(Y>tn-y)a.moveTo(s*j(f),s*S(f)),a.arc(0,0,s,f,c,!t),u>y&&(a.moveTo(u*j(c),u*S(c)),a.arc(0,0,u,c,f,t));else{var m=f,g=c,A=f,T=c,P=Y,O=Y,Q=X.apply(this,arguments)/2,V=Q>y&&(q?+q.apply(this,arguments):C(u*u+s*s)),w=_(rn(s-u)/2,+E.apply(this,arguments)),p=w,x=w,e,r;if(V>y){var $=sn(V/u*S(Q)),F=sn(V/s*S(Q));(P-=$*2)>y?($*=t?1:-1,A+=$,T-=$):(P=0,A=T=(f+c)/2),(O-=F*2)>y?(F*=t?1:-1,m+=F,g-=F):(O=0,m=g=(f+c)/2)}var z=s*j(m),B=s*S(m),G=u*j(T),H=u*S(T);if(w>y){var J=s*j(g),K=s*S(g),M=u*j(A),N=u*S(A),D;if(Y<an)if(D=pn(z,B,M,N,J,K,G,H)){var U=z-D[0],Z=B-D[1],k=J-D[0],b=K-D[1],nn=1/S(fn((U*k+Z*b)/(C(U*U+Z*Z)*C(k*k+b*b)))/2),en=C(D[0]*D[0]+D[1]*D[1]);p=_(w,(u-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}O>y?x>y?(e=L(M,N,z,B,s,x,t),r=L(J,K,G,H,s,x,t),a.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),a.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(a.moveTo(z,B),a.arc(0,0,s,m,g,!t)):a.moveTo(z,B),!(u>y)||!(P>y)?a.lineTo(G,H):p>y?(e=L(G,H,J,K,u,-p,t),r=L(z,B,M,N,u,-p,t),a.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(a.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),a.arc(0,0,u,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),a.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):a.arc(0,0,u,T,A,t)}if(a.closePath(),n)return a=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +R.apply(this,arguments))/2-an/2;return[j(d)*n,S(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:W(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:W(+n),i):h},i.cornerRadius=function(n){return arguments.length?(E=typeof n=="function"?n:W(+n),i):E},i.padRadius=function(n){return arguments.length?(q=n==null?null:typeof n=="function"?n:W(+n),i):q},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:W(+n),i):v},i.endAngle=function(n){return arguments.length?(R=typeof n=="function"?n:W(+n),i):R},i.padAngle=function(n){return arguments.length?(X=typeof n=="function"?n:W(+n),i):X},i.context=function(n){return arguments.length?(a=n??null,i):a},i}export{hn as d};

View file

@ -1 +0,0 @@
import{G as ln,H as un,I as N,J as I,K as J,L as an,M as y,N as tn,O as j,P as _,Q as rn,R as o,S as on,T as sn,V as fn}from"./mermaid.core-DY8EMjrF.js";function cn(l){return l.innerRadius}function yn(l){return l.outerRadius}function gn(l){return l.startAngle}function dn(l){return l.endAngle}function mn(l){return l&&l.padAngle}function pn(l,h,q,O,v,R,K,u){var D=q-l,i=O-h,n=K-v,d=u-R,a=d*D-n*i;if(!(a*a<y))return a=(n*(h-R)-d*(l-v))/a,[l+a*D,h+a*i]}function W(l,h,q,O,v,R,K){var u=l-q,D=h-O,i=(K?R:-R)/j(u*u+D*D),n=i*D,d=-i*u,a=l+n,s=h+d,f=q+n,c=O+d,L=(a+f)/2,t=(s+c)/2,m=f-a,g=c-s,A=m*m+g*g,T=v-R,P=a*c-f*s,E=(g<0?-1:1)*j(on(0,T*T*A-P*P)),G=(P*g-m*E)/A,H=(-P*m-g*E)/A,w=(P*g+m*E)/A,p=(-P*m+g*E)/A,x=G-L,e=H-t,r=w-L,M=p-t;return x*x+e*e>r*r+M*M&&(G=w,H=p),{cx:G,cy:H,x01:-n,y01:-d,x11:G*(v/T-1),y11:H*(v/T-1)}}function hn(){var l=cn,h=yn,q=J(0),O=null,v=gn,R=dn,K=mn,u=null,D=ln(i);function i(){var n,d,a=+l.apply(this,arguments),s=+h.apply(this,arguments),f=v.apply(this,arguments)-an,c=R.apply(this,arguments)-an,L=rn(c-f),t=c>f;if(u||(u=n=D()),s<a&&(d=s,s=a,a=d),!(s>y))u.moveTo(0,0);else if(L>tn-y)u.moveTo(s*N(f),s*I(f)),u.arc(0,0,s,f,c,!t),a>y&&(u.moveTo(a*N(c),a*I(c)),u.arc(0,0,a,c,f,t));else{var m=f,g=c,A=f,T=c,P=L,E=L,G=K.apply(this,arguments)/2,H=G>y&&(O?+O.apply(this,arguments):j(a*a+s*s)),w=_(rn(s-a)/2,+q.apply(this,arguments)),p=w,x=w,e,r;if(H>y){var M=sn(H/a*I(G)),z=sn(H/s*I(G));(P-=M*2)>y?(M*=t?1:-1,A+=M,T-=M):(P=0,A=T=(f+c)/2),(E-=z*2)>y?(z*=t?1:-1,m+=z,g-=z):(E=0,m=g=(f+c)/2)}var Q=s*N(m),V=s*I(m),B=a*N(T),C=a*I(T);if(w>y){var F=s*N(g),U=s*I(g),X=a*N(A),Y=a*I(A),S;if(L<un)if(S=pn(Q,V,X,Y,F,U,B,C)){var Z=Q-S[0],$=V-S[1],k=F-S[0],b=U-S[1],nn=1/I(fn((Z*k+$*b)/(j(Z*Z+$*$)*j(k*k+b*b)))/2),en=j(S[0]*S[0]+S[1]*S[1]);p=_(w,(a-en)/(nn-1)),x=_(w,(s-en)/(nn+1))}else p=x=0}E>y?x>y?(e=W(X,Y,Q,V,s,x,t),r=W(F,U,B,C,s,x,t),u.moveTo(e.cx+e.x01,e.cy+e.y01),x<w?u.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(r.y01,r.x01),!t):(u.arc(e.cx,e.cy,x,o(e.y01,e.x01),o(e.y11,e.x11),!t),u.arc(0,0,s,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),!t),u.arc(r.cx,r.cy,x,o(r.y11,r.x11),o(r.y01,r.x01),!t))):(u.moveTo(Q,V),u.arc(0,0,s,m,g,!t)):u.moveTo(Q,V),!(a>y)||!(P>y)?u.lineTo(B,C):p>y?(e=W(B,C,F,U,a,-p,t),r=W(Q,V,X,Y,a,-p,t),u.lineTo(e.cx+e.x01,e.cy+e.y01),p<w?u.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(r.y01,r.x01),!t):(u.arc(e.cx,e.cy,p,o(e.y01,e.x01),o(e.y11,e.x11),!t),u.arc(0,0,a,o(e.cy+e.y11,e.cx+e.x11),o(r.cy+r.y11,r.cx+r.x11),t),u.arc(r.cx,r.cy,p,o(r.y11,r.x11),o(r.y01,r.x01),!t))):u.arc(0,0,a,T,A,t)}if(u.closePath(),n)return u=null,n+""||null}return i.centroid=function(){var n=(+l.apply(this,arguments)+ +h.apply(this,arguments))/2,d=(+v.apply(this,arguments)+ +R.apply(this,arguments))/2-un/2;return[N(d)*n,I(d)*n]},i.innerRadius=function(n){return arguments.length?(l=typeof n=="function"?n:J(+n),i):l},i.outerRadius=function(n){return arguments.length?(h=typeof n=="function"?n:J(+n),i):h},i.cornerRadius=function(n){return arguments.length?(q=typeof n=="function"?n:J(+n),i):q},i.padRadius=function(n){return arguments.length?(O=n==null?null:typeof n=="function"?n:J(+n),i):O},i.startAngle=function(n){return arguments.length?(v=typeof n=="function"?n:J(+n),i):v},i.endAngle=function(n){return arguments.length?(R=typeof n=="function"?n:J(+n),i):R},i.padAngle=function(n){return arguments.length?(K=typeof n=="function"?n:J(+n),i):K},i.context=function(n){return arguments.length?(u=n??null,i):u},i}export{hn as d};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{U as a,O as n}from"./mermaid.core-CqiFQExc.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};

View file

@ -1 +0,0 @@
import{U as a,C as n}from"./mermaid.core-DY8EMjrF.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};

View file

@ -1 +1 @@
import{_ as i}from"./mermaid.core-DY8EMjrF.js";var r=class{constructor(t){this.init=t,this.records=this.init()}static{i(this,"ImperativeState")}reset(){this.records=this.init()}};export{r as I};
import{_ as i}from"./mermaid.core-CqiFQExc.js";var r=class{constructor(t){this.init=t,this.records=this.init()}static{i(this,"ImperativeState")}reset(){this.records=this.init()}};export{r as I};

View file

@ -1 +1 @@
import{_ as i,d as l,n as d,j as o}from"./mermaid.core-DY8EMjrF.js";var x=i((r,t)=>{const e=r.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const s in t.attrs)e.attr(s,t.attrs[s]);return t.class&&e.attr("class",t.class),e},"drawRect"),p=i((r,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};x(r,e).lower()},"drawBackgroundRect"),y=i((r,t)=>{const e=t.text.replace(d," "),s=r.append("text");s.attr("x",t.x),s.attr("y",t.y),s.attr("class","legend"),s.style("text-anchor",t.anchor),t.class&&s.attr("class",t.class);const a=s.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.text(e),s},"drawText"),m=i((r,t,e,s)=>{const a=r.append("image");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",n)},"drawImage"),g=i((r,t,e,s)=>{const a=r.append("use");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",`#${n}`)},"drawEmbeddedImage"),h=i(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),f=i(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj"),w=i(()=>{let r=l(".mermaidTooltip");return r.empty()&&(r=l("body").append("div").attr("class","mermaidTooltip").style("opacity",0).style("position","absolute").style("text-align","center").style("max-width","200px").style("padding","2px").style("font-size","12px").style("background","#ffffde").style("border","1px solid #333").style("border-radius","2px").style("pointer-events","none").style("z-index","100")),r},"createTooltip");export{p as a,f as b,g as c,x as d,m as e,w as f,h as g,y as h};
import{_ as i,d as l,N as d,j as o}from"./mermaid.core-CqiFQExc.js";var x=i((r,t)=>{const e=r.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const s in t.attrs)e.attr(s,t.attrs[s]);return t.class&&e.attr("class",t.class),e},"drawRect"),p=i((r,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};x(r,e).lower()},"drawBackgroundRect"),y=i((r,t)=>{const e=t.text.replace(d," "),s=r.append("text");s.attr("x",t.x),s.attr("y",t.y),s.attr("class","legend"),s.style("text-anchor",t.anchor),t.class&&s.attr("class",t.class);const a=s.append("tspan");return a.attr("x",t.x+t.textMargin*2),a.text(e),s},"drawText"),m=i((r,t,e,s)=>{const a=r.append("image");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",n)},"drawImage"),g=i((r,t,e,s)=>{const a=r.append("use");a.attr("x",t),a.attr("y",e);const n=o.sanitizeUrl(s);a.attr("xlink:href",`#${n}`)},"drawEmbeddedImage"),h=i(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),f=i(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj"),w=i(()=>{let r=l(".mermaidTooltip");return r.empty()&&(r=l("body").append("div").attr("class","mermaidTooltip").style("opacity",0).style("position","absolute").style("text-align","center").style("max-width","200px").style("padding","2px").style("font-size","12px").style("background","#ffffde").style("border","1px solid #333").style("border-radius","2px").style("pointer-events","none").style("z-index","100")),r},"createTooltip");export{p as a,f as b,g as c,x as d,m as e,w as f,h as g,y as h};

View file

@ -1,4 +1,4 @@
import{_ as e}from"./mermaid.core-DY8EMjrF.js";var l=e(()=>`
import{_ as e}from"./mermaid.core-CqiFQExc.js";var l=e(()=>`
/* Font Awesome icon styling - consolidated */
.label-icon {
display: inline-block;

View file

@ -1 +1 @@
import{_ as i}from"./mermaid.core-DY8EMjrF.js";function t(c,e){c.accDescr&&e.setAccDescription?.(c.accDescr),c.accTitle&&e.setAccTitle?.(c.accTitle),c.title&&e.setDiagramTitle?.(c.title)}i(t,"populateCommonDb");export{t as p};
import{_ as i}from"./mermaid.core-CqiFQExc.js";function t(c,e){c.accDescr&&e.setAccDescription?.(c.accDescr),c.accTitle&&e.setAccTitle?.(c.accTitle),c.title&&e.setDiagramTitle?.(c.title)}i(t,"populateCommonDb");export{t as p};

View file

@ -1 +1 @@
import{_ as a,e as w,l as x}from"./mermaid.core-DY8EMjrF.js";var d=a((e,t,i,r)=>{e.attr("class",i);const{width:o,height:h,x:n,y:c}=u(e,t);w(e,h,o,r);const s=l(n,c,o,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{const i=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),l=a((e,t,i,r,o)=>`${e-o} ${t-o} ${i} ${r}`,"createViewBox");export{d as s};
import{_ as a,e as w,l as x}from"./mermaid.core-CqiFQExc.js";var d=a((e,t,i,r)=>{e.attr("class",i);const{width:o,height:h,x:n,y:c}=u(e,t);w(e,h,o,r);const s=l(n,c,o,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{const i=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),l=a((e,t,i,r,o)=>`${e-o} ${t-o} ${i} ${r}`,"createViewBox");export{d as s};

View file

@ -1 +1 @@
import{_ as a,d as o}from"./mermaid.core-DY8EMjrF.js";var d=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{d as g};
import{_ as a,d as o}from"./mermaid.core-CqiFQExc.js";var d=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{d as g};

View file

@ -1 +0,0 @@
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-B6kqo8yw.js";import{_ as i}from"./mermaid.core-DY8EMjrF.js";import"./chunk-5VM5RSS4-C2_xpzY3.js";import"./chunk-XXDRQBXY-Vyf77H8-.js";import"./chunk-VR4S4FIN-Cm7c9b3N.js";import"./chunk-32BRIVSS-Bb3U5zL8.js";import"./index-8y9QBU_r.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};

View file

@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-Ci6LL3n4.js";import{_ as i}from"./mermaid.core-CqiFQExc.js";import"./chunk-5VM5RSS4-BRWxG5tM.js";import"./chunk-XXDRQBXY-4LplLZkT.js";import"./chunk-VR4S4FIN-DW1NNlBr.js";import"./chunk-32BRIVSS-mfMW4-bV.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};

View file

@ -1 +0,0 @@
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-B6kqo8yw.js";import{_ as i}from"./mermaid.core-DY8EMjrF.js";import"./chunk-5VM5RSS4-C2_xpzY3.js";import"./chunk-XXDRQBXY-Vyf77H8-.js";import"./chunk-VR4S4FIN-Cm7c9b3N.js";import"./chunk-32BRIVSS-Bb3U5zL8.js";import"./index-8y9QBU_r.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};

View file

@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-Ci6LL3n4.js";import{_ as i}from"./mermaid.core-CqiFQExc.js";import"./chunk-5VM5RSS4-BRWxG5tM.js";import"./chunk-XXDRQBXY-4LplLZkT.js";import"./chunk-VR4S4FIN-DW1NNlBr.js";import"./chunk-32BRIVSS-mfMW4-bV.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var f={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{f as diagram};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
import{c as O,w as I,a as J,f as P,b as E,s as A}from"./chunk-RYQCIY6F-DRHEFBXE.js";import{_ as w,am as v,an as D,ao as H,ap as Y,l,c as _,aq as W,ar as $,ag as j,as as q,ah as R,af as F,at as z,au as K,av as G}from"./mermaid.core-CqiFQExc.js";import{G as Q}from"./graph-DOmOIIwC.js";import{l as U}from"./layout-D-LzfAck.js";import"./map-DxJ2ADlA.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var C=w((s,t,g)=>Math.max(t,Math.min(g,s)),"clamp"),B=w((s="TB")=>{switch(s){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),V=w(s=>s==="flowchart"||s==="flowchart-v2"||s==="stateDiagram","shouldMergeSelfLoopSegments"),Z=w((s,t,g,m,c)=>{const o=[],r=new Set;if(g.forEach(({start:i,end:n})=>{i!==m&&r.add(i),n!==m&&r.add(n)}),r.forEach(i=>{const n=s.node(i);typeof n?.x=="number"&&typeof n?.y=="number"&&o.push(n)}),o.length===0&&g.forEach(({edge:i})=>{(i.points??[]).forEach(n=>{typeof n?.x=="number"&&typeof n?.y=="number"&&o.push(n)})}),o.length===0)return B(c);const f=o.reduce((i,n)=>({x:i.x+n.x/o.length,y:i.y+n.y/o.length}),{x:0,y:0}),h=f.x-t.x,a=f.y-t.y;return Math.abs(h)>Math.abs(a)?h>0?"right":"left":Math.abs(a)>0?a>0?"bottom":"top":B(c)},"getSelfLoopSide"),ee=w((s,t="top",g=0,m=0)=>{const c=s.x,o=s.y-g,r=s.width/2,f=s.height/2,h=Math.max(36,Math.min(100,s.width*.8)),a=C(Math.max(m,s.width*.35),36,h),i=C(Math.min(s.width,s.height)*.45,24,48);switch(t){case"bottom":{const n=o+f;return[{x:c-a/2,y:n},{x:c-a/2,y:n+i},{x:c+a/2,y:n+i},{x:c+a/2,y:n}]}case"right":{const n=c+r;return[{x:n,y:o-a/2},{x:n+i,y:o-a/2},{x:n+i,y:o+a/2},{x:n,y:o+a/2}]}case"left":{const n=c-r;return[{x:n,y:o-a/2},{x:n-i,y:o-a/2},{x:n-i,y:o+a/2},{x:n,y:o+a/2}]}case"top":default:{const n=o-f;return[{x:c-a/2,y:n},{x:c-a/2,y:n-i},{x:c+a/2,y:n-i},{x:c+a/2,y:n}]}}},"getSelfLoopPoints"),te=w((s,t,g="top",m=0,c={})=>{const r=s.x,f=s.y-m,h=c.width??0,a=c.height??0;switch(g){case"bottom":return{x:r,y:Math.max(...t.map(i=>i.y))+a/2+4};case"right":return{x:Math.max(...t.map(i=>i.x))+h/2+4,y:f};case"left":return{x:Math.min(...t.map(i=>i.x))-h/2-4,y:f};case"top":default:return{x:r,y:Math.min(...t.map(i=>i.y))-a/2-4}}},"getSelfLoopLabelPosition"),ne=w((s,t=0,{mergeSelfLoops:g=!0}={})=>{const m=new Map,c=[],o=s.graph()?.rankdir;return s.edges().forEach(r=>{const f=s.edge(r);if(g&&f.selfLoop){const h=f.selfLoop.id;m.has(h)||m.set(h,[]),m.get(h).push({edge:f,start:r.v,end:r.w})}else c.push({edge:f,start:r.v,end:r.w})}),m.forEach(r=>{if(r.length!==3){r.forEach(L=>c.push(L));return}r.sort((L,d)=>L.edge.selfLoop.order-d.edge.selfLoop.order);const[f,h,a]=r,i=f.edge.originalEdge??h.edge.originalEdge??a.edge.originalEdge??h.edge,n=s.node(i.start);if(!n){r.forEach(L=>c.push(L));return}const p={width:h.edge.width,height:h.edge.height},y=Z(s,n,r,i.start,o),X=ee(n,y,t,p.width??0),S=te(n,X,y,t,p),b={...h.edge,...i,id:i.id,points:X,start:i.start,end:i.end,x:S.x,y:S.y,width:p.width,height:p.height,labelStyle:h.edge.labelStyle,fromCluster:f.edge.fromCluster??h.edge.fromCluster??a.edge.fromCluster,toCluster:f.edge.toCluster??h.edge.toCluster??a.edge.toCluster};delete b.selfLoop,delete b.originalEdge,c.push({edge:b,start:b.start,end:b.end})}),c},"getEdgesToRender"),T=w(async(s,t,g,m,c,o)=>{l.warn("Graph in recursive render:XAX",I(t),c);const r=t.graph().rankdir;l.trace("Dir in recursive render - dir:",r);const f=s.insert("g").attr("class","root");t.nodes()?l.info("Recursive render XXX",t.nodes()):l.info("No nodes found for",t),t.edges().length>0&&l.info("Recursive edges",t.edge(t.edges()[0]));const h=f.insert("g").attr("class","clusters"),a=f.insert("g").attr("class","edgePaths"),i=f.insert("g").attr("class","edgeLabels"),n=f.insert("g").attr("class","nodes"),p=V(g);await Promise.all(t.nodes().map(async function(d){const e=t.node(d);if(c!==void 0){const u=JSON.parse(JSON.stringify(c.clusterData));l.trace(`Setting data for parent cluster XXX
Node.id = `,d,`
data=`,u.height,`
Parent cluster`,c.height),t.setNode(c.id,u),t.parent(d)||(l.trace("Setting parent",d,c.id),t.setParent(d,c.id,u))}if(l.info("(Insert) Node XXX"+d+": "+JSON.stringify(t.node(d))),e?.clusterNode){l.info("Cluster identified XBX",d,e.width,t.node(d));const{ranksep:u,nodesep:x}=t.graph();e.graph.setGraph({...e.graph.graph(),ranksep:u+25,nodesep:x});const N=await T(n,e.graph,g,m,t.node(d),o),M=N.elem;W(e,M),e.diff=N.diff||0,l.info("New compound node after recursive render XAX",d,"width",e.width,"height",e.height),$(M,e)}else t.children(d).length>0?(l.trace("Cluster - the non recursive path XBX",d,e.id,e,e.width,"Graph:",t),l.trace(P(e.id,t)),E.set(e.id,{id:P(e.id,t),node:e})):(l.trace("Node - the non recursive path XAX",d,n,t.node(d),r),await j(n,t.node(d),{config:o,dir:r}))})),await w(async()=>{const d=t.edges().map(async function(e){const u=t.edge(e.v,e.w,e.name);if(l.info("Edge "+e.v+" -> "+e.w+": "+JSON.stringify(e)),l.info("Edge "+e.v+" -> "+e.w+": ",e," ",JSON.stringify(t.edge(e))),l.info("Fix",E,"ids:",e.v,e.w,"Translating: ",E.get(e.v),E.get(e.w)),p&&u.selfLoop){if(u.selfLoop.order!==1)return;const x=u.id;u.id=u.selfLoop.id,await G(i,u),u.id=x;return}await G(i,u)});await Promise.all(d)},"processEdges")(),l.info("Graph before layout:",JSON.stringify(I(t))),l.info("############################################# XXX"),l.info("### Layout ### XXX"),l.info("############################################# XXX"),U(t),l.info("Graph after layout:",JSON.stringify(I(t)));let X=0,{subGraphTitleTotalMargin:S}=q(o);await Promise.all(A(t).map(async function(d){const e=t.node(d);if(l.info("Position XBX => "+d+": ("+e.x,","+e.y,") width: ",e.width," height: ",e.height),e?.clusterNode)e.y+=S,l.info("A tainted cluster node XBX1",d,e.id,e.width,e.height,e.x,e.y,t.parent(d)),E.get(e.id).node=e,R(e);else if(t.children(d).length>0){l.info("A pure cluster node XBX1",d,e.id,e.x,e.y,e.width,e.height,t.parent(d)),e.height+=S,t.node(e.parentId);const u=e?.padding/2||0,x=e?.labelBBox?.height||0,N=x-u||0;l.debug("OffsetY",N,"labelHeight",x,"halfPadding",u),await F(h,e),E.get(e.id).node=e}else{const u=t.node(e.parentId);e.y+=S/2,l.info("A regular node XBX1 - using the padding",e.id,"parent",e.parentId,e.width,e.height,e.x,e.y,"offsetY",e.offsetY,"parent",u,u?.offsetY,e),R(e)}}));const b=S/2;return ne(t,b,{mergeSelfLoops:p}).forEach(function({edge:d,start:e,end:u}){l.info("Edge "+e+" -> "+u+": "+JSON.stringify(d),d),d.points.forEach(k=>k.y+=b);const x=t.node(e),N=t.node(u),M=z(a,d,E,g,x,N,m);K(d,M)}),t.nodes().forEach(function(d){const e=t.node(d);l.info(d,e.type,e.diff),e.isGroup&&(X=e.diff)}),l.warn("Returning from recursive render XAX",f,X),{elem:f,diff:X}},"recursiveRender"),le=w(async(s,t)=>{const g=new Q({multigraph:!0,compound:!0}).setGraph({rankdir:s.direction,nodesep:s.config?.nodeSpacing||s.config?.flowchart?.nodeSpacing||s.nodeSpacing,ranksep:s.config?.rankSpacing||s.config?.flowchart?.rankSpacing||s.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),m=t.select("g");v(m,s.markers,s.type,s.diagramId),D(),H(),Y(),O(),s.nodes.forEach(o=>{g.setNode(o.id,{...o}),o.parentId&&g.setParent(o.id,o.parentId)}),l.debug("Edges:",s.edges),s.edges.forEach(o=>{if(o.start===o.end){const r=o.start,f=r+"---"+r+"---1",h=r+"---"+r+"---2",a=g.node(r);g.setNode(f,{domId:f,id:f,parentId:a.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),g.setParent(f,a.parentId),g.setNode(h,{domId:h,id:h,parentId:a.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),g.setParent(h,a.parentId);const i=structuredClone(o),n=structuredClone(o),p=structuredClone(o),y=structuredClone(o);n.originalEdge=i,n.selfLoop={id:i.id,order:0},p.originalEdge=i,p.selfLoop={id:i.id,order:1},y.originalEdge=i,y.selfLoop={id:i.id,order:2},n.label="",n.arrowTypeEnd="none",n.endLabelLeft="",n.endLabelRight="",n.startLabelLeft="",n.id=r+"-cyclic-special-1",p.startLabelRight="",p.startLabelLeft="",p.endLabelLeft="",p.endLabelRight="",p.arrowTypeStart="none",p.arrowTypeEnd="none",p.id=r+"-cyclic-special-mid",y.label="",y.startLabelRight="",y.startLabelLeft="",y.arrowTypeStart="none",a.isGroup&&(n.fromCluster=r,y.toCluster=r),y.id=r+"-cyclic-special-2",y.arrowTypeStart="none",g.setEdge(r,f,n,r+"-cyclic-special-0"),g.setEdge(f,h,p,r+"-cyclic-special-1"),g.setEdge(h,r,y,r+"-cyclic-special-2")}else g.setEdge(o.start,o.end,{...o},o.id)}),l.warn("Graph at first:",JSON.stringify(I(g))),J(g),l.warn("Graph after XAX:",JSON.stringify(I(g)));const c=_();await T(m,g,s.type,s.diagramId,void 0,c)},"render");export{ne as getEdgesToRender,le as render};

View file

@ -1,4 +0,0 @@
import{c as O,w as I,a as J,f as P,b as E,s as A}from"./chunk-RYQCIY6F-H_GYUYTT.js";import{_ as w,am as v,an as D,ao as H,ap as Y,l,c as _,aq as W,ar as $,ag as j,as as q,ah as R,af as F,at as z,au as K,av as G}from"./mermaid.core-DY8EMjrF.js";import{G as Q}from"./graph-DOmOIIwC.js";import{l as U}from"./layout-D-LzfAck.js";import"./map-DxJ2ADlA.js";import"./index-8y9QBU_r.js";var C=w((s,t,g)=>Math.max(t,Math.min(g,s)),"clamp"),B=w((s="TB")=>{switch(s){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),V=w(s=>s==="flowchart"||s==="flowchart-v2"||s==="stateDiagram","shouldMergeSelfLoopSegments"),Z=w((s,t,g,y,c)=>{const o=[],r=new Set;if(g.forEach(({start:i,end:n})=>{i!==y&&r.add(i),n!==y&&r.add(n)}),r.forEach(i=>{const n=s.node(i);typeof n?.x=="number"&&typeof n?.y=="number"&&o.push(n)}),o.length===0&&g.forEach(({edge:i})=>{(i.points??[]).forEach(n=>{typeof n?.x=="number"&&typeof n?.y=="number"&&o.push(n)})}),o.length===0)return B(c);const f=o.reduce((i,n)=>({x:i.x+n.x/o.length,y:i.y+n.y/o.length}),{x:0,y:0}),h=f.x-t.x,a=f.y-t.y;return Math.abs(h)>Math.abs(a)?h>0?"right":"left":Math.abs(a)>0?a>0?"bottom":"top":B(c)},"getSelfLoopSide"),ee=w((s,t="top",g=0,y=0)=>{const c=s.x,o=s.y-g,r=s.width/2,f=s.height/2,h=Math.max(36,Math.min(100,s.width*.8)),a=C(Math.max(y,s.width*.35),36,h),i=C(Math.min(s.width,s.height)*.45,24,48);switch(t){case"bottom":{const n=o+f;return[{x:c-a/2,y:n},{x:c-a/2,y:n+i},{x:c+a/2,y:n+i},{x:c+a/2,y:n}]}case"right":{const n=c+r;return[{x:n,y:o-a/2},{x:n+i,y:o-a/2},{x:n+i,y:o+a/2},{x:n,y:o+a/2}]}case"left":{const n=c-r;return[{x:n,y:o-a/2},{x:n-i,y:o-a/2},{x:n-i,y:o+a/2},{x:n,y:o+a/2}]}case"top":default:{const n=o-f;return[{x:c-a/2,y:n},{x:c-a/2,y:n-i},{x:c+a/2,y:n-i},{x:c+a/2,y:n}]}}},"getSelfLoopPoints"),te=w((s,t,g="top",y=0,c={})=>{const r=s.x,f=s.y-y,h=c.width??0,a=c.height??0;switch(g){case"bottom":return{x:r,y:Math.max(...t.map(i=>i.y))+a/2+4};case"right":return{x:Math.max(...t.map(i=>i.x))+h/2+4,y:f};case"left":return{x:Math.min(...t.map(i=>i.x))-h/2-4,y:f};case"top":default:return{x:r,y:Math.min(...t.map(i=>i.y))-a/2-4}}},"getSelfLoopLabelPosition"),ne=w((s,t=0,{mergeSelfLoops:g=!0}={})=>{const y=new Map,c=[],o=s.graph()?.rankdir;return s.edges().forEach(r=>{const f=s.edge(r);if(g&&f.selfLoop){const h=f.selfLoop.id;y.has(h)||y.set(h,[]),y.get(h).push({edge:f,start:r.v,end:r.w})}else c.push({edge:f,start:r.v,end:r.w})}),y.forEach(r=>{if(r.length!==3){r.forEach(L=>c.push(L));return}r.sort((L,d)=>L.edge.selfLoop.order-d.edge.selfLoop.order);const[f,h,a]=r,i=f.edge.originalEdge??h.edge.originalEdge??a.edge.originalEdge??h.edge,n=s.node(i.start);if(!n){r.forEach(L=>c.push(L));return}const p={width:h.edge.width,height:h.edge.height},m=Z(s,n,r,i.start,o),X=ee(n,m,t,p.width??0),S=te(n,X,m,t,p),b={...h.edge,...i,id:i.id,points:X,start:i.start,end:i.end,x:S.x,y:S.y,width:p.width,height:p.height,labelStyle:h.edge.labelStyle,fromCluster:f.edge.fromCluster??h.edge.fromCluster??a.edge.fromCluster,toCluster:f.edge.toCluster??h.edge.toCluster??a.edge.toCluster};delete b.selfLoop,delete b.originalEdge,c.push({edge:b,start:b.start,end:b.end})}),c},"getEdgesToRender"),T=w(async(s,t,g,y,c,o)=>{l.warn("Graph in recursive render:XAX",I(t),c);const r=t.graph().rankdir;l.trace("Dir in recursive render - dir:",r);const f=s.insert("g").attr("class","root");t.nodes()?l.info("Recursive render XXX",t.nodes()):l.info("No nodes found for",t),t.edges().length>0&&l.info("Recursive edges",t.edge(t.edges()[0]));const h=f.insert("g").attr("class","clusters"),a=f.insert("g").attr("class","edgePaths"),i=f.insert("g").attr("class","edgeLabels"),n=f.insert("g").attr("class","nodes"),p=V(g);await Promise.all(t.nodes().map(async function(d){const e=t.node(d);if(c!==void 0){const u=JSON.parse(JSON.stringify(c.clusterData));l.trace(`Setting data for parent cluster XXX
Node.id = `,d,`
data=`,u.height,`
Parent cluster`,c.height),t.setNode(c.id,u),t.parent(d)||(l.trace("Setting parent",d,c.id),t.setParent(d,c.id,u))}if(l.info("(Insert) Node XXX"+d+": "+JSON.stringify(t.node(d))),e?.clusterNode){l.info("Cluster identified XBX",d,e.width,t.node(d));const{ranksep:u,nodesep:x}=t.graph();e.graph.setGraph({...e.graph.graph(),ranksep:u+25,nodesep:x});const N=await T(n,e.graph,g,y,t.node(d),o),M=N.elem;W(e,M),e.diff=N.diff||0,l.info("New compound node after recursive render XAX",d,"width",e.width,"height",e.height),$(M,e)}else t.children(d).length>0?(l.trace("Cluster - the non recursive path XBX",d,e.id,e,e.width,"Graph:",t),l.trace(P(e.id,t)),E.set(e.id,{id:P(e.id,t),node:e})):(l.trace("Node - the non recursive path XAX",d,n,t.node(d),r),await j(n,t.node(d),{config:o,dir:r}))})),await w(async()=>{const d=t.edges().map(async function(e){const u=t.edge(e.v,e.w,e.name);if(l.info("Edge "+e.v+" -> "+e.w+": "+JSON.stringify(e)),l.info("Edge "+e.v+" -> "+e.w+": ",e," ",JSON.stringify(t.edge(e))),l.info("Fix",E,"ids:",e.v,e.w,"Translating: ",E.get(e.v),E.get(e.w)),p&&u.selfLoop){if(u.selfLoop.order!==1)return;const x=u.id;u.id=u.selfLoop.id,await G(i,u),u.id=x;return}await G(i,u)});await Promise.all(d)},"processEdges")(),l.info("Graph before layout:",JSON.stringify(I(t))),l.info("############################################# XXX"),l.info("### Layout ### XXX"),l.info("############################################# XXX"),U(t),l.info("Graph after layout:",JSON.stringify(I(t)));let X=0,{subGraphTitleTotalMargin:S}=q(o);await Promise.all(A(t).map(async function(d){const e=t.node(d);if(l.info("Position XBX => "+d+": ("+e.x,","+e.y,") width: ",e.width," height: ",e.height),e?.clusterNode)e.y+=S,l.info("A tainted cluster node XBX1",d,e.id,e.width,e.height,e.x,e.y,t.parent(d)),E.get(e.id).node=e,R(e);else if(t.children(d).length>0){l.info("A pure cluster node XBX1",d,e.id,e.x,e.y,e.width,e.height,t.parent(d)),e.height+=S,t.node(e.parentId);const u=e?.padding/2||0,x=e?.labelBBox?.height||0,N=x-u||0;l.debug("OffsetY",N,"labelHeight",x,"halfPadding",u),await F(h,e),E.get(e.id).node=e}else{const u=t.node(e.parentId);e.y+=S/2,l.info("A regular node XBX1 - using the padding",e.id,"parent",e.parentId,e.width,e.height,e.x,e.y,"offsetY",e.offsetY,"parent",u,u?.offsetY,e),R(e)}}));const b=S/2;return ne(t,b,{mergeSelfLoops:p}).forEach(function({edge:d,start:e,end:u}){l.info("Edge "+e+" -> "+u+": "+JSON.stringify(d),d),d.points.forEach(k=>k.y+=b);const x=t.node(e),N=t.node(u),M=z(a,d,E,g,x,N,y);K(d,M)}),t.nodes().forEach(function(d){const e=t.node(d);l.info(d,e.type,e.diff),e.isGroup&&(X=e.diff)}),l.warn("Returning from recursive render XAX",f,X),{elem:f,diff:X}},"recursiveRender"),ce=w(async(s,t)=>{const g=new Q({multigraph:!0,compound:!0}).setGraph({rankdir:s.direction,nodesep:s.config?.nodeSpacing||s.config?.flowchart?.nodeSpacing||s.nodeSpacing,ranksep:s.config?.rankSpacing||s.config?.flowchart?.rankSpacing||s.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),y=t.select("g");v(y,s.markers,s.type,s.diagramId),D(),H(),Y(),O(),s.nodes.forEach(o=>{g.setNode(o.id,{...o}),o.parentId&&g.setParent(o.id,o.parentId)}),l.debug("Edges:",s.edges),s.edges.forEach(o=>{if(o.start===o.end){const r=o.start,f=r+"---"+r+"---1",h=r+"---"+r+"---2",a=g.node(r);g.setNode(f,{domId:f,id:f,parentId:a.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),g.setParent(f,a.parentId),g.setNode(h,{domId:h,id:h,parentId:a.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),g.setParent(h,a.parentId);const i=structuredClone(o),n=structuredClone(o),p=structuredClone(o),m=structuredClone(o);n.originalEdge=i,n.selfLoop={id:i.id,order:0},p.originalEdge=i,p.selfLoop={id:i.id,order:1},m.originalEdge=i,m.selfLoop={id:i.id,order:2},n.label="",n.arrowTypeEnd="none",n.endLabelLeft="",n.endLabelRight="",n.startLabelLeft="",n.id=r+"-cyclic-special-1",p.startLabelRight="",p.startLabelLeft="",p.endLabelLeft="",p.endLabelRight="",p.arrowTypeStart="none",p.arrowTypeEnd="none",p.id=r+"-cyclic-special-mid",m.label="",m.startLabelRight="",m.startLabelLeft="",m.arrowTypeStart="none",a.isGroup&&(n.fromCluster=r,m.toCluster=r),m.id=r+"-cyclic-special-2",m.arrowTypeStart="none",g.setEdge(r,f,n,r+"-cyclic-special-0"),g.setEdge(f,h,p,r+"-cyclic-special-1"),g.setEdge(h,r,m,r+"-cyclic-special-2")}else g.setEdge(o.start,o.end,{...o},o.id)}),l.warn("Graph at first:",JSON.stringify(I(g))),J(g),l.warn("Graph after XAX:",JSON.stringify(I(g)));const c=_();await T(y,g,s.type,s.diagramId,void 0,c)},"render");export{ne as getEdgesToRender,ce as render};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,24 @@
import{p as B}from"./chunk-JWPE2WC7-C7S97jLg.js";import{_ as b,A as u,D as $,e as C,l as m,b as S,a as D,n as T,o as P,g as z,s as A,y as E,B as F,p as W}from"./mermaid.core-CqiFQExc.js";import{p as _}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var N=F.packet,w=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=P,this.getAccDescription=z,this.setAccDescription=A}static{b(this,"PacketDB")}getConfig(){const t=u({...N,...E().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},L=1e4,M=b((t,e)=>{B(t,e);let r=-1,o=[],n=1;const{bitsPerRow:l}=e.getConfig();for(let{start:a,end:i,bits:d,label:c}of t.blocks){if(a!==void 0&&i!==void 0&&i<a)throw new Error(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)throw new Error(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)throw new Error(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,m.debug(`Packet block ${a} - ${r} with label ${c}`);o.length<=l+1&&e.getPacket().length<L;){const[p,s]=Y({start:a,end:i,bits:d,label:c},n,l);if(o.push(p),p.end+1===n*l&&(e.pushWord(o),o=[],n++),!s)break;({start:a,end:i,bits:d,label:c}=s)}}e.pushWord(o)},"populate"),Y=b((t,e,r)=>{if(t.start===void 0)throw new Error("start should have been set during first phase");if(t.end===void 0)throw new Error("end should have been set during first phase");if(t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);if(t.end+1<=e*r)return[t,void 0];const o=e*r-1,n=e*r;return[{start:t.start,end:o,label:t.label,bits:o-t.start},{start:n,end:t.end,label:t.label,bits:t.end-n}]},"getNextFittingBlock"),v={parser:{yy:void 0},parse:b(async t=>{const e=await _("packet",t),r=v.parser?.yy;if(!(r instanceof w))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");m.debug(e),M(e,r)},"parse")},I=b((t,e,r,o)=>{const n=o.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),s=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(s?0:a),k=d*c+2,f=$(e);f.attr("viewBox",`0 0 ${k} ${g}`),C(f,g,k,l.useMaxWidth);for(const[x,y]of p.entries())O(f,y,x,l);f.append("text").text(s).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((t,e,r,{rowHeight:o,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{const c=t.append("g"),p=r*(o+l)+l;for(const s of e){const h=s.start%i*a+1,g=(s.end-s.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",o).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+o/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(s.label),!d)continue;const k=s.end===s.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(s.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(s.end)}},"drawWord"),j={draw:I},G={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},H=b(({packet:t}={})=>{const e=u(G,t);return`
.packetByte {
font-size: ${e.byteFontSize};
}
.packetByte.start {
fill: ${e.startByteColor};
}
.packetByte.end {
fill: ${e.endByteColor};
}
.packetLabel {
fill: ${e.labelColor};
font-size: ${e.labelFontSize};
}
.packetTitle {
fill: ${e.titleColor};
font-size: ${e.titleFontSize};
}
.packetBlock {
stroke: ${e.blockStrokeColor};
stroke-width: ${e.blockStrokeWidth};
fill: ${e.blockFillColor};
}
`},"styles"),J={parser:v,get db(){return new w},renderer:j,styles:H};export{J as diagram};

View file

@ -1,24 +0,0 @@
import{p as B}from"./chunk-JWPE2WC7-Bgwiccxa.js";import{_ as b,B as u,F as $,e as C,l as m,b as S,a as D,o as T,p as z,g as F,s as P,z as E,D as A,q as W}from"./mermaid.core-DY8EMjrF.js";import{p as _}from"./cynefin-VYW2F7L2-rK0CO-KU.js";import"./index-8y9QBU_r.js";var N=A.packet,w=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=z,this.getAccDescription=F,this.setAccDescription=P}static{b(this,"PacketDB")}getConfig(){const t=u({...N,...E().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},L=1e4,M=b((t,e)=>{B(t,e);let r=-1,s=[],n=1;const{bitsPerRow:l}=e.getConfig();for(let{start:a,end:i,bits:d,label:c}of t.blocks){if(a!==void 0&&i!==void 0&&i<a)throw new Error(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)throw new Error(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)throw new Error(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,m.debug(`Packet block ${a} - ${r} with label ${c}`);s.length<=l+1&&e.getPacket().length<L;){const[p,o]=Y({start:a,end:i,bits:d,label:c},n,l);if(s.push(p),p.end+1===n*l&&(e.pushWord(s),s=[],n++),!o)break;({start:a,end:i,bits:d,label:c}=o)}}e.pushWord(s)},"populate"),Y=b((t,e,r)=>{if(t.start===void 0)throw new Error("start should have been set during first phase");if(t.end===void 0)throw new Error("end should have been set during first phase");if(t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);if(t.end+1<=e*r)return[t,void 0];const s=e*r-1,n=e*r;return[{start:t.start,end:s,label:t.label,bits:s-t.start},{start:n,end:t.end,label:t.label,bits:t.end-n}]},"getNextFittingBlock"),v={parser:{yy:void 0},parse:b(async t=>{const e=await _("packet",t),r=v.parser?.yy;if(!(r instanceof w))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");m.debug(e),M(e,r)},"parse")},I=b((t,e,r,s)=>{const n=s.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),o=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(o?0:a),k=d*c+2,f=$(e);f.attr("viewBox",`0 0 ${k} ${g}`),C(f,g,k,l.useMaxWidth);for(const[x,y]of p.entries())O(f,y,x,l);f.append("text").text(o).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((t,e,r,{rowHeight:s,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{const c=t.append("g"),p=r*(s+l)+l;for(const o of e){const h=o.start%i*a+1,g=(o.end-o.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(o.label),!d)continue;const k=o.end===o.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(o.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(o.end)}},"drawWord"),j={draw:I},q={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},G=b(({packet:t}={})=>{const e=u(q,t);return`
.packetByte {
font-size: ${e.byteFontSize};
}
.packetByte.start {
fill: ${e.startByteColor};
}
.packetByte.end {
fill: ${e.endByteColor};
}
.packetLabel {
fill: ${e.labelColor};
font-size: ${e.labelFontSize};
}
.packetTitle {
fill: ${e.titleColor};
font-size: ${e.titleFontSize};
}
.packetBlock {
stroke: ${e.blockStrokeColor};
stroke-width: ${e.blockStrokeWidth};
fill: ${e.blockFillColor};
}
`},"styles"),X={parser:v,get db(){return new w},renderer:j,styles:G};export{X as diagram};

View file

@ -1,7 +1,7 @@
import{I as X}from"./chunk-2Q5K7J3B-CQzCXLEo.js";import{p as O}from"./chunk-JWPE2WC7-Bgwiccxa.js";import{o as G,b as Y,s as F,p as P,g as j,a as q,_ as f,B as A,l as D,F as Z,e as U,z as N,q as J,i as K,ai as Q,D as ee,aj as te}from"./mermaid.core-DY8EMjrF.js";import{p as ne}from"./cynefin-VYW2F7L2-rK0CO-KU.js";import"./index-8y9QBU_r.js";var E=/[─━│┃└┗├┣]/,S=/[└┗├┣]/,re=/[─━]/,V=/^[\s│┃]+$/,$=/^\s*(title[\t ]|accTitle[\t ]*:|accDescr[\t ]*[:{])/,k=/^\s*%%/,ie=" ";function L(n){return n.some(e=>E.test(e))}f(L,"isBoxDrawingFormat");function _(n){for(const e of n){const t=S.exec(e);if(t?.index&&t.index>0)return t.index}return 4}f(_,"inferSegmentWidth");function M(n,e){return n.replace(/\bline\s+(\d+)\b/gi,(t,r)=>{const i=parseInt(r,10),a=e.get(i);return a?`line ${a}`:t})}f(M,"remapErrorLines");function R(n){const e=n.split(`
import{I as X}from"./chunk-2Q5K7J3B-CdhiweKX.js";import{p as O}from"./chunk-JWPE2WC7-C7S97jLg.js";import{n as G,b as Y,s as P,o as j,g as F,a as Z,_ as f,A,l as D,D as q,e as U,y as N,p as J,i as K,ai as Q,B as ee,aj as te}from"./mermaid.core-CqiFQExc.js";import{p as ne}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var E=/[─━│┃└┗├┣]/,S=/[└┗├┣]/,re=/[─━]/,V=/^[\s│┃]+$/,$=/^\s*(title[\t ]|accTitle[\t ]*:|accDescr[\t ]*[:{])/,k=/^\s*%%/,ie=" ";function L(n){return n.some(e=>E.test(e))}f(L,"isBoxDrawingFormat");function _(n){for(const e of n){const t=S.exec(e);if(t?.index&&t.index>0)return t.index}return 4}f(_,"inferSegmentWidth");function M(n,e){return n.replace(/\bline\s+(\d+)\b/gi,(t,r)=>{const i=parseInt(r,10),a=e.get(i);return a?`line ${a}`:t})}f(M,"remapErrorLines");function R(n){const e=n.split(`
`),t=new Map;let r=-1;for(const[s,o]of e.entries())if(o.trim()==="treeView-beta"){r=s;break}if(r===-1)return{text:n,lineMap:t};const i=[];for(let s=r+1;s<e.length;s++){const o=e[s];o.trim()===""||k.test(o)||$.test(o)||V.test(o)||i.push(o.replace(/\t/g," "))}if(!L(i))return{text:n,lineMap:t};const a=_(i),c=[];let l=0;for(let s=0;s<=r;s++)c.push(e[s]),l++,t.set(l,s+1);for(let s=r+1;s<e.length;s++){const o=e[s],h=o.trim(),p=s+1;if(h===""){c.push(o),l++,t.set(l,p);continue}if(k.test(o)){c.push(o),l++,t.set(l,p);continue}if($.test(o)){c.push(o),l++,t.set(l,p);continue}if(V.test(o))continue;const d=o.replace(/\t/g," "),w=S.exec(d);if(w?.index!==void 0){const g=w.index,m=Math.round(g/a)+1;let u=g+1;for(;u<d.length&&re.test(d[u]);)u++;for(;u<d.length&&d[u]===" ";)u++;const v=d.slice(u).trimEnd();if(!v)throw new Error(`Line ${p}: Empty node — expected a filename or directory name after the box-drawing prefix`);const W=ie.repeat(m);c.push(W+v),l++,t.set(l,p)}else{if(/^[\s─━│┃└┗├┣]+$/.test(d))continue;if(E.test(d))c.push(o),l++,t.set(l,p);else{if(/^\s+/.test(d))throw new Error(`Line ${p}: Unexpected indentation without box-drawing characters. In box-drawing format, use ├── or └── prefixes for indented nodes.`);c.push(o),l++,t.set(l,p)}}}return{text:c.join(`
`),lineMap:t}}f(R,"preprocessBoxDrawing");var x=new X(()=>({cnt:1,stack:[{id:0,level:-1,name:"/",nodeType:"directory",children:[]}]})),oe=f(()=>{x.reset(),J()},"clear"),se=f(()=>x.records.stack[0],"getRoot"),ae=f(()=>x.records.cnt,"getCount"),ce=ee.treeView,le=f(()=>A(ce,N().treeView),"getConfig"),de=f((n,e,t,r,i,a)=>{for(;n<=x.records.stack[x.records.stack.length-1].level;)x.records.stack.pop();const c={id:x.records.cnt++,level:n,name:e,nodeType:t,icon:i,cssClass:r,description:a,children:[]};x.records.stack[x.records.stack.length-1].children.push(c),x.records.stack.push(c)},"addNode"),he={clear:oe,addNode:de,getRoot:se,getCount:ae,getConfig:le,getAccTitle:q,getAccDescription:j,getDiagramTitle:P,setAccDescription:F,setAccTitle:Y,setDiagramTitle:G},I=he,pe=f(n=>{O(n,I);for(const e of n.nodes){const t=typeof e.indent=="number"?e.indent:0;let r=e.name;const i=r.endsWith("/");i&&(r=r.slice(0,-1));const a=i?"directory":"file",c=e.classAnnotation||void 0,l=e.iconAnnotation,s=l!==void 0?l||"none":void 0,o=e.descAnnotation||void 0,h=o?K(o,N()):void 0;I.addNode(t,r,a,c,s,h)}},"populate"),fe={parse:f(async n=>{const{text:e,lineMap:t}=R(n);try{const r=await ne("treeView",e);D.debug(r),pe(r)}catch(r){throw t.size>0&&r instanceof Error&&(r.message=M(r.message,t)),r}},"parse")},b={prefix:"mermaid-treeview",height:24,width:24,icons:{folder:{body:'<path fill="currentColor" d="M10.59 4.59A2 2 0 0 0 9.17 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.17z"/>'},file:{body:'<path fill="currentColor" fill-rule="evenodd" d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.83a2 2 0 0 0-.59-1.42l-4.82-4.82A2 2 0 0 0 13.17 2H6Zm7.5 1.9l4.6 4.6h-3.6a1 1 0 0 1-1-1V3.9Z" clip-rule="evenodd"/>'}}};function z(n,e){const t=e?.filenameIcons?.[n];if(t)return t;const r=n.lastIndexOf(".");if(r>0){const i=n.substring(r).toLowerCase(),a=e?.extensionIcons;return a?.[i]??a?.[i.slice(1)]}}f(z,"detectIcon");function C(n,e){return n.includes(":")?n:n in b.icons||!e?`${b.prefix}:${n}`:`${e}:${n}`}f(C,"qualifyIcon");function B(n,e){if(n.icon!=="none"){if(n.icon)return C(n.icon,e.defaultIconPack);if(e.showIcons){if(n.nodeType==="file"){const t=z(n.name,e);if(t==="none")return;if(t)return C(t,e.defaultIconPack)}return`${b.prefix}:${n.nodeType==="directory"?"folder":"file"}`}}}f(B,"getNodeIcon");te([{name:b.prefix,icons:b}]);var y=14,ge=4,ue=16,H=f((n,e)=>`tv-icon-${n}-${e.replace(/[^\w-]/g,"-")}`,"iconSymbolId"),we=f(async(n,e,t,r)=>{const i=new Set,a=f(s=>{const o=B(s,t);o&&i.add(o),s.children.forEach(a)},"collect");if(a(e),i.size===0)return;const c=await Promise.all([...i].map(async s=>({icon:s,svg:await Q(s,{height:y,width:y})}))),l=n.append("defs");for(const{icon:s,svg:o}of c)l.append("g").attr("id",H(r,s)).html(o)},"injectIconDefs"),me=f((n,e,t,r,i,a)=>{const c=r.append("g");let l="treeView-node-label";t.nodeType==="directory"&&(l+=" treeView-node-dir"),t.cssClass&&(l+=` ${t.cssClass}`);const s=y+ge,o=B(t,i),h=o!==void 0;o&&c.append("use").attr("xlink:href",`#${H(a,o)}`).attr("x",n+i.paddingX).attr("y",e+i.paddingY).attr("class","treeView-node-icon");const p=c.append("text").text(t.name).attr("dominant-baseline","middle").attr("class",l),{height:d,width:w}=p.node().getBBox(),g=d+i.paddingY*2,m=n+i.paddingX+(h?s:0);p.attr("x",m),p.attr("y",e+g/2);const u=m+w,v=w+i.paddingX*2+(h?s:0);return t.BBox={x:n,y:e,width:v,height:g},t.cssClass?.split(/\s+/).includes("highlight")&&c.insert("rect",":first-child").attr("x",n).attr("y",e+1).attr("width",0).attr("height",g-2).attr("rx",3).attr("class","treeView-highlight-bg"),{node:t,nodeGroup:c,labelRightEdge:u,centerY:e+g/2}},"positionLabel"),T=f((n,e,t,r,i,a)=>n.append("line").attr("x1",e).attr("y1",t).attr("x2",r).attr("y2",i).attr("stroke-width",a).attr("class","treeView-node-line"),"positionLine"),xe=f((n,e,t,r)=>{let i=0,a=0;const c=[],l=f((h,p,d,w)=>{const g=w*(d.rowIndent+d.paddingX),m=me(g,i,p,h,d,r);c.push(m);const{height:u,width:v}=p.BBox;T(h,g-d.rowIndent,i+u/2,g,i+u/2,d.lineThickness),a=Math.max(a,g+v),i+=u},"drawNode"),s=f((h,p=0)=>{l(n,h,t,p),h.children.forEach(m=>{s(m,p+1)});const{x:d,y:w,height:g}=h.BBox;if(h.children.length){const{y:m,height:u}=h.children[h.children.length-1].BBox;T(n,d+t.paddingX,w+g,d+t.paddingX,m+u/2+t.lineThickness/2,t.lineThickness)}},"processNode");s(e);const o=c.filter(h=>h.node.description);if(o.length>0){const p=Math.max(...c.map(d=>d.labelRightEdge))+ue;for(const d of o){const g=d.nodeGroup.append("text").text(d.node.description).attr("dominant-baseline","middle").attr("class","treeView-node-description").attr("x",p).attr("y",d.centerY).node().getBBox();a=Math.max(a,p+g.width+t.paddingX)}}for(const h of c)if(h.node.cssClass?.split(/\s+/).includes("highlight")){const p=h.nodeGroup.select(".treeView-highlight-bg");if(!p.empty()){const d=a-h.node.BBox.x+8;p.attr("width",d),a=Math.max(a,h.node.BBox.x+d+2)}}return{totalHeight:i,totalWidth:a}},"drawTree"),ve=f(async(n,e,t,r)=>{D.debug(`Rendering treeView diagram
`+n);const i=r.db,a=i.getRoot(),c=i.getConfig(),l=Z(e);await we(l,a,c,e);const s=l.append("g");s.attr("class","tree-view");const{totalHeight:o,totalWidth:h}=xe(s,a,c,e);l.attr("viewBox",`-${c.lineThickness/2} 0 ${h} ${o}`),U(l,o,h,c.useMaxWidth)},"draw"),be={draw:ve},Ie=be,Ce={labelFontSize:"16px",labelColor:"black",lineColor:"black",iconColor:"#546e7a",descriptionColor:"#6a9955",highlightBg:"rgba(255, 193, 7, 0.15)",highlightStroke:"#ffc107"},ye=f(({treeView:n})=>{const{labelFontSize:e,labelColor:t,lineColor:r,iconColor:i,descriptionColor:a,highlightBg:c,highlightStroke:l}=A(Ce,n);return`
`),lineMap:t}}f(R,"preprocessBoxDrawing");var x=new X(()=>({cnt:1,stack:[{id:0,level:-1,name:"/",nodeType:"directory",children:[]}]})),oe=f(()=>{x.reset(),J()},"clear"),se=f(()=>x.records.stack[0],"getRoot"),ae=f(()=>x.records.cnt,"getCount"),ce=ee.treeView,le=f(()=>A(ce,N().treeView),"getConfig"),de=f((n,e,t,r,i,a)=>{for(;n<=x.records.stack[x.records.stack.length-1].level;)x.records.stack.pop();const c={id:x.records.cnt++,level:n,name:e,nodeType:t,icon:i,cssClass:r,description:a,children:[]};x.records.stack[x.records.stack.length-1].children.push(c),x.records.stack.push(c)},"addNode"),he={clear:oe,addNode:de,getRoot:se,getCount:ae,getConfig:le,getAccTitle:Z,getAccDescription:F,getDiagramTitle:j,setAccDescription:P,setAccTitle:Y,setDiagramTitle:G},I=he,pe=f(n=>{O(n,I);for(const e of n.nodes){const t=typeof e.indent=="number"?e.indent:0;let r=e.name;const i=r.endsWith("/");i&&(r=r.slice(0,-1));const a=i?"directory":"file",c=e.classAnnotation||void 0,l=e.iconAnnotation,s=l!==void 0?l||"none":void 0,o=e.descAnnotation||void 0,h=o?K(o,N()):void 0;I.addNode(t,r,a,c,s,h)}},"populate"),fe={parse:f(async n=>{const{text:e,lineMap:t}=R(n);try{const r=await ne("treeView",e);D.debug(r),pe(r)}catch(r){throw t.size>0&&r instanceof Error&&(r.message=M(r.message,t)),r}},"parse")},b={prefix:"mermaid-treeview",height:24,width:24,icons:{folder:{body:'<path fill="currentColor" d="M10.59 4.59A2 2 0 0 0 9.17 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.17z"/>'},file:{body:'<path fill="currentColor" fill-rule="evenodd" d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8.83a2 2 0 0 0-.59-1.42l-4.82-4.82A2 2 0 0 0 13.17 2H6Zm7.5 1.9l4.6 4.6h-3.6a1 1 0 0 1-1-1V3.9Z" clip-rule="evenodd"/>'}}};function H(n,e){const t=e?.filenameIcons?.[n];if(t)return t;const r=n.lastIndexOf(".");if(r>0){const i=n.substring(r).toLowerCase(),a=e?.extensionIcons;return a?.[i]??a?.[i.slice(1)]}}f(H,"detectIcon");function C(n,e){return n.includes(":")?n:n in b.icons||!e?`${b.prefix}:${n}`:`${e}:${n}`}f(C,"qualifyIcon");function B(n,e){if(n.icon!=="none"){if(n.icon)return C(n.icon,e.defaultIconPack);if(e.showIcons){if(n.nodeType==="file"){const t=H(n.name,e);if(t==="none")return;if(t)return C(t,e.defaultIconPack)}return`${b.prefix}:${n.nodeType==="directory"?"folder":"file"}`}}}f(B,"getNodeIcon");te([{name:b.prefix,icons:b}]);var y=14,ge=4,ue=16,z=f((n,e)=>`tv-icon-${n}-${e.replace(/[^\w-]/g,"-")}`,"iconSymbolId"),we=f(async(n,e,t,r)=>{const i=new Set,a=f(s=>{const o=B(s,t);o&&i.add(o),s.children.forEach(a)},"collect");if(a(e),i.size===0)return;const c=await Promise.all([...i].map(async s=>({icon:s,svg:await Q(s,{height:y,width:y})}))),l=n.append("defs");for(const{icon:s,svg:o}of c)l.append("g").attr("id",z(r,s)).html(o)},"injectIconDefs"),me=f((n,e,t,r,i,a)=>{const c=r.append("g");let l="treeView-node-label";t.nodeType==="directory"&&(l+=" treeView-node-dir"),t.cssClass&&(l+=` ${t.cssClass}`);const s=y+ge,o=B(t,i),h=o!==void 0;o&&c.append("use").attr("xlink:href",`#${z(a,o)}`).attr("x",n+i.paddingX).attr("y",e+i.paddingY).attr("class","treeView-node-icon");const p=c.append("text").text(t.name).attr("dominant-baseline","middle").attr("class",l),{height:d,width:w}=p.node().getBBox(),g=d+i.paddingY*2,m=n+i.paddingX+(h?s:0);p.attr("x",m),p.attr("y",e+g/2);const u=m+w,v=w+i.paddingX*2+(h?s:0);return t.BBox={x:n,y:e,width:v,height:g},t.cssClass?.split(/\s+/).includes("highlight")&&c.insert("rect",":first-child").attr("x",n).attr("y",e+1).attr("width",0).attr("height",g-2).attr("rx",3).attr("class","treeView-highlight-bg"),{node:t,nodeGroup:c,labelRightEdge:u,centerY:e+g/2}},"positionLabel"),T=f((n,e,t,r,i,a)=>n.append("line").attr("x1",e).attr("y1",t).attr("x2",r).attr("y2",i).attr("stroke-width",a).attr("class","treeView-node-line"),"positionLine"),xe=f((n,e,t,r)=>{let i=0,a=0;const c=[],l=f((h,p,d,w)=>{const g=w*(d.rowIndent+d.paddingX),m=me(g,i,p,h,d,r);c.push(m);const{height:u,width:v}=p.BBox;T(h,g-d.rowIndent,i+u/2,g,i+u/2,d.lineThickness),a=Math.max(a,g+v),i+=u},"drawNode"),s=f((h,p=0)=>{l(n,h,t,p),h.children.forEach(m=>{s(m,p+1)});const{x:d,y:w,height:g}=h.BBox;if(h.children.length){const{y:m,height:u}=h.children[h.children.length-1].BBox;T(n,d+t.paddingX,w+g,d+t.paddingX,m+u/2+t.lineThickness/2,t.lineThickness)}},"processNode");s(e);const o=c.filter(h=>h.node.description);if(o.length>0){const p=Math.max(...c.map(d=>d.labelRightEdge))+ue;for(const d of o){const g=d.nodeGroup.append("text").text(d.node.description).attr("dominant-baseline","middle").attr("class","treeView-node-description").attr("x",p).attr("y",d.centerY).node().getBBox();a=Math.max(a,p+g.width+t.paddingX)}}for(const h of c)if(h.node.cssClass?.split(/\s+/).includes("highlight")){const p=h.nodeGroup.select(".treeView-highlight-bg");if(!p.empty()){const d=a-h.node.BBox.x+8;p.attr("width",d),a=Math.max(a,h.node.BBox.x+d+2)}}return{totalHeight:i,totalWidth:a}},"drawTree"),ve=f(async(n,e,t,r)=>{D.debug(`Rendering treeView diagram
`+n);const i=r.db,a=i.getRoot(),c=i.getConfig(),l=q(e);await we(l,a,c,e);const s=l.append("g");s.attr("class","tree-view");const{totalHeight:o,totalWidth:h}=xe(s,a,c,e);l.attr("viewBox",`-${c.lineThickness/2} 0 ${h} ${o}`),U(l,o,h,c.useMaxWidth)},"draw"),be={draw:ve},Ie=be,Ce={labelFontSize:"16px",labelColor:"black",lineColor:"black",iconColor:"#546e7a",descriptionColor:"#6a9955",highlightBg:"rgba(255, 193, 7, 0.15)",highlightStroke:"#ffc107"},ye=f(({treeView:n})=>{const{labelFontSize:e,labelColor:t,lineColor:r,iconColor:i,descriptionColor:a,highlightBg:c,highlightStroke:l}=A(Ce,n);return`
.treeView-node-label {
font-size: ${e};
fill: ${t};
@ -27,4 +27,4 @@ import{I as X}from"./chunk-2Q5K7J3B-CQzCXLEo.js";import{p as O}from"./chunk-JWPE
stroke: ${l};
stroke-width: 1;
}
`},"styles"),Be=ye,De={db:I,renderer:Ie,parser:fe,styles:Be};export{De as diagram};
`},"styles"),Be=ye,Ne={db:I,renderer:Ie,parser:fe,styles:Be};export{Ne as diagram};

View file

@ -1,41 +0,0 @@
import{p as k}from"./chunk-JWPE2WC7-Bgwiccxa.js";import{s as R,g as F,p as I,o as _,a as D,b as E,_ as c,F as z,q as P,B as y,z as C,D as G,l as B,W,e as V}from"./mermaid.core-DY8EMjrF.js";import{p as H}from"./cynefin-VYW2F7L2-rK0CO-KU.js";import"./index-8y9QBU_r.js";var m={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:m},x=structuredClone(w),j=G.radar,q=c(()=>y({...j,...C().radar}),"getConfig"),b=c(()=>x.axes,"getAxes"),N=c(()=>x.curves,"getCurves"),U=c(()=>x.options,"getOptions"),X=c(a=>{x.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Y=c(a=>{x.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:Z(t.entries)}))},"setCurves"),Z=c(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>s.axis?.$refText===e.name);if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),J=c(a=>{const t=a.reduce((e,r)=>(e[r.name]=r,e),{});x.options={showLegend:t.showLegend?.value??m.showLegend,ticks:t.ticks?.value??m.ticks,max:t.max?.value??m.max,min:t.min?.value??m.min,graticule:t.graticule?.value??m.graticule}},"setOptions"),K=c(()=>{P(),x=structuredClone(w)},"clear"),$={getAxes:b,getCurves:N,getOptions:U,setAxes:X,setCurves:Y,setOptions:J,getConfig:q,clear:K,setAccTitle:E,getAccTitle:D,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Q=c(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),tt={parse:c(async a=>{const t=await H("radar",a);B.debug(t),Q(t)},"parse")},et=c((a,t,e,r)=>{const s=r.db,i=s.getAxes(),l=s.getCurves(),n=s.getOptions(),o=s.getConfig(),d=s.getDiagramTitle(),p=z(t),u=at(p,o),g=n.max??Math.max(...l.map(f=>Math.max(...f.entries))),h=n.min,v=Math.min(o.width,o.height)/2;rt(u,i,v,n.ticks,n.graticule),st(u,i,v,o),A(u,i,l,h,g,n.graticule,o),T(u,l,n.showLegend,o),u.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-o.height/2-o.marginTop)},"draw"),at=c((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return V(a,r,e,t.useMaxWidth??!0),a.attr("viewBox",`0 0 ${e} ${r}`).attr("overflow","visible"),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),rt=c((a,t,e,r,s)=>{if(s==="circle")for(let i=0;i<r;i++){const l=e*(i+1)/r;a.append("circle").attr("r",l).attr("class","radarGraticule")}else if(s==="polygon"){const i=t.length;for(let l=0;l<r;l++){const n=e*(l+1)/r,o=t.map((d,p)=>{const u=2*p*Math.PI/i-Math.PI/2,g=n*Math.cos(u),h=n*Math.sin(u);return`${g},${h}`}).join(" ");a.append("polygon").attr("points",o).attr("class","radarGraticule")}}},"drawGraticule"),st=c((a,t,e,r)=>{const s=t.length;for(let i=0;i<s;i++){const l=t[i].label,n=2*i*Math.PI/s-Math.PI/2,o=Math.cos(n),d=Math.sin(n);a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*o).attr("y2",e*r.axisScaleFactor*d).attr("class","radarAxisLine");const p=o>.01?"start":o<-.01?"end":"middle",u=d>.01?"hanging":d<-.01?"auto":"central",g=4;a.append("text").text(l).attr("x",e*r.axisLabelFactor*o+g*o).attr("y",e*r.axisLabelFactor*d+g*d).attr("text-anchor",p).attr("dominant-baseline",u).attr("class","radarAxisLabel")}},"drawAxes");function A(a,t,e,r,s,i,l){const n=t.length,o=Math.min(l.width,l.height)/2;e.forEach((d,p)=>{if(d.entries.length!==n)return;const u=d.entries.map((g,h)=>{const v=2*Math.PI*h/n-Math.PI/2,f=M(g,r,s,o),S=f*Math.cos(v),O=f*Math.sin(v);return{x:S,y:O}});i==="circle"?a.append("path").attr("d",L(u,l.curveTension)).attr("class",`radarCurve-${p}`):i==="polygon"&&a.append("polygon").attr("points",u.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${p}`)})}c(A,"drawCurves");function M(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}c(M,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const i=a[(s-1+e)%e],l=a[s],n=a[(s+1)%e],o=a[(s+2)%e],d={x:l.x+(n.x-i.x)*t,y:l.y+(n.y-i.y)*t},p={x:n.x-(o.x-l.x)*t,y:n.y-(o.y-l.y)*t};r+=` C${d.x},${d.y} ${p.x},${p.y} ${n.x},${n.y}`}return`${r} Z`}c(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,i=-(r.height/2+r.marginTop)*3/4,l=20;t.forEach((n,o)=>{const d=a.append("g").attr("transform",`translate(${s}, ${i+o*l})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${o}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}c(T,"drawLegend");var nt={draw:et},ot=c((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
.radarCurve-${r} {
color: ${s};
fill: ${s};
fill-opacity: ${t.curveOpacity};
stroke: ${s};
stroke-width: ${t.curveStrokeWidth};
}
.radarLegendBox-${r} {
fill: ${s};
fill-opacity: ${t.curveOpacity};
stroke: ${s};
}
`}return e},"genIndexStyles"),it=c(a=>{const t=W(),e=C(),r=y(t,e.themeVariables),s=y(r.radar,a);return{themeVariables:r,radarOptions:s}},"buildRadarStyleOptions"),lt=c(({radar:a}={})=>{const{themeVariables:t,radarOptions:e}=it(a);return`
.radarTitle {
font-size: ${t.fontSize};
color: ${t.titleColor};
dominant-baseline: hanging;
text-anchor: middle;
}
.radarAxisLine {
stroke: ${e.axisColor};
stroke-width: ${e.axisStrokeWidth};
}
.radarAxisLabel {
font-size: ${e.axisLabelFontSize}px;
color: ${e.axisColor};
}
.radarGraticule {
fill: ${e.graticuleColor};
fill-opacity: ${e.graticuleOpacity};
stroke: ${e.graticuleColor};
stroke-width: ${e.graticuleStrokeWidth};
}
.radarLegendText {
text-anchor: start;
font-size: ${e.legendFontSize}px;
dominant-baseline: hanging;
}
${ot(t,e)}
`},"styles"),gt={parser:tt,db:$,renderer:nt,styles:lt};export{gt as diagram};

View file

@ -0,0 +1,41 @@
import{p as k}from"./chunk-JWPE2WC7-C7S97jLg.js";import{s as R,g as E,o as I,n as _,a as D,b as F,_ as c,D as P,p as z,A as y,y as C,B as G,l as B,E as W,e as V}from"./mermaid.core-CqiFQExc.js";import{p as H}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var m={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:m},x=structuredClone(w),j=G.radar,N=c(()=>y({...j,...C().radar}),"getConfig"),b=c(()=>x.axes,"getAxes"),U=c(()=>x.curves,"getCurves"),X=c(()=>x.options,"getOptions"),Y=c(a=>{x.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Z=c(a=>{x.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:q(t.entries)}))},"setCurves"),q=c(a=>{if(a[0].axis==null)return a.map(e=>e.value);const t=b();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(e=>{const r=a.find(s=>s.axis?.$refText===e.name);if(r===void 0)throw new Error("Missing entry for axis "+e.label);return r.value})},"computeCurveEntries"),J=c(a=>{const t=a.reduce((e,r)=>(e[r.name]=r,e),{});x.options={showLegend:t.showLegend?.value??m.showLegend,ticks:t.ticks?.value??m.ticks,max:t.max?.value??m.max,min:t.min?.value??m.min,graticule:t.graticule?.value??m.graticule}},"setOptions"),K=c(()=>{z(),x=structuredClone(w)},"clear"),$={getAxes:b,getCurves:U,getOptions:X,setAxes:Y,setCurves:Z,setOptions:J,getConfig:N,clear:K,setAccTitle:F,getAccTitle:D,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:E,setAccDescription:R},Q=c(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),tt={parse:c(async a=>{const t=await H("radar",a);B.debug(t),Q(t)},"parse")},et=c((a,t,e,r)=>{const s=r.db,i=s.getAxes(),l=s.getCurves(),n=s.getOptions(),o=s.getConfig(),d=s.getDiagramTitle(),p=P(t),u=at(p,o),g=n.max??Math.max(...l.map(f=>Math.max(...f.entries))),h=n.min,v=Math.min(o.width,o.height)/2;rt(u,i,v,n.ticks,n.graticule),st(u,i,v,o),A(u,i,l,h,g,n.graticule,o),T(u,l,n.showLegend,o),u.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-o.height/2-o.marginTop)},"draw"),at=c((a,t)=>{const e=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return V(a,r,e,t.useMaxWidth??!0),a.attr("viewBox",`0 0 ${e} ${r}`).attr("overflow","visible"),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),rt=c((a,t,e,r,s)=>{if(s==="circle")for(let i=0;i<r;i++){const l=e*(i+1)/r;a.append("circle").attr("r",l).attr("class","radarGraticule")}else if(s==="polygon"){const i=t.length;for(let l=0;l<r;l++){const n=e*(l+1)/r,o=t.map((d,p)=>{const u=2*p*Math.PI/i-Math.PI/2,g=n*Math.cos(u),h=n*Math.sin(u);return`${g},${h}`}).join(" ");a.append("polygon").attr("points",o).attr("class","radarGraticule")}}},"drawGraticule"),st=c((a,t,e,r)=>{const s=t.length;for(let i=0;i<s;i++){const l=t[i].label,n=2*i*Math.PI/s-Math.PI/2,o=Math.cos(n),d=Math.sin(n);a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*o).attr("y2",e*r.axisScaleFactor*d).attr("class","radarAxisLine");const p=o>.01?"start":o<-.01?"end":"middle",u=d>.01?"hanging":d<-.01?"auto":"central",g=4;a.append("text").text(l).attr("x",e*r.axisLabelFactor*o+g*o).attr("y",e*r.axisLabelFactor*d+g*d).attr("text-anchor",p).attr("dominant-baseline",u).attr("class","radarAxisLabel")}},"drawAxes");function A(a,t,e,r,s,i,l){const n=t.length,o=Math.min(l.width,l.height)/2;e.forEach((d,p)=>{if(d.entries.length!==n)return;const u=d.entries.map((g,h)=>{const v=2*Math.PI*h/n-Math.PI/2,f=M(g,r,s,o),S=f*Math.cos(v),O=f*Math.sin(v);return{x:S,y:O}});i==="circle"?a.append("path").attr("d",L(u,l.curveTension)).attr("class",`radarCurve-${p}`):i==="polygon"&&a.append("polygon").attr("points",u.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${p}`)})}c(A,"drawCurves");function M(a,t,e,r){const s=Math.min(Math.max(a,t),e);return r*(s-t)/(e-t)}c(M,"relativeRadius");function L(a,t){const e=a.length;let r=`M${a[0].x},${a[0].y}`;for(let s=0;s<e;s++){const i=a[(s-1+e)%e],l=a[s],n=a[(s+1)%e],o=a[(s+2)%e],d={x:l.x+(n.x-i.x)*t,y:l.y+(n.y-i.y)*t},p={x:n.x-(o.x-l.x)*t,y:n.y-(o.y-l.y)*t};r+=` C${d.x},${d.y} ${p.x},${p.y} ${n.x},${n.y}`}return`${r} Z`}c(L,"closedRoundCurve");function T(a,t,e,r){if(!e)return;const s=(r.width/2+r.marginRight)*3/4,i=-(r.height/2+r.marginTop)*3/4,l=20;t.forEach((n,o)=>{const d=a.append("g").attr("transform",`translate(${s}, ${i+o*l})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${o}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}c(T,"drawLegend");var nt={draw:et},ot=c((a,t)=>{let e="";for(let r=0;r<a.THEME_COLOR_LIMIT;r++){const s=a[`cScale${r}`];e+=`
.radarCurve-${r} {
color: ${s};
fill: ${s};
fill-opacity: ${t.curveOpacity};
stroke: ${s};
stroke-width: ${t.curveStrokeWidth};
}
.radarLegendBox-${r} {
fill: ${s};
fill-opacity: ${t.curveOpacity};
stroke: ${s};
}
`}return e},"genIndexStyles"),it=c(a=>{const t=W(),e=C(),r=y(t,e.themeVariables),s=y(r.radar,a);return{themeVariables:r,radarOptions:s}},"buildRadarStyleOptions"),lt=c(({radar:a}={})=>{const{themeVariables:t,radarOptions:e}=it(a);return`
.radarTitle {
font-size: ${t.fontSize};
color: ${t.titleColor};
dominant-baseline: hanging;
text-anchor: middle;
}
.radarAxisLine {
stroke: ${e.axisColor};
stroke-width: ${e.axisStrokeWidth};
}
.radarAxisLabel {
font-size: ${e.axisLabelFontSize}px;
color: ${e.axisColor};
}
.radarGraticule {
fill: ${e.graticuleColor};
fill-opacity: ${e.graticuleOpacity};
stroke: ${e.graticuleColor};
stroke-width: ${e.graticuleStrokeWidth};
}
.radarLegendText {
text-anchor: start;
font-size: ${e.legendFontSize}px;
dominant-baseline: hanging;
}
${ot(t,e)}
`},"styles"),xt={parser:tt,db:$,renderer:nt,styles:lt};export{xt as diagram};

View file

@ -1 +0,0 @@
import{g as l,r as m,d as n}from"./chunk-MOJQB5TN-CClISXJW.js";import{p}from"./chunk-JWPE2WC7-Bgwiccxa.js";import{_ as t,l as o}from"./mermaid.core-DY8EMjrF.js";import{M as u,a as f}from"./cynefin-VYW2F7L2-rK0CO-KU.js";import"./index-8y9QBU_r.js";var c=f().RailroadEbnf.parser.LangiumParser,s=t(e=>{const r=e.alternatives.map(E);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformChoice"),E=t(e=>{const r=e.elements.map(d);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformSequence"),i=t(e=>{switch(e.$type){case"EbnfTerminal":return{type:"terminal",value:e.value};case"EbnfNonTerminal":return{type:"nonterminal",name:e.name};case"EbnfSpecial":return{type:"special",text:e.text};case"EbnfGroup":return s(e.element);case"EbnfOptional":return{type:"optional",element:s(e.element)};case"EbnfRepetition":return{type:"repetition",element:s(e.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${e.$type}`)}},"transformPrimary"),b=t((e,r)=>{switch(r.$type){case"EbnfOptionalPostfix":return{type:"optional",element:e};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:e,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:e,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[e,{type:"terminal",value:"-"},i(r.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${r.$type}`)}},"transformPostfix"),d=t(e=>e.postfixes.reduce((r,a)=>b(r,a),i(e.base)),"transformTerm"),y=t(e=>({name:e.name,definition:s(e.definition)}),"transformRule"),v=t(e=>{p(e,n),e.title&&n.setTitle(e.title),e.rules.map(r=>n.addRule(y(r)))},"populateDb"),g={parse:t(e=>{n.clear(),o.debug("[EBNF Parser] Starting Langium parse");const r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new u(r);const a=r.value;o.debug("[EBNF Parser] Parsed rules:",a.rules.length),v(a),o.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:n}},R={parser:g,db:n,renderer:m,styles:l};export{R as diagram};

View file

@ -0,0 +1 @@
import{g as l,r as m,d as n}from"./chunk-MOJQB5TN-DcgqWRh_.js";import{p}from"./chunk-JWPE2WC7-C7S97jLg.js";import{_ as t,l as o}from"./mermaid.core-CqiFQExc.js";import{M as u,a as f}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";var c=f().RailroadEbnf.parser.LangiumParser,s=t(e=>{const r=e.alternatives.map(E);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformChoice"),E=t(e=>{const r=e.elements.map(d);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformSequence"),i=t(e=>{switch(e.$type){case"EbnfTerminal":return{type:"terminal",value:e.value};case"EbnfNonTerminal":return{type:"nonterminal",name:e.name};case"EbnfSpecial":return{type:"special",text:e.text};case"EbnfGroup":return s(e.element);case"EbnfOptional":return{type:"optional",element:s(e.element)};case"EbnfRepetition":return{type:"repetition",element:s(e.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${e.$type}`)}},"transformPrimary"),b=t((e,r)=>{switch(r.$type){case"EbnfOptionalPostfix":return{type:"optional",element:e};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:e,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:e,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[e,{type:"terminal",value:"-"},i(r.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${r.$type}`)}},"transformPostfix"),d=t(e=>e.postfixes.reduce((r,a)=>b(r,a),i(e.base)),"transformTerm"),y=t(e=>({name:e.name,definition:s(e.definition)}),"transformRule"),v=t(e=>{p(e,n),e.title&&n.setTitle(e.title),e.rules.map(r=>n.addRule(y(r)))},"populateDb"),g={parse:t(e=>{n.clear(),o.debug("[EBNF Parser] Starting Langium parse");const r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new u(r);const a=r.value;o.debug("[EBNF Parser] Parsed rules:",a.rules.length),v(a),o.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:n}},S={parser:g,db:n,renderer:m,styles:l};export{S as diagram};

Some files were not shown because too many files have changed in this diff Show more