- ClusterDb: add query() (per-shard fan-out with skip=0 and limit=skip+limit, global re-sort, then skip/limit) and compound index management (create/drop/list through the cluster registry, fanned out to every shard and caught up on shard open) - MiniDbQueryStore: replace the single MiniDb with a 16-shard ClusterDb, so multiple kimi processes share the read model instead of failing with storage.locked; per-shard LockError now propagates as a transient error rather than permanently disabling the read model - corruption: lift openOrRebuild's predicate (SyntaxError / CorruptFrameError) to one process-lifetime wipe-and-reopen rebuild - lower lockAcquireTimeoutMs to 1s for the cache read model - tests: cluster query merge and compound fan-out; store coexistence with a peer instance, corrupt-registry rebuild, 16-shard topology; sessionIndex locked-fallback test now drives a stub IQueryStore
8.3 KiB
errors
Error infrastructure for agent-core-v2: base classes, the per-domain code contract, the public
ErrorCodesfacade, wire serialization, and the conventions domains follow when raising errors.
Base classes and serialization are centralized in _base/errors; error codes
are decentralized — each domain owns an errors.ts that contributes 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 theisError2guard andunwrapErrorCause.src/_base/errors/codes.ts: theErrorDomaincontract, theErrorCodetype (aliased to the protocol'sKimiErrorCode), the runtime registry (registerErrorDomain/errorInfo/isErrorCode), and the domain-independentCoreErrors(internal,not_implemented).src/_base/errors/serialize.ts:ErrorPayload,isCodedError,toErrorPayload,fromErrorPayload,makeErrorPayload. Reads retryability from the registry viaerrorInfo. The wire-facing names (KimiErrorPayload,toKimiErrorPayload) mirror the protocol contract and keep their names even though the in-process class isError2.src/_base/errors/errorMessage.ts:toErrorMessage(error, verbose?)for logs/CLI.src/_base/errors/unexpectedError.ts:onUnexpectedError/setUnexpectedErrorHandler/safelyCallListener.src/<domain>/errors.ts: each domain'sXxxErrorsdescriptor (codes + retryable list + per-code info overrides), self-registered on import.src/errors.ts: the facade — imports every domain'serrors.ts(triggering registration), builds the unifiedErrorCodesconst, and re-exports all error primitives. This is the import throw sites use.
Conventions (hard rules)
- Throw a coded error, not a bare string.
throw new Error2(ErrorCodes.X, …).throw new Error('x')only for unreachable guards;BugIndicatingErrorwhen the throw site indicates a caller bug (e.g. reading a service before itsready);NotImplementedError('feature')for stubs. - Define codes in the owning domain. A domain's codes live in
<domain>/errors.tsnext to its interfaces, exported as anXxxErrorsdescriptor — never in_base/errors. - One
codeper failure mode. Codes readdomain.reason(e.g.tool.unknown_tool). The set of valid code strings is fixed by the protocol (KimiErrorCode); adding a brand-new code means updating the protocol first. Renaming/removing a code is a major (breaks SDK clients). - Import from the facade. Throw sites and cross-domain consumers do
import { ErrorCodes, Error2 } from '#/errors'. A domain's ownerrors.tsreferences its own descriptor (LoopErrors.codes.X) and imports only from#/_base/errors(never from#/errors, to avoid cycles). - Translate foreign errors at the boundary. Provider/HTTP, fs, MCP errors are caught at the domain boundary and re-thrown as the domain's coded error.
_base/errorsnever imports a business domain. - Translation is idempotent. A translator (
toHostFsError,toStorageIoError, …) returns its input unchanged when it is already the domain's error type, so layered boundaries never double-wrap. The original error always goes tocause. detailsis structured and JSON-serializable;messageis a short human sentence. Paths, errnos, syscalls, scope/key, line numbers go intodetails; the message must stay readable without them.- Cancellation passes through untranslated. A translation boundary that can see a cancellation-class error (
UserCancellationErrorfrom_base/utils/abort) rethrows it as-is. fs/process translation never encounters cancellation, so those translators do not check for it — apply the rule only at boundaries that actually can. - Classify wrapped foreign errors via
unwrapErrorCause. Predicates that branch on raw shapes (errno, provider status) testunwrapErrorCause(error), since boundary-translated errors carry the raw error ascause. - Branch on
code, neverinstanceof, across the wire. Class identity does not survive serialization. In-process,instanceof Error2/isCodedErrorare fine.
Adding a domain error (recipe)
In <domain>/errors.ts:
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors';
export const ToolErrors = {
codes: {
UNKNOWN_TOOL: 'tool.unknown_tool',
EXECUTION_FAILED: 'tool.execution_failed',
},
retryable: ['tool.execution_failed'],
info: {
'tool.unknown_tool': {
title: 'Unknown tool',
retryable: false,
public: true,
action: 'Check the tool name passed by the model.',
},
},
} as const satisfies ErrorDomain;
registerErrorDomain(ToolErrors);
Then wire it into the facade in src/errors.ts: import ToolErrors, add
...ToolErrors.codes to the ErrorCodes spread, and re-export it. The
satisfies ErrorDomain guarantees every code value is a protocol-known
ErrorCode, and registerErrorDomain makes its metadata available to
serialization.
Domain tiers in practice
The os / persistence / wire domains show the standard shapes:
os.fs(HostFsError,os/interface/hostFsErrors.ts) — everyIHostFileSystembackend translates raw errnos at its boundary via the puretoHostFsError(err, { path, op }):ENOENT→os.fs.not_found,EISDIR→os.fs.is_directory,ENOTDIR→os.fs.not_directory,EEXIST→os.fs.already_exists,EACCES/EPERM→os.fs.permission_denied,ENOTEMPTY→os.fs.not_empty, everything elseos.fs.unknown.detailscarries{ path, op, errno?, syscall? }. Documented boolean semantics (e.g.createExclusivereturningfalseonEEXIST) stay booleans, not errors.os.process(HostProcessError,os/interface/hostProcess.ts) —os.process.spawn_failed(details{ command, args?, cwd?, errno? }) andos.process.kill_failed; both carry the raw error ascause. Kill keeps its deliberate tolerances:ESRCHis a silent no-op,EPERMdegrades tochild.kill().storage(StorageError,persistence/interface/storage.ts) —storage.not_found/decode_failed/corrupted/io_failed/locked. ENOENT keeps its established absence semantics (read → undefined,list → []) and is not an error; other I/O failures becomestorage.io_failed(retryable). Codec parse failures becomestorage.decode_failedwith{ scope, key, format }; append-log corruption isAppendLogCorruptedError(storage.corrupted).storage.lockedis reserved for a store exclusively held by another process — consumers (e.g.FileSessionIndex) catch it explicitly and fall back to their non-read-model path with a one-time warning; there is no silent no-op degradation. (The minidb query-store backend is a multi-processClusterDband no longer throws it: peers share the store, and per-shard lock contention surfaces as a transientLockErrorinstead.)wire(WireError,wire/errors.ts) —DuplicateOpError(wire.duplicate_op, a build-time bug),CycleError(wire.cycle, details carry the drain depth and a capped op-type sample), andwire.unknown_record: replay skips records whose Op type is absent fromOP_REGISTRY(compatibility), reports each skip throughonUnexpectedError, and returns{ unknownRecords }so the caller knows the restore was lossy.
Serialization & boundary translation
toErrorPayload(error): any coded error (incl. deserialized shapes) → its code +retryablefromerrorInfo; anything else →internal.fromErrorPayload(payload): rehydrates anError2for in-processinstanceof/isCodedErroruse at the SDK/RPC boundary.isCodedError(error): structural guard (checkscodeagainst the registry), so it works for bothError2instances and plain objects revived from a payload.- The registry is populated when the facade is imported (the package
index.tsre-exports it); tests that import a single domain get that domain's codes via its self-registration.errorInfofalls back to{ title: code, retryable, public: true }for any unregistered code.
References
packages/agent-core-v2/src/_base/errors/— contract, registry, base classes, serialization.packages/agent-core-v2/src/errors.ts— the aggregating facade.packages/protocol/src/events.ts— the canonicalKimiErrorCodewire union.