From b0e0b94ea464f6ac03b4e3c442f99d8504a8d581 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 21:59:31 +0800 Subject: [PATCH 01/42] fix: replace deprecated tsconfig baseUrl with explicit paths entries TypeScript 6 and tsgolint treat "baseUrl" as an error-level deprecation. Resolve #/* subpath imports through per-package paths mappings instead, which also makes the source-first workspace resolvable under Bun 1.4. --- apps/kimi-code/tsconfig.json | 3 +++ packages/agent-core-v2/tsconfig.json | 5 ++++- packages/i18n/tsconfig.json | 12 +++++++++++- packages/kaos/tsconfig.json | 14 ++++++++++++-- packages/klient/tsconfig.json | 17 +++++++++++++++-- packages/kosong/tsconfig.json | 19 +++++++++++++++---- packages/migration-legacy/tsconfig.json | 14 ++++++++++++-- packages/minidb/tsconfig.json | 14 +++++++++++--- packages/node-sdk/tsconfig.json | 4 ++++ packages/oauth/tsconfig.json | 15 +++++++++++++-- packages/pi-tui/tsconfig.json | 21 +++++++++++++++------ packages/protocol/tsconfig.json | 15 ++++++++++++--- packages/telemetry/tsconfig.json | 15 ++++++++++++--- packages/transcript/tsconfig.json | 16 +++++++++++++--- packages/tree-sitter-bash/tsconfig.json | 14 +++++++++++--- 15 files changed, 163 insertions(+), 35 deletions(-) diff --git a/apps/kimi-code/tsconfig.json b/apps/kimi-code/tsconfig.json index 592ed9d7750..792e31e5d75 100644 --- a/apps/kimi-code/tsconfig.json +++ b/apps/kimi-code/tsconfig.json @@ -5,6 +5,9 @@ "experimentalDecorators": true, "paths": { "@/*": ["./src/*"], + "#/tui/theme": ["./src/tui/theme/index.ts"], + "#/tui/commands": ["./src/tui/commands/index.ts"], + "#/cli/sub/web/*": ["./src/cli/sub/web/*.ts"], "#/*": ["./src/*"] } }, diff --git a/packages/agent-core-v2/tsconfig.json b/packages/agent-core-v2/tsconfig.json index 760fb974a5c..4a379554b2b 100644 --- a/packages/agent-core-v2/tsconfig.json +++ b/packages/agent-core-v2/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "experimentalDecorators": true + "experimentalDecorators": true, + "paths": { + "#/*": ["./src/*.ts"] + } }, "include": ["src", "test"] } diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json index 5eddb4b47a7..c560a30b0a0 100644 --- a/packages/i18n/tsconfig.json +++ b/packages/i18n/tsconfig.json @@ -1,4 +1,14 @@ { "extends": "../../tsconfig.json", - "include": ["src", "test"] + "include": [ + "src", + "test" + ], + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + } } \ No newline at end of file diff --git a/packages/kaos/tsconfig.json b/packages/kaos/tsconfig.json index ef502e89c86..c560a30b0a0 100644 --- a/packages/kaos/tsconfig.json +++ b/packages/kaos/tsconfig.json @@ -1,4 +1,14 @@ { "extends": "../../tsconfig.json", - "include": ["src", "test"] -} + "include": [ + "src", + "test" + ], + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + } +} \ No newline at end of file diff --git a/packages/klient/tsconfig.json b/packages/klient/tsconfig.json index dca1267d559..33f310c5652 100644 --- a/packages/klient/tsconfig.json +++ b/packages/klient/tsconfig.json @@ -1,4 +1,17 @@ { "extends": "../../tsconfig.json", - "include": ["src", "test", "scenarios", "scripts", "../agent-core-v2/src/env.d.ts"] -} + "include": [ + "src", + "test", + "scenarios", + "scripts", + "../agent-core-v2/src/env.d.ts" + ], + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + } +} \ No newline at end of file diff --git a/packages/kosong/tsconfig.json b/packages/kosong/tsconfig.json index fd6ddf1b946..7f2a00207e4 100644 --- a/packages/kosong/tsconfig.json +++ b/packages/kosong/tsconfig.json @@ -1,6 +1,17 @@ { "extends": "../../tsconfig.json", - "compilerOptions": {}, - "include": ["src", "test"], - "exclude": ["test/type-safety-negative.ts"] -} + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + }, + "include": [ + "src", + "test" + ], + "exclude": [ + "test/type-safety-negative.ts" + ] +} \ No newline at end of file diff --git a/packages/migration-legacy/tsconfig.json b/packages/migration-legacy/tsconfig.json index ef502e89c86..c560a30b0a0 100644 --- a/packages/migration-legacy/tsconfig.json +++ b/packages/migration-legacy/tsconfig.json @@ -1,4 +1,14 @@ { "extends": "../../tsconfig.json", - "include": ["src", "test"] -} + "include": [ + "src", + "test" + ], + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + } +} \ No newline at end of file diff --git a/packages/minidb/tsconfig.json b/packages/minidb/tsconfig.json index 8e90bc90a59..f337b7213f8 100644 --- a/packages/minidb/tsconfig.json +++ b/packages/minidb/tsconfig.json @@ -1,5 +1,13 @@ { "extends": "../../tsconfig.json", - "compilerOptions": {}, - "include": ["src"] -} + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/node-sdk/tsconfig.json b/packages/node-sdk/tsconfig.json index ffb5f67e8c1..f305fc41af2 100644 --- a/packages/node-sdk/tsconfig.json +++ b/packages/node-sdk/tsconfig.json @@ -4,6 +4,10 @@ "lib": ["ESNext"], "experimentalDecorators": true, "paths": { + "#/legacy": ["./src/legacy/index.ts"], + "#/legacy/*": ["./src/legacy/*.ts"], + "#/config-local": ["./src/config-local/index.ts"], + "#/config-local/*": ["./src/config-local/*.ts"], "#/*": ["./src/*"] } }, diff --git a/packages/oauth/tsconfig.json b/packages/oauth/tsconfig.json index b999c057517..67d817d3d7d 100644 --- a/packages/oauth/tsconfig.json +++ b/packages/oauth/tsconfig.json @@ -1,4 +1,15 @@ { "extends": "../../tsconfig.json", - "include": ["src", "test", "examples"] -} + "include": [ + "src", + "test", + "examples" + ], + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + } +} \ No newline at end of file diff --git a/packages/pi-tui/tsconfig.json b/packages/pi-tui/tsconfig.json index 5c3ca434f25..d1bb210d81b 100644 --- a/packages/pi-tui/tsconfig.json +++ b/packages/pi-tui/tsconfig.json @@ -1,7 +1,16 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "lib": ["ESNext"] - }, - "include": ["src"] -} + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": [ + "ESNext" + ], + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/protocol/tsconfig.json b/packages/protocol/tsconfig.json index 8218f8155a8..d1bb210d81b 100644 --- a/packages/protocol/tsconfig.json +++ b/packages/protocol/tsconfig.json @@ -1,7 +1,16 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": ["ESNext"] + "lib": [ + "ESNext" + ], + "paths": { + "#/*": [ + "./src/*.ts" + ] + } }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/telemetry/tsconfig.json b/packages/telemetry/tsconfig.json index 46f71c10fe9..a3a85321706 100644 --- a/packages/telemetry/tsconfig.json +++ b/packages/telemetry/tsconfig.json @@ -1,5 +1,14 @@ { "extends": "../../tsconfig.json", - "compilerOptions": {}, - "include": ["src", "test"] -} + "compilerOptions": { + "paths": { + "#/*": [ + "./src/*.ts" + ] + } + }, + "include": [ + "src", + "test" + ] +} \ No newline at end of file diff --git a/packages/transcript/tsconfig.json b/packages/transcript/tsconfig.json index 2abca3c5c85..eed078bfbc8 100644 --- a/packages/transcript/tsconfig.json +++ b/packages/transcript/tsconfig.json @@ -1,7 +1,17 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": ["ESNext"] + "lib": [ + "ESNext" + ], + "paths": { + "#/*": [ + "./src/*.ts" + ] + } }, - "include": ["src", "test"] -} + "include": [ + "src", + "test" + ] +} \ No newline at end of file diff --git a/packages/tree-sitter-bash/tsconfig.json b/packages/tree-sitter-bash/tsconfig.json index 760fb974a5c..106f554cbdc 100644 --- a/packages/tree-sitter-bash/tsconfig.json +++ b/packages/tree-sitter-bash/tsconfig.json @@ -1,7 +1,15 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "experimentalDecorators": true + "experimentalDecorators": true, + "paths": { + "#/*": [ + "./src/*.ts" + ] + } }, - "include": ["src", "test"] -} + "include": [ + "src", + "test" + ] +} \ No newline at end of file From 30da7cb20a42b8990b6779b2a6370b0f67f83b85 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 22:00:17 +0800 Subject: [PATCH 02/42] feat: run host terminal sessions on both Node and Bun runtimes Detect the runtime at spawn time: under Bun use the built-in Bun.Terminal PTY (decoding chunks to strings and taking the real subprocess exit code from the exited promise), elsewhere keep the existing node-pty path. The TerminalProcess contract is unchanged. --- .../node-local/hostTerminalService.ts | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts b/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts index 726f86e01fa..f228366c0ed 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts @@ -3,15 +3,93 @@ import type { IPty } from 'node-pty'; import { Service } from '#/_base/di/service'; import { LifecycleScope } from '#/app/scopes'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { Emitter } from '#/_base/event'; import { IHostTerminalService, type TerminalProcess, type TerminalSpawnOptions } from '#/os/interface/terminal'; +interface BunTerminal { + write(data: string | Uint8Array): void; + resize(cols: number, rows: number): void; + close(): void; +} + +interface BunSubprocessWithTerminal { + readonly terminal: BunTerminal; + readonly exited: Promise; + kill(): void; +} + +interface BunGlobalLike { + spawn( + command: readonly string[], + options: { + cwd?: string; + env?: Record; + terminal: { + name?: string; + cols?: number; + rows?: number; + data(terminal: BunTerminal, data: Uint8Array): void; + }; + }, + ): BunSubprocessWithTerminal; +} + +function currentBun(): BunGlobalLike | undefined { + return (globalThis as unknown as { Bun?: BunGlobalLike }).Bun; +} + export class HostTerminalService extends Service implements IHostTerminalService { declare readonly _serviceBrand: undefined; private readonly processes = new Set(); - async spawn(options: TerminalSpawnOptions): Promise { + spawn(options: TerminalSpawnOptions): Promise { + const bun = currentBun(); + if (bun !== undefined) return Promise.resolve(this.spawnBun(bun, options)); + return this.spawnNodePty(options); + } + + private spawnBun(bun: BunGlobalLike, options: TerminalSpawnOptions): TerminalProcess { + const dataEmitter = new Emitter('hostTerminal.data'); + const exitEmitter = new Emitter<{ exitCode: number | null }>('hostTerminal.exit'); + const decoder = new TextDecoder(); + const proc = bun.spawn([options.shell], { + cwd: options.cwd, + env: { ...process.env }, + terminal: { + name: 'xterm-256color', + cols: options.cols, + rows: options.rows, + data: (_terminal, data) => dataEmitter.fire(decoder.decode(data)), + }, + }); + void proc.exited.then((exitCode) => { + exitEmitter.fire({ exitCode }); + dataEmitter.dispose(); + exitEmitter.dispose(); + return undefined; + }); + const terminalProcess: TerminalProcess = { + onProcessData: dataEmitter.event, + onProcessExit: exitEmitter.event, + write: (data) => proc.terminal.write(data), + resize: (cols, rows) => proc.terminal.resize(cols, rows), + kill: () => { + try { + proc.kill(); + } catch { + proc.terminal.close(); + return; + } + proc.terminal.close(); + }, + }; + this.processes.add(terminalProcess); + return terminalProcess; + } + + private async spawnNodePty(options: TerminalSpawnOptions): Promise { const pty = await import('node-pty'); const proc: IPty = pty.spawn(options.shell, [], { name: 'xterm-256color', From c7a54b316317bbc4eac16d693886722255353c63 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 22:02:11 +0800 Subject: [PATCH 03/42] chore: apply safe oxlint auto-fixes across non-engine packages Mechanical fixes only (import dedup, catch parameter naming, redundant type conversions), audited for semantics; agent-core-v2 excluded because its module-registration side effects depend on import statement order. Structured log keys and wire payload keys renamed by catch-error-name are restored explicitly. --- apps/kimi-code/src/cli/sub/acp.ts | 4 +- apps/kimi-code/src/tui/utils/tmux-keyboard.ts | 4 +- .../src/components/SessionToolsDialog.vue | 24 ++++---- .../src/components/WorkspaceFileBrowser.vue | 4 +- .../src/components/chat/ChatHeader.vue | 4 +- .../components/mobile/MobileSettingsSheet.vue | 4 +- .../components/settings/SettingsDialog.vue | 4 +- apps/vis/server/src/routes/context.ts | 4 +- apps/vis/server/src/routes/wire.ts | 4 +- .../webview-ui/src/components/LoginScreen.tsx | 4 +- packages/kap-server/src/instanceRegistry.ts | 32 +++++----- packages/kap-server/src/routes/fs.ts | 22 +++---- .../kap-server/src/routes/modelCatalog.ts | 42 +++++++------- packages/kap-server/src/routes/skills.ts | 4 +- packages/kap-server/src/routes/snapshot.ts | 8 +-- packages/kap-server/src/routes/workspaceFs.ts | 22 +++---- .../src/services/auth/persistentToken.ts | 6 +- .../src/services/auth/privateFiles.ts | 4 +- packages/kap-server/src/start.ts | 2 +- .../klient/test/e2e/harness/reverse-rpc.ts | 4 +- packages/klient/test/e2e/harness/ws.ts | 14 ++--- packages/kosong/src/http/undici-agent.ts | 9 ++- packages/minidb/bench/reader-worker.ts | 4 +- packages/minidb/src/backup.ts | 14 ++--- packages/minidb/src/cluster/coordinator.ts | 8 +-- packages/minidb/src/cluster/index.ts | 42 +++++++------- packages/minidb/src/cluster/lock-pool.ts | 12 ++-- packages/minidb/src/cluster/topology.ts | 4 +- packages/minidb/src/compaction.ts | 16 ++--- packages/minidb/src/gen-codec.ts | 36 ++++++------ packages/minidb/src/generation-files.ts | 12 ++-- packages/minidb/src/index-admin.ts | 24 ++++---- packages/minidb/src/lifecycle.ts | 52 ++++++++--------- packages/minidb/src/mini-db.ts | 24 ++++---- packages/minidb/src/recovery.ts | 18 +++--- packages/minidb/src/rename-replace.ts | 4 +- packages/minidb/src/server.ts | 8 +-- packages/minidb/src/text-index/builder.ts | 8 +-- packages/minidb/src/text-registry.ts | 8 +-- packages/minidb/src/value-codec.ts | 6 +- packages/minidb/src/value-reader.ts | 6 +- packages/minidb/src/wal.ts | 18 +++--- packages/minidb/src/worker/text-build-core.ts | 8 +-- packages/minidb/src/write-path.ts | 58 +++++++++---------- .../kimi-datasource/bin/kimi-datasource.mjs | 32 +++++----- scripts/fix-node-pty-perms.mjs | 4 +- 46 files changed, 329 insertions(+), 326 deletions(-) diff --git a/apps/kimi-code/src/cli/sub/acp.ts b/apps/kimi-code/src/cli/sub/acp.ts index 3dac3c1b927..9c39487554b 100644 --- a/apps/kimi-code/src/cli/sub/acp.ts +++ b/apps/kimi-code/src/cli/sub/acp.ts @@ -120,8 +120,8 @@ export function registerAcpCommand(parent: Command): void { : {}), }); process.exit(0); - } catch (err) { - process.stderr.write(`acp server: fatal error: ${String(err)}\n`); + } catch (error) { + process.stderr.write(`acp server: fatal error: ${String(error)}\n`); process.exit(1); } }); diff --git a/apps/kimi-code/src/tui/utils/tmux-keyboard.ts b/apps/kimi-code/src/tui/utils/tmux-keyboard.ts index b97076c7ff3..c8cd01731b8 100644 --- a/apps/kimi-code/src/tui/utils/tmux-keyboard.ts +++ b/apps/kimi-code/src/tui/utils/tmux-keyboard.ts @@ -31,8 +31,8 @@ export async function detectTmuxKeyboardWarning( if (extendedKeysFormat === 'xterm') { return TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING; } - } catch (err) { - console.debug('Failed to read tmux option:', err); + } catch (error) { + console.debug('Failed to read tmux option:', error); return undefined; } diff --git a/apps/kimi-web/src/components/SessionToolsDialog.vue b/apps/kimi-web/src/components/SessionToolsDialog.vue index c753466efee..ba04daefb7c 100644 --- a/apps/kimi-web/src/components/SessionToolsDialog.vue +++ b/apps/kimi-web/src/components/SessionToolsDialog.vue @@ -57,8 +57,8 @@ async function loadServers(): Promise { try { const { servers: list } = await getKimiWebApi().listMcpServers(sid); servers.value = list; - } catch (err) { - serversError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + serversError.value = isDaemonApiError(error) ? error.message : String(error); } finally { serversLoading.value = false; } @@ -80,10 +80,10 @@ async function toggleServerDetail(server: AppMcpServer): Promise { try { const detail = await getKimiWebApi().getMcpServerDetail(sid, name); serverDetails.value = { ...serverDetails.value, [name]: detail }; - } catch (err) { + } catch (error) { detailError.value = { ...detailError.value, - [name]: isDaemonApiError(err) ? err.message : String(err), + [name]: isDaemonApiError(error) ? error.message : String(error), }; // Keep the row collapsed on failure — a retry is one click away. serverDetails.value = { ...serverDetails.value, [name]: null }; @@ -102,8 +102,8 @@ async function reconnect(name: string): Promise { try { await getKimiWebApi().reconnectMcpServer(sid, name); await loadServers(); - } catch (err) { - reconnectError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + reconnectError.value = isDaemonApiError(error) ? error.message : String(error); } finally { const rest = new Set(reconnecting.value); rest.delete(name); @@ -182,8 +182,8 @@ async function loadCron(): Promise { try { const { tasks } = await getKimiWebApi().listCronTasks(sid); cronTasks.value = tasks; - } catch (err) { - cronError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + cronError.value = isDaemonApiError(error) ? error.message : String(error); } finally { cronLoading.value = false; } @@ -215,8 +215,8 @@ async function submitCron(): Promise { cronPrompt.value = ''; cronRecurring.value = true; showCronForm.value = false; - } catch (err) { - cronFormError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + cronFormError.value = isDaemonApiError(error) ? error.message : String(error); } finally { cronSubmitting.value = false; } @@ -229,8 +229,8 @@ async function deleteCron(taskId: string): Promise { try { await getKimiWebApi().deleteCronTask(sid, taskId); cronTasks.value = (cronTasks.value ?? []).filter((task) => task.id !== taskId); - } catch (err) { - cronError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + cronError.value = isDaemonApiError(error) ? error.message : String(error); } finally { const rest = new Set(deletingIds.value); rest.delete(taskId); diff --git a/apps/kimi-web/src/components/WorkspaceFileBrowser.vue b/apps/kimi-web/src/components/WorkspaceFileBrowser.vue index e954f3a7cf4..5cc36195eed 100644 --- a/apps/kimi-web/src/components/WorkspaceFileBrowser.vue +++ b/apps/kimi-web/src/components/WorkspaceFileBrowser.vue @@ -94,9 +94,9 @@ async function openFile(entry: { path: string; name: string }): Promise { truncated: result.truncated, languageId: result.languageId, }; - } catch (err) { + } catch (error) { if (seq !== previewSeq) return; - previewError.value = isDaemonApiError(err) ? err.message : String(err); + previewError.value = isDaemonApiError(error) ? error.message : String(error); } finally { if (seq === previewSeq) previewLoading.value = false; } diff --git a/apps/kimi-web/src/components/chat/ChatHeader.vue b/apps/kimi-web/src/components/chat/ChatHeader.vue index ce3a31c090a..a32824e44d1 100644 --- a/apps/kimi-web/src/components/chat/ChatHeader.vue +++ b/apps/kimi-web/src/components/chat/ChatHeader.vue @@ -267,8 +267,8 @@ async function loadChildSessions(): Promise { childLoadError.value = null; try { childSessions.value = await getKimiWebApi().listChildSessions(props.sessionId); - } catch (err) { - childLoadError.value = isDaemonApiError(err) ? err.message : String(err); + } catch (error) { + childLoadError.value = isDaemonApiError(error) ? error.message : String(error); } } diff --git a/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue b/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue index 3e360a92333..f5efe5119ec 100644 --- a/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue +++ b/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue @@ -189,8 +189,8 @@ async function loadAllArchived(): Promise { } archivedItems.value = all; archivedLoaded.value = true; - } catch (err) { - console.warn('loadAllArchived failed', err); + } catch (error) { + console.warn('loadAllArchived failed', error); } finally { archivedLoading.value = false; } diff --git a/apps/kimi-web/src/components/settings/SettingsDialog.vue b/apps/kimi-web/src/components/settings/SettingsDialog.vue index 85b087e63ea..b3ea4f442a1 100644 --- a/apps/kimi-web/src/components/settings/SettingsDialog.vue +++ b/apps/kimi-web/src/components/settings/SettingsDialog.vue @@ -264,8 +264,8 @@ async function loadAllArchived(): Promise { } archivedItems.value = all; archivedLoaded.value = true; - } catch (err) { - console.warn('loadAllArchived failed', err); + } catch (error) { + console.warn('loadAllArchived failed', error); } finally { archivedLoading.value = false; } diff --git a/apps/vis/server/src/routes/context.ts b/apps/vis/server/src/routes/context.ts index b61240c30ae..51b867b3954 100644 --- a/apps/vis/server/src/routes/context.ts +++ b/apps/vis/server/src/routes/context.ts @@ -45,8 +45,8 @@ export function contextRoute(home: string = KIMI_CODE_HOME): Hono { goal: proj.goal, swarm: proj.swarm, }); - } catch (err) { - const msg = (err as Error).message; + } catch (error) { + const msg = (error as Error).message; return c.json({ error: msg, code: 'READ_ERROR' }, 500); } }); diff --git a/apps/vis/server/src/routes/wire.ts b/apps/vis/server/src/routes/wire.ts index e3fc9e2ca53..fc698bbb56b 100644 --- a/apps/vis/server/src/routes/wire.ts +++ b/apps/vis/server/src/routes/wire.ts @@ -39,8 +39,8 @@ export function wireRoute(home: string = KIMI_CODE_HOME): Hono { records: result.records, warnings: result.warnings, }); - } catch (err) { - const msg = (err as Error).message; + } catch (error) { + const msg = (error as Error).message; return c.json({ error: msg, code: 'READ_ERROR' }, 500); } }); diff --git a/apps/vscode/webview-ui/src/components/LoginScreen.tsx b/apps/vscode/webview-ui/src/components/LoginScreen.tsx index 8826e362de2..9943f504561 100644 --- a/apps/vscode/webview-ui/src/components/LoginScreen.tsx +++ b/apps/vscode/webview-ui/src/components/LoginScreen.tsx @@ -57,8 +57,8 @@ export function LoginScreen({ onLoginSuccess, onSkip }: LoginScreenProps) { setError(errorMessage); } } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); if (isPaymentRequiredError(errorMessage)) { setShowSubscribeDialog(true); setState("idle"); diff --git a/packages/kap-server/src/instanceRegistry.ts b/packages/kap-server/src/instanceRegistry.ts index 21725d0d523..a0dbe147bbb 100644 --- a/packages/kap-server/src/instanceRegistry.ts +++ b/packages/kap-server/src/instanceRegistry.ts @@ -65,8 +65,8 @@ function pidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; if (code === 'ESRCH') return false; if (code === 'EPERM') return true; return true; @@ -120,8 +120,8 @@ function decode(raw: string): ServerInstanceInfo | undefined { async function readInstanceFile(filePath: string): Promise { try { return decode(await readFile(filePath, 'utf8')); - } catch (err) { - if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; return undefined; } } @@ -152,9 +152,9 @@ async function sweepStale(instancesDir: string): Promise { let names: string[]; try { names = await readdir(instancesDir); - } catch (err) { - if ((err as NodeJS.ErrnoException).code === 'ENOENT') return; - throw err; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return; + throw error; } await Promise.all( names.filter(isInstanceFile).map(async (name) => { @@ -163,8 +163,8 @@ async function sweepStale(instancesDir: string): Promise { if (info === undefined || pidAlive(info.pid)) return; try { await unlink(filePath); - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } }), ); @@ -174,9 +174,9 @@ async function listLiveInternal(instancesDir: string): Promise {}); } await rm(tmp, { force: true }).catch(() => {}); - throw err; + throw error; } } diff --git a/packages/kap-server/src/start.ts b/packages/kap-server/src/start.ts index 186fae0e639..1057d4c5528 100644 --- a/packages/kap-server/src/start.ts +++ b/packages/kap-server/src/start.ts @@ -480,7 +480,7 @@ export async function startServer(opts: ServerStartOptions): Promise { - void close().catch((err: unknown) => logger.error({ err }, 'server close failed')); + void close().catch((error: unknown) => logger.error({ err: error }, 'server close failed')); }, connectionRegistry, broadcaster, diff --git a/packages/klient/test/e2e/harness/reverse-rpc.ts b/packages/klient/test/e2e/harness/reverse-rpc.ts index 25779323ab3..a2ba8b68319 100644 --- a/packages/klient/test/e2e/harness/reverse-rpc.ts +++ b/packages/klient/test/e2e/harness/reverse-rpc.ts @@ -54,9 +54,9 @@ export function installReverseRpcHandler( const response = await opts.handler(payload); await opts.postResolve(sessionId, id, response); }) - .catch((err) => { + .catch((error) => { opts.logger('warn', `reverse-rpc: ${opts.requestEventType} resolve failed`, { - err: String(err), + err: String(error), sessionId, id, }); diff --git a/packages/klient/test/e2e/harness/ws.ts b/packages/klient/test/e2e/harness/ws.ts index dd7d6550f86..e6e9a0fa735 100644 --- a/packages/klient/test/e2e/harness/ws.ts +++ b/packages/klient/test/e2e/harness/ws.ts @@ -189,15 +189,15 @@ export class WsClient { try { const raw = typeof data === 'string' ? data : String(data); frame = JSON.parse(raw) as AnyFrame; - } catch (err) { - this.opts.logger('warn', 'ws: dropped non-JSON frame', { err: String(err) }); + } catch (error) { + this.opts.logger('warn', 'ws: dropped non-JSON frame', { err: String(error) }); recordReportEvent( { kind: 'ws', direction: 'in', url: this.opts.url, message: 'dropped non-JSON frame', - error: errorForReport(err), + error: errorForReport(error), }, { reportDir: this.opts.reportDir }, ); @@ -217,8 +217,8 @@ export class WsClient { for (const sub of this._subscribers) { try { sub(frame); - } catch (err) { - this.opts.logger('warn', 'ws: subscriber threw', { err: String(err) }); + } catch (error) { + this.opts.logger('warn', 'ws: subscriber threw', { err: String(error) }); } } @@ -229,8 +229,8 @@ export class WsClient { let matches = false; try { matches = w.match(frame); - } catch (err) { - this.opts.logger('warn', 'ws: waiter predicate threw', { err: String(err) }); + } catch (error) { + this.opts.logger('warn', 'ws: waiter predicate threw', { err: String(error) }); } if (matches) { this._waiters.splice(i, 1); diff --git a/packages/kosong/src/http/undici-agent.ts b/packages/kosong/src/http/undici-agent.ts index 59363f6dfef..9c48f734082 100644 --- a/packages/kosong/src/http/undici-agent.ts +++ b/packages/kosong/src/http/undici-agent.ts @@ -2,15 +2,18 @@ * Shared undici Agent with tuned keep-alive for upstream LLM providers. * * The Anthropic, OpenAI Chat Completions, and OpenAI Responses SDKs all - * delegate to `globalThis.fetch`, which under Node 24 maps to undici - * with the default global Agent: 50 idle conns, 5 s connect timeout, + * delegate to a `fetch` option. On Node, `globalThis.fetch` maps to + * undici's default global Agent: 50 idle conns, 5 s connect timeout, * no explicit keep-alive tuning. The CLI is a short-lived process that * issues a handful of LLM calls in a single session, so the first * cold call of each session pays the full TCP+TLS setup cost. * * This module returns a single shared Agent that the SDKs route * their traffic through, both cutting the cold-call latency and - * keeping warm-call reuse predictable. + * keeping warm-call reuse predictable. The vendored `undici` package + * is used explicitly (via `createSharedFetch`) rather than relying on + * the runtime global, so Node and Bun get identical pooling, timeout, + * and CA behaviour — verified on Bun 1.4 including SSE streaming. * * The Agent is lazy: no connections open until the first request * actually goes out, so importing this module is free. diff --git a/packages/minidb/bench/reader-worker.ts b/packages/minidb/bench/reader-worker.ts index e26819f29b2..7a76beb8dbc 100644 --- a/packages/minidb/bench/reader-worker.ts +++ b/packages/minidb/bench/reader-worker.ts @@ -98,7 +98,7 @@ async function main(): Promise { process.exit(1); } -main().catch((e) => { - out({ ok: 0, mode, error: String(e && (e as Error).stack ? (e as Error).stack : e) }); +main().catch((error) => { + out({ ok: 0, mode, error: String(error && (error as Error).stack ? (error as Error).stack : error) }); process.exit(1); }); diff --git a/packages/minidb/src/backup.ts b/packages/minidb/src/backup.ts index 34cc99e2e56..208823a364c 100644 --- a/packages/minidb/src/backup.ts +++ b/packages/minidb/src/backup.ts @@ -144,13 +144,13 @@ async function copyBackupAtomic(deps: BackupDeps, destDir: string): Promise {}); - throw err; + throw error; } await fs.rm(aside, { recursive: true, force: true }); await fsyncDir(parent, { strict: true, stats: deps.stats }); @@ -176,8 +176,8 @@ async function copyIfExists(dir: string, name: string, destDir: string): Promise if (st.isDirectory()) await fs.cp(src, path.join(destDir, name), { recursive: true }); else await fs.copyFile(src, path.join(destDir, name)); return true; - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return false; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; + throw error; } } diff --git a/packages/minidb/src/cluster/coordinator.ts b/packages/minidb/src/cluster/coordinator.ts index 54a70abe63e..695b10959d3 100644 --- a/packages/minidb/src/cluster/coordinator.ts +++ b/packages/minidb/src/cluster/coordinator.ts @@ -49,9 +49,9 @@ export class Coordinator { for (const [id, items] of groups) { try { await run(id, items); - } catch (e) { + } catch (error) { // best-effort: earlier groups may be committed already; report, don't hide. - errors.push(e); + errors.push(error); } } if (errors.length > 0) { @@ -83,8 +83,8 @@ export class Coordinator { if (existing.length > 0) await db.batch(existing.map((key) => ({ op: 'del' as const, key }))); return existing.length; }); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } } if (errors.length > 0) { diff --git a/packages/minidb/src/cluster/index.ts b/packages/minidb/src/cluster/index.ts index 9d0ff3f1feb..f21fbd3ae0c 100644 --- a/packages/minidb/src/cluster/index.ts +++ b/packages/minidb/src/cluster/index.ts @@ -123,9 +123,9 @@ export class ClusterDb { for (const { name, fields } of reg.textIndexes) { try { await db.createTextIndex(name, { fields: fields ?? undefined }); - } catch (e) { + } catch (error) { // Idempotent apply: the def may already exist on this shard. - if (!(e instanceof Error) || !e.message.includes('already exists')) throw e; + if (!(error instanceof Error) || !error.message.includes('already exists')) throw error; } } }, @@ -305,9 +305,9 @@ export class ClusterDb { try { const raw = JSON.parse(await fs.readFile(file, 'utf8')) as Partial; return { indexes: raw.indexes ?? [], compoundIndexes: raw.compoundIndexes ?? [], textIndexes: raw.textIndexes ?? [] }; - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { indexes: [], compoundIndexes: [], textIndexes: [] }; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { indexes: [], compoundIndexes: [], textIndexes: [] }; + throw error; } } @@ -421,14 +421,14 @@ export class ClusterDb { createdOn.push(shardId); } }); - } catch (e) { + } catch (error) { // Roll back the partial fan-out: drop the index from exactly the shards // this call created it on, so no shard keeps enforcing an index the // registry never recorded. await this.rollbackShards(createdOn, async (db) => { await db.dropIndex(name); }); - throw e; + throw error; } await this.mutateRegistry((current) => { const existing = current.indexes.find((i) => i.name === name); @@ -536,12 +536,12 @@ export class ClusterDb { createdOn.push(shardId); } }); - } catch (e) { + } catch (error) { // Roll back the partial fan-out (see createIndex). await this.rollbackShards(createdOn, async (db) => { await db.dropCompoundIndex(name); }); - throw e; + throw error; } await this.mutateRegistry((current) => { const existing = current.compoundIndexes.find((i) => i.name === name); @@ -596,17 +596,17 @@ export class ClusterDb { try { await db.createTextIndex(name, opts); createdOn.push(shardId); - } catch (e) { - if (!(e instanceof Error) || !e.message.includes('already exists')) throw e; + } catch (error) { + if (!(error instanceof Error) || !error.message.includes('already exists')) throw error; } }); - } catch (e) { + } catch (error) { // Roll back the partial fan-out: drop the text index only from the // shards this call created it on (see createIndex). await this.rollbackShards(createdOn, async (db) => { await db.dropTextIndex(name); }); - throw e; + throw error; } const fields = opts.fields ?? null; await this.mutateRegistry((current) => { @@ -623,8 +623,8 @@ export class ClusterDb { await this.forEachShardWriter(async (db) => { try { await db.dropTextIndex(name); - } catch (e) { - if (!(e instanceof Error) || !e.message.includes('no such text index')) throw e; + } catch (error) { + if (!(error instanceof Error) || !error.message.includes('no such text index')) throw error; } }); if (!existed) return false; @@ -647,9 +647,9 @@ export class ClusterDb { const rows = await this.reader(id, (db) => { try { return db.search(name, q, opts); - } catch (e) { - if (e instanceof Error && e.message.includes('no such text index')) return []; - throw e; + } catch (error) { + if (error instanceof Error && error.message.includes('no such text index')) return []; + throw error; } }); out.push(...rows); @@ -670,9 +670,9 @@ export class ClusterDb { try { await this.writer(id, (db) => db.compact()); compacted.push(id); - } catch (e) { - if (e instanceof LockError) skipped.push(id); - else throw e; + } catch (error) { + if (error instanceof LockError) skipped.push(id); + else throw error; } } return { compacted, skipped }; diff --git a/packages/minidb/src/cluster/lock-pool.ts b/packages/minidb/src/cluster/lock-pool.ts index 774ea9b34a0..3636cd780df 100644 --- a/packages/minidb/src/cluster/lock-pool.ts +++ b/packages/minidb/src/cluster/lock-pool.ts @@ -249,9 +249,9 @@ export class ShardLockPool { this.stats.writerOpens++; try { await this.opts.applyDefs(handle.db); - } catch (e) { + } catch (error) { await handle.close().catch(() => {}); - throw e; + throw error; } const entry: WriterEntry = { handle, @@ -280,10 +280,10 @@ export class ShardLockPool { } this.writers.set(shardId, entry); return entry; - } catch (e) { + } catch (error) { // Apply-time failures (e.g. a unique index that does not backfill) are // permanent; only lock contention is retried, until the deadline. - if (!(e instanceof LockError) || Date.now() + delay > deadline) throw e; + if (!(error instanceof LockError) || Date.now() + delay > deadline) throw error; this.stats.lockWaits++; await sleep(delay + Math.floor(Math.random() * delay)); delay = Math.min(delay * 2, 250); @@ -376,8 +376,8 @@ export class ShardLockPool { }; this.readers.set(shardId, entry); return entry; - } catch (e) { - lastErr = e; + } catch (error) { + lastErr = error; await sleep(25); } } diff --git a/packages/minidb/src/cluster/topology.ts b/packages/minidb/src/cluster/topology.ts index 675e75b1d54..caee6f45f60 100644 --- a/packages/minidb/src/cluster/topology.ts +++ b/packages/minidb/src/cluster/topology.ts @@ -48,8 +48,8 @@ export class Topology { await fs.writeFile(tmpPath, JSON.stringify(requested, null, 2)); await fs.link(tmpPath, metaPath); return new Topology(dir, requested); - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; } finally { await fs.unlink(tmpPath).catch(() => {}); } diff --git a/packages/minidb/src/compaction.ts b/packages/minidb/src/compaction.ts index 19e6453e5af..14c275afe35 100644 --- a/packages/minidb/src/compaction.ts +++ b/packages/minidb/src/compaction.ts @@ -147,8 +147,8 @@ export async function fsyncDir( try { fh = await fs.open(dir, 'r'); await fh.sync(); - } catch (e) { - const code = (e as NodeJS.ErrnoException).code; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; // Some platforms cannot fsync a directory at all. That is a permanent // environment property, not a rotation fault: mark the degraded durability // state and continue without directory fsync in both modes. @@ -158,7 +158,7 @@ export async function fsyncDir( } // Strict mode (the rotation path): a failed directory fsync breaks the // rename-durability invariant, so the caller must abort — never swallow. - if (opts.strict) throw e; + if (opts.strict) throw error; /* best-effort otherwise */ } finally { if (fh) await fh.close().catch(() => {}); @@ -222,10 +222,10 @@ export async function compact(db: CompactionTarget): Promise { db.stats.compactions++; db.stats.compactionDurationMs = (db.stats.compactionDurationMs ?? 0) + (performance.now() - t0); db.lastCompactError = null; - } catch (err) { + } catch (error) { db.stats.compactErrors = (db.stats.compactErrors ?? 0) + 1; - db.lastCompactError = err; - throw err; + db.lastCompactError = error; + throw error; } finally { db.compacting = false; // A failed rotation must not leave writers parked forever. @@ -371,7 +371,7 @@ async function runCompaction(db: CompactionTarget): Promise { // observe a new pointer against an old fd or vice versa. remap(); db.valueReader?.reopenBoth(); - } catch (err) { + } catch (error) { try { // Swap the sealed/closed WAL for a fresh handle on db.walPath. The swap // comes first: it both restores appendability and stops late in-flight @@ -387,7 +387,7 @@ async function runCompaction(db: CompactionTarget): Promise { } catch { // Best-effort recovery only — on-disk state is consistent regardless. } - throw err; + throw error; } finally { releaseRotation(); db._rotateLock = null; diff --git a/packages/minidb/src/gen-codec.ts b/packages/minidb/src/gen-codec.ts index f8957a60624..baf35a7b3ee 100644 --- a/packages/minidb/src/gen-codec.ts +++ b/packages/minidb/src/gen-codec.ts @@ -230,9 +230,9 @@ export class GenFileWriter { const fh = await fs.open(path, 'w'); try { return new GenFileWriter(fh, magic, version); - } catch (e) { + } catch (error) { await fh.close().catch(() => {}); - throw e; + throw error; } } @@ -321,8 +321,8 @@ export async function readGenerationFile(path: string, magic: string, version: n let buf: Buffer; try { buf = await fs.readFile(path); - } catch (e) { - throw new GenerationCorruptError(`generation file unreadable: ${(e as NodeJS.ErrnoException).code ?? String(e)}`); + } catch (error) { + throw new GenerationCorruptError(`generation file unreadable: ${(error as NodeJS.ErrnoException).code ?? String(error)}`); } return parseGenerationBuffer(buf, magic, version); } @@ -370,8 +370,8 @@ export async function readGenerationFileCheckedAsync( let buf: Buffer; try { buf = await fs.readFile(path); - } catch (e) { - throw new GenerationCorruptError(`generation file unreadable: ${(e as NodeJS.ErrnoException).code ?? String(e)}`); + } catch (error) { + throw new GenerationCorruptError(`generation file unreadable: ${(error as NodeJS.ErrnoException).code ?? String(error)}`); } if (buf.length < 8 + 4) throw new GenerationCorruptError('generation file too short'); for (let i = 0; i < 4; i++) { @@ -522,9 +522,9 @@ export async function writeStoreImage( } const info = await w.finish(); return { ...info, count }; - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -590,9 +590,9 @@ export async function writeDtIndexImage(path: string, cols: DtImageColumn[]): Pr } } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -663,9 +663,9 @@ export async function writeSecondaryIndexImage( } } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -825,9 +825,9 @@ export async function writeCompoundIndexImage( } } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -923,9 +923,9 @@ export async function writeTextDictionaryImage( }); } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -997,9 +997,9 @@ export async function writeTextDocsImage(path: string, image: TextDocsImage): Pr } } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } diff --git a/packages/minidb/src/generation-files.ts b/packages/minidb/src/generation-files.ts index eb5c1fdcf50..2925e22a565 100644 --- a/packages/minidb/src/generation-files.ts +++ b/packages/minidb/src/generation-files.ts @@ -56,9 +56,9 @@ export async function listGenerations(dir: string): Promise<{ id: string; n: num let names: string[]; try { names = await fs.readdir(generationsDir(dir)); - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return []; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; } const out: { id: string; n: number; tmp: boolean }[] = []; for (const name of names) { @@ -83,11 +83,11 @@ export async function readManifest(dir: string, id: string): Promise { cmpB?.add(rec.kstr, value, rec.dt); if (this.deps.indexable(value)) for (const { b } of textBs) b.add(rec.kstr, value); } - } catch (e) { + } catch (error) { for (const { b } of textBs) b.abort(); - throw e; + throw error; } this.deps.stats.indexRebuildDurationMs += performance.now() - t0; @@ -102,9 +102,9 @@ export class IndexAdmin { const t1 = performance.now(); try { for (const { b } of textBs) await b.commit(); - } catch (e) { + } catch (error) { for (const { b } of textBs) b.abort(); - throw e; + throw error; } this.deps.stats.textRebuildDurationMs += performance.now() - t1; secB?.commit(); @@ -116,8 +116,8 @@ export class IndexAdmin { try { const raw = await fs.readFile(indexPath, 'utf8'); for (const d of JSON.parse(raw) as (IndexInfo & IndexDef)[]) this.deps.indexes.create(d.name, d); - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } @@ -127,8 +127,8 @@ export class IndexAdmin { for (const d of JSON.parse(raw) as (CompoundIndexInfo & { name: string })[]) { this.deps.compound.create(d.name, { groupBy: d.groupBy, orderBy: d.orderBy, orderType: d.orderType }); } - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } @@ -149,9 +149,9 @@ export class IndexAdmin { // A unique index must not be created over data that already violates it. this.deps.indexes.assertUniqueValid(name); await this.deps.persistIndexDefinitions([...this.deps.indexes.list(), this.deps.indexes.stagedInfo(name)]); - } catch (e) { + } catch (error) { this.deps.indexes.discardStaged(name); - throw e; + throw error; } this.deps.indexes.publish(name); }); @@ -201,9 +201,9 @@ export class IndexAdmin { try { this.deps.compound.rebuildStaged(name, this.deps.liveRecords()); await this.deps.persistCompoundIndexDefinitions([...this.deps.compound.list(), this.deps.compound.stagedInfo(name)]); - } catch (e) { + } catch (error) { this.deps.compound.discardStaged(name); - throw e; + throw error; } this.deps.compound.publish(name); }); diff --git a/packages/minidb/src/lifecycle.ts b/packages/minidb/src/lifecycle.ts index 7fc0b566dac..a1b7de45d83 100644 --- a/packages/minidb/src/lifecycle.ts +++ b/packages/minidb/src/lifecycle.ts @@ -305,9 +305,9 @@ export async function openMiniDb(db: LifecycleHost, opts: OpenOptions, hoo let ids: ReturnType; try { ids = reader.open(); - } catch (e) { + } catch (error) { reader.close(); - throw e; + throw error; } const sameInode = (a: { dev: number; ino: number } | null, i: { dev: number; ino: number } | null): boolean => a === null ? i === null : i !== null && i.dev === a.dev && i.ino === a.ino; @@ -371,7 +371,7 @@ export async function openMiniDb(db: LifecycleHost, opts: OpenOptions, hoo } db.lifecycle.time('openMs', performance.now() - openT0); db.lifecycle.finishOpen(); - } catch (err) { + } catch (error) { // A background open-time compaction may still be in flight: settle it // before tearing down the WAL/store/handles it touches. if (db.compacting && db._compactDone) await db._compactDone.catch(() => {}); @@ -392,8 +392,8 @@ export async function openMiniDb(db: LifecycleHost, opts: OpenOptions, hoo // onLockFail:'readonly'): the instance never owned the directory, so // openOrRebuild must not "rebuild" (delete) anything in it — it rethrows // instead of touching a live writer's files (lock-review repro). - if (db.readOnly && err && typeof err === 'object') (err as { readOnlyOpen?: boolean }).readOnlyOpen = true; - throw err; + if (db.readOnly && error && typeof error === 'object') (error as { readOnlyOpen?: boolean }).readOnlyOpen = true; + throw error; } } @@ -461,8 +461,8 @@ async function closeResources(db: LifecycleHost, hooks: LifecycleHooks): P const errors: unknown[] = []; try { hooks.closeAllTextIndexes(); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } // Drop a read-only deferred build's private scratch dir. The postings // handles are closed above (fd-before-rm for Windows); the dir is outside @@ -471,24 +471,24 @@ async function closeResources(db: LifecycleHost, hooks: LifecycleHooks): P try { await fs.rm(db.roScratchDir, { recursive: true, force: true }); db.roScratchDir = null; - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } } try { db.store.close(); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } try { db.valueReader?.close(); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } try { await db.wal.close(); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } while (!hooks.walRecoveryIdle()) await hooks.walRecoveryChain(); try { @@ -496,8 +496,8 @@ async function closeResources(db: LifecycleHost, hooks: LifecycleHooks): P await db.lock.release(); db.lock = null; } - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } if (errors.length > 0) { throw new AggregateError( @@ -536,17 +536,17 @@ export async function openOrRebuildMiniDb( ): Promise { try { return await open(opts); - } catch (err) { - if (err instanceof LockError || (err as { code?: string }).code === 'ELOCKED') throw err; + } catch (error) { + if (error instanceof LockError || (error as { code?: string }).code === 'ELOCKED') throw error; // Only rebuild on errors that indicate unrecoverable/corrupt state (e.g. // malformed index-definition JSON). Transient I/O errors (EACCES, ENOSPC, // EIO, EMFILE, …) are rethrown so a cache opener never destroys data // because of a recoverable system error. - const rebuildable = err instanceof SyntaxError || (err as { name?: string }).name === 'CorruptFrameError'; - if (!rebuildable) throw err; - if ((err as { readOnlyOpen?: boolean }).readOnlyOpen) throw err; - if (hooks.onRebuild) hooks.onRebuild(err); - if (err instanceof SyntaxError) { + const rebuildable = error instanceof SyntaxError || (error as { name?: string }).name === 'CorruptFrameError'; + if (!rebuildable) throw error; + if ((error as { readOnlyOpen?: boolean }).readOnlyOpen) throw error; + if (hooks.onRebuild) hooks.onRebuild(error); + if (error instanceof SyntaxError) { // A corrupted index-definition sidecar holds only derived metadata and // must not cost the whole database: drop the sidecars (indexes can be // recreated by the caller) and retry once before falling back to a @@ -565,8 +565,8 @@ export async function openOrRebuildMiniDb( } if (hooks.allowDestructiveRebuild !== true) { throw new Error( - `MiniDb.openOrRebuild: ${opts.dir} failed to open with an unrecoverable corruption error (${err instanceof Error ? err.message : String(err)}); refusing to erase it. Repair the directory manually, or pass allowDestructiveRebuild: true only if this database is a rebuildable cache.`, - { cause: err }, + `MiniDb.openOrRebuild: ${opts.dir} failed to open with an unrecoverable corruption error (${error instanceof Error ? error.message : String(error)}); refusing to erase it. Repair the directory manually, or pass allowDestructiveRebuild: true only if this database is a rebuildable cache.`, + { cause: error }, ); } await fs.rm(opts.dir, { recursive: true, force: true }); diff --git a/packages/minidb/src/mini-db.ts b/packages/minidb/src/mini-db.ts index f62c3da4018..4da7a4495f4 100644 --- a/packages/minidb/src/mini-db.ts +++ b/packages/minidb/src/mini-db.ts @@ -700,8 +700,8 @@ export class MiniDb { const snapAnchor = fsSync.statSync(path.join(this.dir, SNAPSHOT_FILE)); snapshotDev = snapAnchor.dev; snapshotIno = snapAnchor.ino; - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } const checkpoint: TextBuildCheckpoint = { walOffset: walAnchor.size, @@ -894,8 +894,8 @@ export class MiniDb { const snapAnchor = fsSync.statSync(path.join(this.dir, SNAPSHOT_FILE)); snapDev = snapAnchor.dev; snapIno = snapAnchor.ino; - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } else { sealedOffset = checkpoint.walOffset; @@ -904,9 +904,9 @@ export class MiniDb { snapDev = checkpoint.snapshotDev; snapIno = checkpoint.snapshotIno; } - } catch (e) { + } catch (error) { ti.abortRebase(); - throw e; + throw error; } // In-place builds land artifacts in a per-index tmp dir inside the db @@ -1025,9 +1025,9 @@ export class MiniDb { }); if (!handle.inline) this.stats.textWorkerBuilds++; return handle.inline ? 'inline' : 'worker'; - } catch (e) { + } catch (error) { ti.abortRebase(); - throw e; + throw error; } finally { slotRelease?.(); if (tmpDir !== null) { @@ -1185,8 +1185,8 @@ export class MiniDb { // un-acked tail, so skipping the truncate is the correct recovery. const st = await fs.stat(this.walPath); if (poison.failedAtOffset <= st.size) await fs.truncate(this.walPath, poison.failedAtOffset); - } catch (err) { - this.writeDisabled = err; + } catch (error) { + this.writeDisabled = error; return; } await this.wal.refreshSize(); @@ -1464,8 +1464,8 @@ export class MiniDb { try { const existing = await fs.readdir(destDir); if (existing.length > 0) throw new Error(`restore destination is not empty: ${destDir}`); - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } await fs.mkdir(destDir, { recursive: true }); diff --git a/packages/minidb/src/recovery.ts b/packages/minidb/src/recovery.ts index f9d4271a647..f6e2f5350ea 100644 --- a/packages/minidb/src/recovery.ts +++ b/packages/minidb/src/recovery.ts @@ -269,9 +269,9 @@ function statIdentity(p: string): FileIdentity | null { try { const st = fsSync.statSync(p); return { dev: st.dev, ino: st.ino, size: st.size }; - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; } } @@ -335,12 +335,12 @@ export async function recover({ let pass: RecoverPassResult; try { pass = await recoverPass({ snapPath, walPath, store, mode, truncate, valueMode, signal, timings }); - } catch (e) { + } catch (error) { // A cancelled scan may have applied a prefix of the pass's frames: // discard the partial application so the error never carries state // into a caller that retries with the same Store. - if ((e as Error).name === 'AbortError') resetStore(store); - throw e; + if ((error as Error).name === 'AbortError') resetStore(store); + throw error; } if (pass.consistent && (!attachValueReader || attachValueReader(pass.anchors))) { pass.info.generationRetries = attempt; @@ -519,9 +519,9 @@ export async function catchUpWalAsync( let fd: number; try { fd = fsSync.openSync(walPath, 'r'); - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; } try { const st = fsSync.fstatSync(fd); diff --git a/packages/minidb/src/rename-replace.ts b/packages/minidb/src/rename-replace.ts index f3072fd560b..40305284591 100644 --- a/packages/minidb/src/rename-replace.ts +++ b/packages/minidb/src/rename-replace.ts @@ -28,8 +28,8 @@ export async function renameReplace(src: string, dst: string, opts: RenameReplac for (let attempt = 0; ; attempt++) { try { return await fs.rename(src, dst); - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'EPERM' || attempt >= retries) throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EPERM' || attempt >= retries) throw error; await sleep(base + Math.floor(Math.random() * (base + 10))); } } diff --git a/packages/minidb/src/server.ts b/packages/minidb/src/server.ts index ec3a9c6d476..8d856913640 100644 --- a/packages/minidb/src/server.ts +++ b/packages/minidb/src/server.ts @@ -234,8 +234,8 @@ export async function startServer({ dir, port = 6379, host = '127.0.0.1', fsyncP // One failing command must not starve the replies of the // commands already parsed from the same chunk. res = await handle(db, parsed.args); - } catch (e) { - res = reply.err((e as Error).message); + } catch (error) { + res = reply.err((error as Error).message); } } if (res === null) { @@ -244,8 +244,8 @@ export async function startServer({ dir, port = 6379, host = '127.0.0.1', fsyncP } send(res); } - } catch (e) { - send(reply.err((e as Error).message)); + } catch (error) { + send(reply.err((error as Error).message)); } }); }); diff --git a/packages/minidb/src/text-index/builder.ts b/packages/minidb/src/text-index/builder.ts index b79ca89f6f9..41bc4f7b1bf 100644 --- a/packages/minidb/src/text-index/builder.ts +++ b/packages/minidb/src/text-index/builder.ts @@ -80,11 +80,11 @@ export class StagedBuild implements TextIndexBuild { docLens: this.newDocLen, n: this.n, }); - } catch (e) { + } catch (error) { // Staging never touched the live view, so the previous index is // intact; the queued ops were already applied to it — just disarm. this.hooks.disarm(); - throw e; + throw error; } } @@ -117,9 +117,9 @@ export async function feedBuild( await yieldToLoop(); } } - } catch (e) { + } catch (error) { b.abort(); - throw e; + throw error; } await b.commit(); } diff --git a/packages/minidb/src/text-registry.ts b/packages/minidb/src/text-registry.ts index 7437601af00..3be4064101f 100644 --- a/packages/minidb/src/text-registry.ts +++ b/packages/minidb/src/text-registry.ts @@ -147,8 +147,8 @@ export class TextRegistry { }), ); } - } catch (e) { - if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } @@ -183,14 +183,14 @@ export class TextRegistry { const hosted = await this.deps.boundedTextBuild(name, ti, def, null); if (hosted === null) await ti.build(this.deps.textRecords()); await this.deps.persistTextIndexDefinitions([...this.textDefs, def]); - } catch (e) { + } catch (error) { // Discard the staged index so the in-memory state and the definition // sidecar (which does not name this index) do not diverge; drop the // derived postings file with it, exactly like dropTextIndex would. this.text.delete(name); ti.close(); await fs.rm(this.textPostingsPath(name), { force: true }).catch(() => {}); - throw e; + throw error; } this.textDefs.push(def); }); diff --git a/packages/minidb/src/value-codec.ts b/packages/minidb/src/value-codec.ts index de1d0a340fa..be2184913a9 100644 --- a/packages/minidb/src/value-codec.ts +++ b/packages/minidb/src/value-codec.ts @@ -75,9 +75,9 @@ export function normDt(dt?: Record | null): Record { try { return (await fs.stat(file)).size; - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return 0; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 0; + throw error; } } diff --git a/packages/minidb/src/value-reader.ts b/packages/minidb/src/value-reader.ts index ac9ed383359..04b62bfd635 100644 --- a/packages/minidb/src/value-reader.ts +++ b/packages/minidb/src/value-reader.ts @@ -51,9 +51,9 @@ export class ValueReader { private openIfExists(file: string): number | null { try { return fs.openSync(file, 'r'); - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; - throw e; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; } } diff --git a/packages/minidb/src/wal.ts b/packages/minidb/src/wal.ts index a9ade9f5f09..d41efdfbcb2 100644 --- a/packages/minidb/src/wal.ts +++ b/packages/minidb/src/wal.ts @@ -274,8 +274,8 @@ export class WAL { } } } - } catch (err) { - failure = err; + } catch (error) { + failure = error; if (this.stats) this.stats.walWriteErrors++; } if (!failure && this.policy === 'always') { @@ -284,8 +284,8 @@ export class WAL { // like a write failure. try { await this.sync(); - } catch (err) { - failure = err; + } catch (error) { + failure = error; } } if (failure) { @@ -411,12 +411,12 @@ export class WAL { const gen = this.writeGen; try { await this.fh.sync(); - } catch (err) { + } catch (error) { if (this.stats) { this.stats.walFsyncErrors++; - this.stats.lastWalFsyncError = err; + this.stats.lastWalFsyncError = error; } - throw err; + throw error; } if (this.stats) this.stats.walFsyncs++; // Only generations issued BEFORE this fsync may be marked synced: a flush @@ -469,8 +469,8 @@ export class WAL { if (!this.poisoned) { try { await this.flush(); - } catch (err) { - if (!this.poisoned) throw err; + } catch (error) { + if (!this.poisoned) throw error; } if (this.fh) await this.sync(); } diff --git a/packages/minidb/src/worker/text-build-core.ts b/packages/minidb/src/worker/text-build-core.ts index fc7f4f7d902..e0d5d298f50 100644 --- a/packages/minidb/src/worker/text-build-core.ts +++ b/packages/minidb/src/worker/text-build-core.ts @@ -412,9 +412,9 @@ async function writeBaseDocsImage( }); } return await w.finish(); - } catch (e) { + } catch (error) { await w.abort(); - throw e; + throw error; } } @@ -710,9 +710,9 @@ export async function buildTextArtifacts(spec: TextBuildCoreSpec): Promise {}); } diff --git a/packages/minidb/src/write-path.ts b/packages/minidb/src/write-path.ts index c63d57189c8..fa5ee706c14 100644 --- a/packages/minidb/src/write-path.ts +++ b/packages/minidb/src/write-path.ts @@ -108,11 +108,11 @@ export class WritePath { private async retryOnWalSeal(commit: () => Promise): Promise { try { await commit(); - } catch (e) { - const sealed = (e as { code?: string }).code === 'WAL_SEALED'; + } catch (error) { + const sealed = (error as { code?: string }).code === 'WAL_SEALED'; const closedMidRotation = - this.deps.rotateLock() !== null && e instanceof Error && e.message === 'WAL is closed'; - if (!sealed && !closedMidRotation) throw e; + this.deps.rotateLock() !== null && error instanceof Error && error.message === 'WAL is closed'; + if (!sealed && !closedMidRotation) throw error; await this.awaitRotation(); await commit(); } @@ -141,28 +141,28 @@ export class WritePath { this.applyOp(op, applied); prev = applied.prev; seq = this.deps.store().map.get(op.pk)?.seq; - } catch (err) { + } catch (error) { // See set() for this defensive path (applyOp's must-not-throw contract). void appended.done.catch(() => {}); // this op throws here; swallow the frame's rejection if (group) { - wal.poisonPending(err); + wal.poisonPending(error); this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); this.deps.walGroups.kickWalRecovery(wal); } else { this.restoreGroupKey(op.pk, applied.prev); } - throw this.deps.walGroups.markAmbiguous(err); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.groupNoteKey(group, op.pk, prev); try { await appended.done; this.deps.stats.evictions++; - } catch (e) { + } catch (error) { if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); else this.restoreKey(op.pk, prev, seq); this.deps.walGroups.kickWalRecovery(wal); - throw this.deps.walGroups.markAmbiguous(e); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.settleGroup(group, wal, appended.batchId); }; @@ -259,7 +259,7 @@ export class WritePath { // await lets a later op overwrite it. prev = applied.prev; seq = this.deps.store().map.get(op.pk)?.seq; - } catch (err) { + } catch (error) { // applyOp violated its must-not-throw contract (see its doc — stage 11 // makes it structural; this try is the defensive layer). An enqueued // frame (batchId >= 0) is un-acked and must never reach disk: poison @@ -268,23 +268,23 @@ export class WritePath { // nothing — only the partial in-memory mutation needs undoing. void appended.done.catch(() => {}); // this op throws here; swallow the frame's rejection if (group) { - wal.poisonPending(err); + wal.poisonPending(error); this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); this.deps.walGroups.kickWalRecovery(wal); } else { this.restoreGroupKey(op.pk, applied.prev); } - throw this.deps.walGroups.markAmbiguous(err); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.groupNoteKey(group, op.pk, prev); try { await appended.done; - } catch (e) { + } catch (error) { if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); else this.restoreKey(op.pk, prev, seq); this.deps.walGroups.kickWalRecovery(wal); - throw this.deps.walGroups.markAmbiguous(e); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.settleGroup(group, wal, appended.batchId); if (this.deps.valueMode() === 'disk') { @@ -323,27 +323,27 @@ export class WritePath { this.applyOp(op, applied); prev = applied.prev; seq = this.deps.store().map.get(op.pk)?.seq; - } catch (err) { + } catch (error) { // See set() for this defensive path (applyOp's must-not-throw contract). void appended.done.catch(() => {}); // this op throws here; swallow the frame's rejection if (group) { - wal.poisonPending(err); + wal.poisonPending(error); this.deps.walGroups.groupNoteKey(group, op.pk, applied.prev); this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); this.deps.walGroups.kickWalRecovery(wal); } else { this.restoreGroupKey(op.pk, applied.prev); } - throw this.deps.walGroups.markAmbiguous(err); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.groupNoteKey(group, op.pk, prev); try { await appended.done; - } catch (e) { + } catch (error) { if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); else this.restoreKey(op.pk, prev, seq); this.deps.walGroups.kickWalRecovery(wal); - throw this.deps.walGroups.markAmbiguous(e); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.settleGroup(group, wal, appended.batchId); this.deps.maybeAutoCompact(); @@ -412,20 +412,20 @@ export class WritePath { this.applyOp(op, applied); if (!prevs.has(op.pk)) prevs.set(op.pk, applied.prev); } - } catch (err) { + } catch (error) { // See set() for this defensive path (applyOp's must-not-throw // contract); the op that threw mid-apply has its pre-state in `applied`. if (cur && !prevs.has(cur.pk)) prevs.set(cur.pk, applied.prev); void appended.done.catch(() => {}); // this batch throws here; swallow the frame's rejection if (group) { - wal.poisonPending(err); + wal.poisonPending(error); for (const [pk, p] of prevs) this.deps.walGroups.groupNoteKey(group, pk, p); this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); this.deps.walGroups.kickWalRecovery(wal); } else { for (const [pk, p] of prevs) this.restoreGroupKey(pk, p); } - throw this.deps.walGroups.markAmbiguous(err); + throw this.deps.walGroups.markAmbiguous(error); } for (const [pk, p] of prevs) this.deps.walGroups.groupNoteKey(group, pk, p); // Seq identity of each record as this batch left it (undefined where the @@ -452,11 +452,11 @@ export class WritePath { } try { await appended.done; - } catch (e) { + } catch (error) { if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); else for (const [pk, prev] of prevs) this.restoreKey(pk, prev, seqs.get(pk)); this.deps.walGroups.kickWalRecovery(wal); - throw this.deps.walGroups.markAmbiguous(e); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.settleGroup(group, wal, appended.batchId); for (const [pk, { op, loc, seq }] of lastSet) { @@ -745,29 +745,29 @@ export class WritePath { gb.bytes += curValue.length + 64; } seq = this.deps.store().map.get(k)?.seq; - } catch (err) { + } catch (error) { // The in-memory mutation failed: an enqueued frame poisons the WAL // exactly like a write failure and rolls the group back; a // never-enqueued one only needs the per-op undo (see set()). void appended.done.catch(() => {}); // this op throws here; swallow the frame's rejection if (group) { - wal.poisonPending(err); + wal.poisonPending(error); this.deps.walGroups.groupNoteKey(group, k, prev); this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); this.deps.walGroups.kickWalRecovery(wal); } else { this.restoreGroupKey(k, prev); } - throw this.deps.walGroups.markAmbiguous(err); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.groupNoteKey(group, k, prev); try { await appended.done; - } catch (e) { + } catch (error) { if (group) this.deps.walGroups.rollbackGroup(group, wal, appended.batchId); else this.restoreKey(k, prev, seq); this.deps.walGroups.kickWalRecovery(wal); - throw this.deps.walGroups.markAmbiguous(e); + throw this.deps.walGroups.markAmbiguous(error); } this.deps.walGroups.settleGroup(group, wal, appended.batchId); if (this.deps.valueMode() === 'disk') { diff --git a/plugins/official/kimi-datasource/bin/kimi-datasource.mjs b/plugins/official/kimi-datasource/bin/kimi-datasource.mjs index 38fa166a3ea..0a62e68ff11 100755 --- a/plugins/official/kimi-datasource/bin/kimi-datasource.mjs +++ b/plugins/official/kimi-datasource/bin/kimi-datasource.mjs @@ -166,8 +166,8 @@ async function runTool(params) { const text = extractText(response); const formatted = (handler.format?.(text, built) ?? text).trim(); return { content: [{ type: 'text', text: appendTrace(appendWarnings(formatted, fileWarnings), trace) }] }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: appendTrace(message, trace) }], isError: true, @@ -316,16 +316,16 @@ async function loadAccessToken() { let parsed; try { parsed = JSON.parse(await readFile(credentialsFile, 'utf8')); - } catch (err) { - if (isNotFound(err)) { + } catch (error) { + if (isNotFound(error)) { throw new Error( `Kimi Code credentials file not found: ${credentialsFile}\nRun /login in Kimi Code first.`, ); } - if (err instanceof SyntaxError) { - throw new Error(`Failed to parse Kimi Code credentials file: ${err.message}`); + if (error instanceof SyntaxError) { + throw new Error(`Failed to parse Kimi Code credentials file: ${error.message}`); } - throw err; + throw error; } if (!isRecord(parsed)) { @@ -378,11 +378,11 @@ async function callKimiTool(method, params, trace = {}) { } catch { return text; } - } catch (err) { - if (err instanceof DOMException && err.name === 'AbortError') { + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { throw new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1000} seconds.`); } - throw err; + throw error; } finally { clearTimeout(timeout); } @@ -527,14 +527,14 @@ async function dispatch(message) { try { const result = await handleRequest(message); sendResult(id, result ?? {}); - } catch (err) { - if (err && typeof err === 'object' && err.jsonRpc !== undefined) { - sendError(id, err.jsonRpc); + } catch (error) { + if (error && typeof error === 'object' && error.jsonRpc !== undefined) { + sendError(id, error.jsonRpc); return; } sendError(id, { code: -32603, - message: err instanceof Error ? err.message : String(err), + message: error instanceof Error ? error.message : String(error), }); } } @@ -547,10 +547,10 @@ function start() { let message; try { message = JSON.parse(trimmed); - } catch (err) { + } catch (error) { sendError(null, { code: -32700, - message: `Parse error: ${err instanceof Error ? err.message : String(err)}`, + message: `Parse error: ${error instanceof Error ? error.message : String(error)}`, }); return; } diff --git a/scripts/fix-node-pty-perms.mjs b/scripts/fix-node-pty-perms.mjs index 5994235721d..31415598575 100644 --- a/scripts/fix-node-pty-perms.mjs +++ b/scripts/fix-node-pty-perms.mjs @@ -40,7 +40,7 @@ try { fixed++; } if (fixed > 0) console.log(`[fix-node-pty-perms] made ${fixed} spawn-helper binary(ies) executable`); -} catch (err) { - console.warn('[fix-node-pty-perms] skipped:', err instanceof Error ? err.message : String(err)); +} catch (error) { + console.warn('[fix-node-pty-perms] skipped:', error instanceof Error ? error.message : String(error)); } process.exit(0); From f7ea48dcad68e972c8d94353d0f429e79e57da1a Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 22:42:16 +0800 Subject: [PATCH 04/42] feat: add experimental Bun single-file native build Add build:native:bun alongside the existing SEA pipeline: reuse the tsdown bundles and asset collectors, stage all embedded files with a .bin suffix (bun compile otherwise parses .cjs/.json imports as modules), generate a key-to-path manifest module, and compile with bun build --compile. At runtime a Bun-backed NativeAssetSource feeds the same extraction and validation path used for SEA, so workers, web assets, and native bindings behave identically. Also migrate pnpm overrides from package.json (ignored since pnpm 10.33) into pnpm-workspace.yaml to keep the hardened dependency floor active. --- apps/kimi-code/package.json | 1 + apps/kimi-code/scripts/native/build-bun.mjs | 103 ++++++++++++++++++++ apps/kimi-code/scripts/native/bun-entry.ts | 23 +++++ apps/kimi-code/src/native/bun-assets.ts | 24 +++++ apps/kimi-code/src/native/native-assets.ts | 14 ++- package.json | 32 +----- pnpm-lock.yaml | 78 ++++++--------- pnpm-workspace.yaml | 29 +++++- 8 files changed, 217 insertions(+), 87 deletions(-) create mode 100644 apps/kimi-code/scripts/native/build-bun.mjs create mode 100644 apps/kimi-code/scripts/native/bun-entry.ts create mode 100644 apps/kimi-code/src/native/bun-assets.ts diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json index 2d87717c574..b6ba7b72c71 100644 --- a/apps/kimi-code/package.json +++ b/apps/kimi-code/package.json @@ -57,6 +57,7 @@ "smoke": "node scripts/smoke.mjs", "build:native:js": "node scripts/native/01-bundle.mjs", "build:native:sea": "node scripts/native/build.mjs --profile=local", + "build:native:bun": "node scripts/native/build-bun.mjs", "build:native:release": "node scripts/native/build.mjs --profile=release", "package:native": "node scripts/native/package.mjs", "produce:native:manifest": "node scripts/native/produce-manifest.mjs", diff --git a/apps/kimi-code/scripts/native/build-bun.mjs b/apps/kimi-code/scripts/native/build-bun.mjs new file mode 100644 index 00000000000..39a97fc889c --- /dev/null +++ b/apps/kimi-code/scripts/native/build-bun.mjs @@ -0,0 +1,103 @@ +import { chmodSync, copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { homedir } from 'node:os'; + +import { runBundleStep } from './01-bundle.mjs'; +import { collectNativeAssets, nativeAssetManifestKey } from './assets.mjs'; +import { run } from './exec.mjs'; +import { + appRoot, + executableName, + nativeBinPath, + nativeIntermediatesDir, + nativeJsBundlePath, + targetTriple, +} from './paths.mjs'; +import { collectWebAssets, webAssetManifestKey } from './web-assets.mjs'; + +const MAIN_ASSET_KEY = 'runtime/main.cjs'; +const ASSET_SUFFIX = '.bin'; + +function resolveBun() { + const candidates = [ + process.env.BUN_INSTALL ? join(process.env.BUN_INSTALL, 'bin', 'bun') : null, + join(homedir(), '.bun', 'bin', 'bun'), + '/usr/local/bin/bun', + ].filter((candidate) => candidate !== null && existsSync(candidate)); + return candidates[0] ?? 'bun'; +} + +async function buildBunNative() { + if (process.versions.bun !== undefined) { + console.error('Run this script with Node; the compiled binary itself runs on Bun.'); + process.exit(1); + } + + console.log('==> Bun native build'); + await runBundleStep(); + + const target = targetTriple(); + const stageRoot = join(nativeIntermediatesDir(), 'bun-stage', target); + mkdirSync(stageRoot, { recursive: true }); + + console.log(`==> Collecting assets (target=${target})`); + const native = await collectNativeAssets({ appRoot, target }); + const web = await collectWebAssets({ appRoot, target }); + + const entries = []; + const putAsset = (key, srcPath) => { + const dest = join(stageRoot, `${key.replaceAll('/', '__')}${ASSET_SUFFIX}`); + mkdirSync(dirname(dest), { recursive: true }); + copyFileSync(srcPath, dest); + entries.push([key, dest]); + }; + + const nativeManifestPath = join( + nativeIntermediatesDir(), + 'native-assets', + target, + 'manifest.json', + ); + mkdirSync(dirname(nativeManifestPath), { recursive: true }); + writeFileSync(nativeManifestPath, native.manifestJson); + putAsset(nativeAssetManifestKey(target), nativeManifestPath); + + const webManifestPath = join(nativeIntermediatesDir(), 'web-assets', target, 'manifest.json'); + mkdirSync(dirname(webManifestPath), { recursive: true }); + writeFileSync(webManifestPath, web.manifestJson); + putAsset(webAssetManifestKey(target), webManifestPath); + + putAsset(MAIN_ASSET_KEY, nativeJsBundlePath()); + const packageFileCount = Object.keys(native.assets).length + Object.keys(web.assets).length; + for (const [key, srcPath] of [...Object.entries(native.assets), ...Object.entries(web.assets)]) { + putAsset(key, srcPath); + } + console.log(`Staged ${entries.length} assets (${packageFileCount} package/web files)`); + + const genLines = []; + const pairs = []; + entries.forEach(([key, dest], index) => { + genLines.push(`import a${index} from ${JSON.stringify(dest)} with { type: 'file' };`); + pairs.push(` [${JSON.stringify(key)}, a${index}],`); + }); + genLines.push('', 'export const bunAssets: Array<[string, string]> = [', ...pairs, '];', ''); + writeFileSync(join(stageRoot, 'bun-assets.gen.ts'), genLines.join('\n')); + copyFileSync(join(appRoot, 'scripts', 'native', 'bun-entry.ts'), join(stageRoot, 'bun-entry.ts')); + + const outfile = nativeBinPath(target); + mkdirSync(dirname(outfile), { recursive: true }); + console.log('==> bun build --compile'); + await run(resolveBun(), [ + 'build', + '--compile', + '--outfile', + outfile, + join(stageRoot, 'bun-entry.ts'), + ]); + + if (process.platform !== 'win32') chmodSync(outfile, 0o755); + const mb = (statSync(outfile).size / 1024 / 1024).toFixed(1); + console.log(`==> Bun native build complete: ${outfile} (${mb} MB)`); +} + +await buildBunNative(); diff --git a/apps/kimi-code/scripts/native/bun-entry.ts b/apps/kimi-code/scripts/native/bun-entry.ts new file mode 100644 index 00000000000..9d433e4e1fc --- /dev/null +++ b/apps/kimi-code/scripts/native/bun-entry.ts @@ -0,0 +1,23 @@ +import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { bunAssets } from './bun-assets.gen'; + +const assets: Record = {}; +for (const [key, path] of bunAssets) { + assets[key] = path; +} +( + globalThis as unknown as { __KIMI_BUN_ASSETS__?: Record } +).__KIMI_BUN_ASSETS__ = assets; + +const mainAsset = bunAssets.find(([key]) => key === 'runtime/main.cjs'); +if (mainAsset === undefined) { + throw new Error('Bun bundle is missing the runtime/main.cjs asset'); +} +const dir = mkdtempSync(join(tmpdir(), 'kimi-bun-main-')); +const mainPath = join(dir, 'main.cjs'); +writeFileSync(mainPath, readFileSync(mainAsset[1])); +await import(pathToFileURL(mainPath).href); diff --git a/apps/kimi-code/src/native/bun-assets.ts b/apps/kimi-code/src/native/bun-assets.ts new file mode 100644 index 00000000000..551e27fbd3d --- /dev/null +++ b/apps/kimi-code/src/native/bun-assets.ts @@ -0,0 +1,24 @@ +import { readFileSync } from 'node:fs'; + +import type { NativeAssetSource } from './native-assets'; + +interface BunAssetsGlobal { + __KIMI_BUN_ASSETS__?: Readonly>; +} + +export function getBunEmbeddedAssetSource(): NativeAssetSource | null { + const assets = (globalThis as unknown as BunAssetsGlobal).__KIMI_BUN_ASSETS__; + if (assets === undefined) return null; + const keys = Object.keys(assets); + if (keys.length === 0) return null; + return { + getAssetKeys: () => Object.keys(assets), + getRawAsset: (assetKey) => { + const path = assets[assetKey]; + if (path === undefined) { + throw new Error(`Unknown Bun embedded asset: ${assetKey}`); + } + return readFileSync(path); + }, + }; +} diff --git a/apps/kimi-code/src/native/native-assets.ts b/apps/kimi-code/src/native/native-assets.ts index 06354d4adaf..99ef902f650 100644 --- a/apps/kimi-code/src/native/native-assets.ts +++ b/apps/kimi-code/src/native/native-assets.ts @@ -15,6 +15,8 @@ import { dirname, isAbsolute, join, relative, resolve, win32 as pathWin32 } from import { join as joinPosix } from 'pathe'; +import { getBunEmbeddedAssetSource } from './bun-assets'; + import { KIMI_BUILD_INFO } from '#/cli/build-info'; import { @@ -269,11 +271,13 @@ function sanitizeSegment(value: string): string { export function getSeaAssetSource(): NativeAssetSource | null { const sea = loadSeaModule(); - if (sea === null || !sea.isSea()) return null; - return { - getAssetKeys: () => sea.getAssetKeys(), - getRawAsset: (assetKey) => sea.getRawAsset(assetKey), - }; + if (sea !== null && sea.isSea()) { + return { + getAssetKeys: () => sea.getAssetKeys(), + getRawAsset: (assetKey) => sea.getRawAsset(assetKey), + }; + } + return getBunEmbeddedAssetSource(); } export function getEmbeddedNativeAssetManifest( diff --git a/package.json b/package.json index 1bb22400185..9595a7eb727 100644 --- a/package.json +++ b/package.json @@ -97,35 +97,5 @@ "engines": { "node": ">=24.15.0" }, - "packageManager": "pnpm@10.33.0", - "pnpm": { - "overrides": { - "protobufjs": ">=7.5.5", - "undici": ">=7.28.0", - "shell-quote": ">=1.8.4", - "dompurify": ">=3.4.12", - "tar": ">=7.5.22", - "fast-uri": ">=3.1.1", - "serialize-javascript": ">=7.0.3", - "react-router": ">=7.15.1 <8.0.0", - "hono": ">=4.12.18", - "body-parser": ">=2.3.0", - "ws": ">=8.21.0", - "@hono/node-server": ">=2.0.5", - "js-yaml": ">=4.2.0", - "vite": ">=6.4.3 <8.0.0", - "postcss": ">=8.5.18", - "brace-expansion": ">=5.0.8", - "ip-address": ">=10.3.1", - "find-my-way": ">=9.7.0", - "linkify-it": ">=5.0.2", - "qs": ">=6.15.2", - "diff": ">=8.0.3", - "@babel/core": ">=7.29.6", - "esbuild": ">=0.28.1", - "nanoid": ">=3.3.18", - "mermaid": ">=11.16.1", - "gray-matter>js-yaml": "^3.14.1" - } - } + "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d2ff7128aa..002eadeedb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,32 +11,36 @@ catalogs: version: 4.3.6 overrides: - protobufjs: '>=7.5.5' - undici: '>=7.28.0' - shell-quote: '>=1.8.4' + '@babel/core': '>=7.29.6' + '@hono/node-server': '>=2.0.5' + body-parser: '>=2.3.0' + brace-expansion: '>=5.0.8' + diff: '>=8.0.3' dompurify: '>=3.4.12' - tar: '>=7.5.22' + esbuild: '>=0.28.1' fast-uri: '>=3.1.1' - serialize-javascript: '>=7.0.3' - react-router: '>=7.15.1 <8.0.0' + find-my-way: '>=9.7.0' + gray-matter>js-yaml: ^3.14.1 hono: '>=4.12.18' - body-parser: '>=2.3.0' - ws: '>=8.21.0' - '@hono/node-server': '>=2.0.5' - js-yaml: '>=4.2.0' - vite: '>=6.4.3 <8.0.0' - postcss: '>=8.5.18' - brace-expansion: '>=5.0.8' ip-address: '>=10.3.1' - find-my-way: '>=9.7.0' + js-yaml: '>=4.2.0' + kimi-code>@tailwindcss/vite: 4.1.18 + kimi-code>tailwindcss: 4.1.18 linkify-it: '>=5.0.2' - qs: '>=6.15.2' - diff: '>=8.0.3' - '@babel/core': '>=7.29.6' - esbuild: '>=0.28.1' - nanoid: '>=3.3.18' mermaid: '>=11.16.1' - gray-matter>js-yaml: ^3.14.1 + nanoid: '>=3.3.18' + postcss: '>=8.5.18' + protobufjs: '>=7.5.5' + qs: '>=6.15.2' + react-router: '>=7.15.1 <8.0.0' + serialize-javascript: '>=7.0.3' + shell-quote: '>=1.8.4' + ssh2@1.17.0>cpu-features: '-' + ssh2@1.17.0>nan: '-' + tar: '>=7.5.22' + undici: '>=7.28.0' + vite: '>=6.4.3 <8.0.0' + ws: '>=8.21.0' importers: @@ -427,8 +431,8 @@ importers: version: 5.0.14(@types/react@19.2.14)(immer@11.1.11)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) devDependencies: '@tailwindcss/vite': - specifier: ^4.1.4 - version: 4.2.2(vite@7.3.6(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.18 + version: 4.1.18(vite@7.3.6(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@types/diff': specifier: ^8.0.0 version: 8.0.0 @@ -472,8 +476,8 @@ importers: specifier: 1.0.2 version: 1.0.2 tailwindcss: - specifier: ^4.1.4 - version: 4.2.2 + specifier: 4.1.18 + version: 4.1.18 vite: specifier: '>=6.4.3 <8.0.0' version: 7.3.6(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) @@ -5298,10 +5302,6 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buildcheck@0.0.7: - resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} - engines: {node: '>=10.0.0'} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -5589,10 +5589,6 @@ packages: typescript: optional: true - cpu-features@0.0.10: - resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} - engines: {node: '>=10.0.0'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -7548,9 +7544,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.28.0: - resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} - nanoid@6.0.1: resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} engines: {node: ^22 || ^24 || >=26} @@ -14153,9 +14146,6 @@ snapshots: ieee754: 1.2.1 optional: true - buildcheck@0.0.7: - optional: true - bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -14443,12 +14433,6 @@ snapshots: optionalDependencies: typescript: 6.0.3 - cpu-features@0.0.10: - dependencies: - buildcheck: 0.0.7 - nan: 2.28.0 - optional: true - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -16773,9 +16757,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.28.0: - optional: true - nanoid@6.0.1: {} napi-build-utils@2.0.0: @@ -18152,9 +18133,6 @@ snapshots: dependencies: asn1: 0.2.6 bcrypt-pbkdf: 1.0.2 - optionalDependencies: - cpu-features: 0.0.10 - nan: 2.28.0 stackback@0.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3d5293599e6..ba7e5c6bf9e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,10 +22,37 @@ catalog: tsx: ^4.21.0 overrides: + "@babel/core": ">=7.29.6" + "@hono/node-server": ">=2.0.5" + "body-parser": ">=2.3.0" + "brace-expansion": ">=5.0.8" + "diff": ">=8.0.3" + "dompurify": ">=3.4.12" + "esbuild": ">=0.28.1" + "fast-uri": ">=3.1.1" + "find-my-way": ">=9.7.0" + "gray-matter>js-yaml": "^3.14.1" + "hono": ">=4.12.18" + "ip-address": ">=10.3.1" + "js-yaml": ">=4.2.0" "kimi-code>@tailwindcss/vite": "4.1.18" + "kimi-code>tailwindcss": "4.1.18" + "linkify-it": ">=5.0.2" + "mermaid": ">=11.16.1" + "nanoid": ">=3.3.18" + "postcss": ">=8.5.18" + "protobufjs": ">=7.5.5" + "qs": ">=6.15.2" + "react-router": ">=7.15.1 <8.0.0" + "serialize-javascript": ">=7.0.3" + "shell-quote": ">=1.8.4" "ssh2@1.17.0>cpu-features": "-" "ssh2@1.17.0>nan": "-" - "kimi-code>tailwindcss": "4.1.18" + "tar": ">=7.5.22" + "undici": ">=7.28.0" + "vite": ">=6.4.3 <8.0.0" + "ws": ">=8.21.0" + onlyBuiltDependencies: - esbuild - node-pty From 7a961e171883a2412cf128938d5e861f1d2f2bd2 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 22:49:59 +0800 Subject: [PATCH 05/42] ci: gate an experimental linux-x64 Bun native bundle job Add a build-bun workflow input (default false) that builds, smokes, and packages the Bun single-file binary on ubuntu-24.04. The build script now maps KIMI_CODE_BUILD_TARGET to bun compile targets and reuses the shared sign/checksum step for artifact parity. --- .github/workflows/_native-build.yml | 58 +++++++++++++++++++++ apps/kimi-code/scripts/native/build-bun.mjs | 32 +++++++++--- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/.github/workflows/_native-build.yml b/.github/workflows/_native-build.yml index 92f1510e04c..c1a6e3c4397 100644 --- a/.github/workflows/_native-build.yml +++ b/.github/workflows/_native-build.yml @@ -18,6 +18,11 @@ on: required: false type: boolean default: false + build-bun: + description: 'Also build the experimental Bun single-file binary (linux-x64 only)' + required: false + type: boolean + default: false secrets: APPLE_CERTIFICATE_P12: required: false @@ -132,3 +137,56 @@ jobs: apps/kimi-code/dist-native/artifacts/kimi-code-${{ matrix.target }}.zip apps/kimi-code/dist-native/artifacts/kimi-code-${{ matrix.target }}.zip.sha256 if-no-files-found: ignore + + native-bundle-bun: + name: Native bundle (bun, linux-x64) + runs-on: ubuntu-24.04 + if: inputs.build-bun == true + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Rust toolchain (napi native-tools) + uses: dtolnay/rust-toolchain@stable + + - name: Build kimi-native-tools (.node for embedded assets) + run: pnpm --filter @moonshot-ai/kimi-native-tools run build + + - name: Build Kimi web assets check + run: node apps/kimi-code/scripts/check-web-assets.mjs + + - name: Build native executable (Bun) + run: pnpm --filter @moonshot-ai/kimi-code run build:native:bun + + - name: Smoke test native executable + run: pnpm --filter @moonshot-ai/kimi-code run test:native:smoke + + - name: Package native artifact + run: pnpm --filter @moonshot-ai/kimi-code run package:native + + - name: Upload native artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.upload-artifact-prefix }}-bun-linux-x64 + retention-days: ${{ inputs.retention-days }} + path: | + apps/kimi-code/dist-native/artifacts/kimi-code-linux-x64.zip + apps/kimi-code/dist-native/artifacts/kimi-code-linux-x64.zip.sha256 + if-no-files-found: ignore diff --git a/apps/kimi-code/scripts/native/build-bun.mjs b/apps/kimi-code/scripts/native/build-bun.mjs index 39a97fc889c..26685b4df91 100644 --- a/apps/kimi-code/scripts/native/build-bun.mjs +++ b/apps/kimi-code/scripts/native/build-bun.mjs @@ -1,13 +1,13 @@ -import { chmodSync, copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'; +import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { runBundleStep } from './01-bundle.mjs'; import { collectNativeAssets, nativeAssetManifestKey } from './assets.mjs'; import { run } from './exec.mjs'; +import { runSignStep } from './04-sign.mjs'; import { appRoot, - executableName, nativeBinPath, nativeIntermediatesDir, nativeJsBundlePath, @@ -18,6 +18,15 @@ import { collectWebAssets, webAssetManifestKey } from './web-assets.mjs'; const MAIN_ASSET_KEY = 'runtime/main.cjs'; const ASSET_SUFFIX = '.bin'; +const BUN_TARGETS = new Map([ + ['linux-x64', 'bun-linux-x64'], + ['linux-arm64', 'bun-linux-arm64'], + ['darwin-x64', 'bun-darwin-x64'], + ['darwin-arm64', 'bun-darwin-arm64'], + ['win32-x64', 'bun-windows-x64'], + ['win32-arm64', 'bun-windows-arm64'], +]); + function resolveBun() { const candidates = [ process.env.BUN_INSTALL ? join(process.env.BUN_INSTALL, 'bin', 'bun') : null, @@ -33,14 +42,20 @@ async function buildBunNative() { process.exit(1); } - console.log('==> Bun native build'); + const target = targetTriple(); + const bunTarget = BUN_TARGETS.get(target); + if (bunTarget === undefined) { + console.error(`Unsupported Bun native target: ${target}`); + process.exit(1); + } + + console.log(`==> Bun native build (target=${target})`); await runBundleStep(); - const target = targetTriple(); const stageRoot = join(nativeIntermediatesDir(), 'bun-stage', target); mkdirSync(stageRoot, { recursive: true }); - console.log(`==> Collecting assets (target=${target})`); + console.log('==> Collecting assets'); const native = await collectNativeAssets({ appRoot, target }); const web = await collectWebAssets({ appRoot, target }); @@ -86,16 +101,19 @@ async function buildBunNative() { const outfile = nativeBinPath(target); mkdirSync(dirname(outfile), { recursive: true }); - console.log('==> bun build --compile'); + console.log(`==> bun build --compile --target=${bunTarget}`); await run(resolveBun(), [ 'build', '--compile', + '--target', + bunTarget, '--outfile', outfile, join(stageRoot, 'bun-entry.ts'), ]); - if (process.platform !== 'win32') chmodSync(outfile, 0o755); + await runSignStep(); + const mb = (statSync(outfile).size / 1024 / 1024).toFixed(1); console.log(`==> Bun native build complete: ${outfile} (${mb} MB)`); } From 6f188bf94b2e7a0a746d1c6706319e144b361789 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 23:06:10 +0800 Subject: [PATCH 06/42] refactor: use nullish coalescing where operands cannot be falsy Convert || to ?? only at sites whose declared types exclude empty strings, zero, and false; wire-parsed and index-access sites stay on || deliberately. --- packages/minidb/src/crc32.ts | 2 +- packages/minidb/src/query-engine.ts | 4 ++-- packages/minidb/src/server.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/minidb/src/crc32.ts b/packages/minidb/src/crc32.ts index 2e7d452b2d0..dac548578d2 100644 --- a/packages/minidb/src/crc32.ts +++ b/packages/minidb/src/crc32.ts @@ -30,7 +30,7 @@ function buildTable(): Uint32Array { * @returns unsigned 32-bit crc */ export function crc32(buf: Buffer | Uint8Array, prev = 0): number { - if (TABLE === null) TABLE = buildTable(); + TABLE ??= buildTable(); let c = prev ^ 0xffffffff; for (let i = 0; i < buf.length; i++) { c = TABLE[(c ^ buf[i]!) & 0xff]! ^ (c >>> 8); diff --git a/packages/minidb/src/query-engine.ts b/packages/minidb/src/query-engine.ts index 6bf02cd3e07..b3d28a03ea2 100644 --- a/packages/minidb/src/query-engine.ts +++ b/packages/minidb/src/query-engine.ts @@ -270,7 +270,7 @@ export class QueryEngine { keys = keys === null ? indexed : filterKeys(keys, (k) => set.has(k)); } - if (keys === null) keys = this.deps.store().rawKeys({}); + keys ??= this.deps.store().rawKeys({}); const stats = this.deps.stats; const skip = q.skip ?? 0; @@ -375,7 +375,7 @@ export class QueryEngine { keys = keys === null ? indexed : filterKeys(keys, (k) => set.has(k)); } - if (keys === null) keys = this.deps.store().rawKeys({}); + keys ??= this.deps.store().rawKeys({}); const stats = this.deps.stats; const skip = q.skip ?? 0; diff --git a/packages/minidb/src/server.ts b/packages/minidb/src/server.ts index 8d856913640..09a496438e1 100644 --- a/packages/minidb/src/server.ts +++ b/packages/minidb/src/server.ts @@ -134,7 +134,7 @@ async function handle(db: MiniDb, args: Buffer[]): Promise, args: Buffer[]): Promise Date: Sun, 23 Aug 2026 23:06:10 +0800 Subject: [PATCH 07/42] test: cover the Bun embedded asset source --- .../test/native/native-assets.test.ts | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/apps/kimi-code/test/native/native-assets.test.ts b/apps/kimi-code/test/native/native-assets.test.ts index 7c2cce2ecb1..30f14e49ed9 100644 --- a/apps/kimi-code/test/native/native-assets.test.ts +++ b/apps/kimi-code/test/native/native-assets.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'no import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { getTextBuildWorkerRuntimeState, resetTextBuildWorkerRuntime, @@ -18,6 +18,7 @@ import { type NativeAssetManifest, type NativeAssetSource, } from '#/native/native-assets'; +import { getBunEmbeddedAssetSource } from '#/native/bun-assets'; import { installMinidbTextBuildWorker } from '#/native/minidb-worker'; import { loadNativePackage } from '#/native/native-require'; @@ -334,3 +335,65 @@ describe('native assets', () => { ).toThrow(/duplicate assetKey/); }); }); + +describe('bun embedded assets', () => { + const bunGlobal = globalThis as unknown as { __KIMI_BUN_ASSETS__?: Record }; + + beforeEach(() => { + delete bunGlobal.__KIMI_BUN_ASSETS__; + }); + + afterEach(() => { + delete bunGlobal.__KIMI_BUN_ASSETS__; + }); + + it('returns null when the Bun asset global is missing or empty', () => { + expect(getBunEmbeddedAssetSource()).toBeNull(); + bunGlobal.__KIMI_BUN_ASSETS__ = {}; + expect(getBunEmbeddedAssetSource()).toBeNull(); + }); + + it('exposes asset keys and raw file contents from mapped paths', () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-bun-assets-')); + try { + const textPath = join(dir, 'worker.mjs'); + const binaryPath = join(dir, 'native.bin'); + const worker = 'export const worker = true;\n'; + const binary = Buffer.from([0x00, 0xff, 0x10, 0xfe]); + writeFileSync(textPath, worker, 'utf-8'); + writeFileSync(binaryPath, binary); + + bunGlobal.__KIMI_BUN_ASSETS__ = { + 'native/test-target/runtime/worker': textPath, + 'native/test-target/native.bin': binaryPath, + }; + + const source = getBunEmbeddedAssetSource(); + expect(source).not.toBeNull(); + expect(source!.getAssetKeys()).toEqual([ + 'native/test-target/runtime/worker', + 'native/test-target/native.bin', + ]); + expect(source!.getRawAsset('native/test-target/runtime/worker')).toEqual( + Buffer.from(worker, 'utf-8'), + ); + expect(source!.getRawAsset('native/test-target/native.bin')).toEqual(binary); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('throws when looking up an unknown asset key', () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-bun-assets-unknown-')); + try { + const path = join(dir, 'asset.txt'); + writeFileSync(path, 'ok'); + bunGlobal.__KIMI_BUN_ASSETS__ = { 'native/known': path }; + + const source = getBunEmbeddedAssetSource()!; + expect(() => source.getRawAsset('native/missing')).toThrow(/Unknown Bun embedded asset/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); From 5c410fc0dfea437efe9e24c5a07d941b8b02ed49 Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 23:06:11 +0800 Subject: [PATCH 08/42] chore: drop stray debug log from TUI startup path --- apps/kimi-code/src/tui/utils/tmux-keyboard.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/kimi-code/src/tui/utils/tmux-keyboard.ts b/apps/kimi-code/src/tui/utils/tmux-keyboard.ts index c8cd01731b8..6777997f805 100644 --- a/apps/kimi-code/src/tui/utils/tmux-keyboard.ts +++ b/apps/kimi-code/src/tui/utils/tmux-keyboard.ts @@ -31,8 +31,7 @@ export async function detectTmuxKeyboardWarning( if (extendedKeysFormat === 'xterm') { return TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING; } - } catch (error) { - console.debug('Failed to read tmux option:', error); + } catch { return undefined; } From 02b9cf4e96644117671d6573f6aaf34476cfe58d Mon Sep 17 00:00:00 2001 From: luozihao Date: Sun, 23 Aug 2026 23:06:11 +0800 Subject: [PATCH 09/42] docs: describe the experimental Bun native build in contributing guides --- CONTRIBUTING.md | 15 +++++++++++++++ CONTRIBUTING.zh-CN.md | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 032cc7db0dc..da77d4a9bf6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -165,6 +165,21 @@ mv ~/.kimi-code/bin/kimi-new ~/.kimi-code/bin/kimi > **Note**: The SEA build currently requires `@moonshot-ai/kimi-native-tools` listed as a dependency in `apps/kimi-code/package.json` and registered in `apps/kimi-code/scripts/native/native-deps.mjs`. See [Common Issues](#common-issues) for known pitfalls. +### Native Bun build (experimental) + +An experimental single-file build compiles the CLI with Bun instead of Node.js SEA. Requires Bun >= 1.4 (`curl -fsSL https://bun.sh/install | bash`; see [bun.sh](https://bun.sh)). Node.js >= 24.15 still runs the build script itself, and a Rust toolchain is required because the `kimi-native-tools` `.node` binary is embedded. + +From `apps/kimi-code`, run `node scripts/native/build-bun.mjs` (or `pnpm run build:native:bun`), then verify with the existing smoke test: + +```sh +node scripts/native/build-bun.mjs +pnpm run test:native:smoke +``` + +Output: `apps/kimi-code/dist-native/bin//kimi`. + +Status caveats: validated on linux-x64 only; macOS signing reuses the shared codesign step; cross-target staging requires that target's platform packages to be present locally (the collector fails fast otherwise). This pipeline is experimental and parallel to the default SEA pipeline (`build:native:sea`), which remains the release default. Runtime asset loading is unified: both pipelines feed the same extraction/cache layer. + ### Common Issues | Symptom | Cause | Fix | diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 160e6d76c18..1a1c1bcf970 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -54,6 +54,21 @@ pnpm install - `pnpm lint:fix` — oxlint 自动修复 - `pnpm build` — 构建全部包 +### 原生 Bun 构建(实验性) + +实验性的单文件构建方案:用 Bun 编译 CLI,替代 Node.js SEA。前置要求:Bun >= 1.4(一行安装命令 `curl -fsSL https://bun.sh/install | bash`,详见 [bun.sh](https://bun.sh));构建脚本本身仍由 Node.js >= 24.15 运行;Rust 工具链仍然必需,因为需要嵌入 `kimi-native-tools` 的 `.node` 二进制。 + +在 `apps/kimi-code` 下运行 `node scripts/native/build-bun.mjs`(或 `pnpm run build:native:bun`),然后用现有的冒烟测试验证: + +```sh +node scripts/native/build-bun.mjs +pnpm run test:native:smoke +``` + +产物输出到 `apps/kimi-code/dist-native/bin//kimi`。 + +当前状态注意事项:仅在 linux-x64 上验证过;macOS 签名复用共享的 codesign 步骤;交叉目标暂存需要在本地存在目标平台对应的包(否则收集器会快速失败)。该流水线目前是实验性的,与默认的 SEA 流水线(`build:native:sea`)并行,后者仍是发布版本的默认选择。运行时资源加载已统一:两条流水线使用同一套提取/缓存层。 + ## 提交规范 所有 commit 和 PR 标题必须遵循 [Conventional Commits](https://www.conventionalcommits.org/)。 From fb46b8c8fcf21da0bf85aa47ae35329764814988 Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 00:00:13 +0800 Subject: [PATCH 10/42] fix: guard corrupt-input paths in minidb Replace non-null assertions with explicit guards where values cross trust boundaries: RESP command arity from clients, worker postMessage results, disk-ref reads through optional handles, and empty-token caller input. Hot-path skiplointer chasing keeps its structural invariants documented by loop bounds. --- packages/minidb/src/generation-builder.ts | 12 ++++++--- packages/minidb/src/mini-db.ts | 3 ++- packages/minidb/src/query.ts | 1 + packages/minidb/src/server.ts | 30 +++++++++++++++-------- packages/minidb/src/worker/text-build.ts | 3 ++- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/minidb/src/generation-builder.ts b/packages/minidb/src/generation-builder.ts index 412042669ed..cdf301620f0 100644 --- a/packages/minidb/src/generation-builder.ts +++ b/packages/minidb/src/generation-builder.ts @@ -484,10 +484,14 @@ export class GenerationBuilder { imageRecords.set(kstr, { ref: rec.ref, expireAt: rec.expireAt, dt: rec.dt }); dtB.set(kstr, rec.dt); if (needValues) { - const buf = - rec.ref.kind === 'memory' - ? rec.ref.value - : this.deps.getValueReader()!.read(rec.ref.loc); + let buf: Buffer; + if (rec.ref.kind === 'memory') { + buf = rec.ref.value; + } else { + const reader = this.deps.getValueReader(); + if (!reader) throw new Error('ValueReader is not open'); + buf = reader.read(rec.ref.loc); + } const doc = this.deps.decode(buf); if (this.deps.indexable(doc)) { secB.add(kstr, doc); diff --git a/packages/minidb/src/mini-db.ts b/packages/minidb/src/mini-db.ts index 4da7a4495f4..e8cc157c5d4 100644 --- a/packages/minidb/src/mini-db.ts +++ b/packages/minidb/src/mini-db.ts @@ -992,7 +992,8 @@ export class MiniDb { } // Verify before trusting (the worker's output is discardable evidence): // streaming crc on the raw postings file, envelope+crc on the images. - const r = result.indexes[0]!; + const r = result.indexes[0]; + if (!r) throw new Error('text build produced no index result'); await verifyFileCrcAsync(postingsPath, r.postingsInfo); const dictPayload = await readGenerationFileCheckedAsync( dictionaryPath, diff --git a/packages/minidb/src/query.ts b/packages/minidb/src/query.ts index 922a4856587..f6fd05d2a4c 100644 --- a/packages/minidb/src/query.ts +++ b/packages/minidb/src/query.ts @@ -40,6 +40,7 @@ export function getPath(doc: Doc, path: Path): unknown { export function setPath(obj: Doc, path: Path, value: unknown): Doc { const tokens = tokenizePath(path); + if (tokens.length === 0) return obj; let cur = obj as Record; for (let i = 0; i < tokens.length - 1; i++) { const t = tokens[i]!; diff --git a/packages/minidb/src/server.ts b/packages/minidb/src/server.ts index 09a496438e1..eafee490997 100644 --- a/packages/minidb/src/server.ts +++ b/packages/minidb/src/server.ts @@ -124,7 +124,8 @@ export class RespParser { } async function handle(db: MiniDb, args: Buffer[]): Promise { - const cmd = args[0]!.toString().toUpperCase(); + const cmd = args[0]?.toString().toUpperCase(); + if (cmd === undefined) return reply.err('empty command'); const S = (i: number): string | undefined => (args[i] === undefined ? undefined : args[i]!.toString()); switch (cmd) { @@ -133,12 +134,14 @@ async function handle(db: MiniDb, args: Buffer[]): Promise, args: Buffer[]): Promise, args: Buffer[]): Promise { const i = argv.indexOf(`--${name}`); - return i === -1 ? def : argv[i + 1]!; + const v = i === -1 ? undefined : argv[i + 1]; + return v ?? def; }; const dir = arg('dir', './data'); const port = Number(arg('port', '6379')); diff --git a/packages/minidb/src/worker/text-build.ts b/packages/minidb/src/worker/text-build.ts index fbb5d96e58d..a0c8ba8a6f7 100644 --- a/packages/minidb/src/worker/text-build.ts +++ b/packages/minidb/src/worker/text-build.ts @@ -292,7 +292,8 @@ export function startWorkerTextBuild( if (message.type === 'done') { await stopWorker(); if (cancelRequested) throw abortError(); - return message.result!; + if (!message.result) throw new WorkerTextBuildError('text build worker returned no result', false); + return message.result; } if (message.type === 'failed') { await stopWorker(); From f9acfbf60cfaed4316376fb315c097b1c3da4bbd Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 00:00:13 +0800 Subject: [PATCH 11/42] refactor: replace explicit any with precise types across packages Union narrowing over real payload types for TUI session replay, Event2 for event bus seams, contravariance-correct constructor params in the channel registry, and typed React global lookup in i18n-shared. The public SDK SessionMeta.custom record stays as-is (consumer-visible contract). --- apps/kimi-code/src/cli/v2/run-v2-print.ts | 6 ++-- .../src/tui/controllers/session-replay.ts | 32 +++++++++---------- apps/vis/server/src/lib/context-projector.ts | 8 ++--- packages/i18n-shared/src/web.ts | 12 ++++--- .../src/transport/channelRegistry.ts | 2 +- .../ws/v1/sessionEventBroadcaster.ts | 16 ++++++---- packages/node-sdk/src/v2/session-wiring.ts | 4 +-- 7 files changed, 43 insertions(+), 37 deletions(-) diff --git a/apps/kimi-code/src/cli/v2/run-v2-print.ts b/apps/kimi-code/src/cli/v2/run-v2-print.ts index 7f1ec10edad..d4bdeef2325 100644 --- a/apps/kimi-code/src/cli/v2/run-v2-print.ts +++ b/apps/kimi-code/src/cli/v2/run-v2-print.ts @@ -429,7 +429,7 @@ async function runNativeTurn( await agent.accessor.get(IAuthSummaryService).ensureReady(); const turnEndings = createPrintTurnEndings(); - const subscription = agent.accessor.get(IEventBus).subscribe((event: Event2) => { + const subscription = agent.accessor.get(IEventBus).subscribe((event) => { dispatchNativeEvent(writer, event, stderr); // Arm the turn-endings collector before `turn.result` settles so a // background-task completion that steers a new turn right after the main @@ -526,7 +526,7 @@ async function runNativeGoal( replace: goal.replace, }); let completedSnapshot: { readonly status: string } | null = null; - const subscription = agent.accessor.get(IEventBus).subscribe((event: Event2) => { + const subscription = agent.accessor.get(IEventBus).subscribe((event) => { if (event.type === 'goal.updated') { const updated = event as unknown as GoalUpdated; if (updated.change?.kind === 'completion' && updated.snapshot !== null) { @@ -552,7 +552,7 @@ async function runNativeGoal( function dispatchNativeEvent( writer: PromptTurnWriter, - event: Event2, + event: Event2, stderr: PromptOutput, ): void { switch (event.type) { diff --git a/apps/kimi-code/src/tui/controllers/session-replay.ts b/apps/kimi-code/src/tui/controllers/session-replay.ts index 6636e2e6d23..6ff1610139b 100644 --- a/apps/kimi-code/src/tui/controllers/session-replay.ts +++ b/apps/kimi-code/src/tui/controllers/session-replay.ts @@ -1,10 +1,12 @@ import type { AgentReplayRecord, + ContentPart, ContextMessage, GoalChange, PermissionMode, ResumedAgentState, Session, + TextPromptPart, ToolCall, } from '@moonshot-ai/kimi-code-sdk'; @@ -85,6 +87,11 @@ function unescapeBashXml(text: string): string { .replaceAll('&', '&'); } +function trailingTextPart(content: readonly ContentPart[]): string | undefined { + const textParts = content.filter((part): part is TextPromptPart => part.type === 'text'); + return textParts[textParts.length - 1]?.text; +} + export class SessionReplayRenderer { constructor(private readonly host: SessionReplayHost) {} @@ -206,15 +213,11 @@ export class SessionReplayRenderer { nextRecord !== undefined && nextRecord.type === 'message' && nextRecord.message.role === 'user' && - Array.isArray((nextRecord.message.origin as any)?.skillActivations) + nextRecord.message.origin?.kind === 'user' && + Array.isArray(nextRecord.message.origin.skillActivations) ) { - const nextUserMsg = nextRecord.message; + const activations = nextRecord.message.origin.skillActivations; this.advanceTurn(context); - const activations = (nextUserMsg.origin as any).skillActivations as Array<{ - activationId: string; - skillName: string; - skillArgs?: string; - }>; for (const act of activations) { const entry: TranscriptEntry = { id: nextTranscriptId(), @@ -231,9 +234,9 @@ export class SessionReplayRenderer { this.host.appendTranscriptEntry(entry); } this.renderHookResult(context, record.message); - const textParts = nextUserMsg.content.filter((c: any) => c.type === 'text'); const promptText = - (textParts[textParts.length - 1] as any)?.text ?? contentPartsToText(nextUserMsg.content); + trailingTextPart(nextRecord.message.content) ?? + contentPartsToText(nextRecord.message.content); this.host.appendTranscriptEntry(replayEntry(context, 'user', promptText, 'plain')); i++; continue; @@ -242,14 +245,10 @@ export class SessionReplayRenderer { record.type === 'message' && record.message.role === 'user' && record.message.origin?.kind === 'user' && - Array.isArray((record.message.origin as any).skillActivations) + Array.isArray(record.message.origin.skillActivations) ) { this.advanceTurn(context); - const activations = (record.message.origin as any).skillActivations as Array<{ - activationId: string; - skillName: string; - skillArgs?: string; - }>; + const activations = record.message.origin.skillActivations; for (const act of activations) { const entry: TranscriptEntry = { id: nextTranscriptId(), @@ -274,9 +273,8 @@ export class SessionReplayRenderer { this.renderHookResult(context, nextRecord.message); i++; } - const textParts = record.message.content.filter((c: any) => c.type === 'text'); const promptText = - (textParts[textParts.length - 1] as any)?.text ?? contentPartsToText(record.message.content); + trailingTextPart(record.message.content) ?? contentPartsToText(record.message.content); this.host.appendTranscriptEntry(replayEntry(context, 'user', promptText, 'plain')); continue; } diff --git a/apps/vis/server/src/lib/context-projector.ts b/apps/vis/server/src/lib/context-projector.ts index 8c202857255..92a0b2e7cb5 100644 --- a/apps/vis/server/src/lib/context-projector.ts +++ b/apps/vis/server/src/lib/context-projector.ts @@ -593,10 +593,10 @@ export function projectContext( } function addUsage(into: TokenUsage, src: TokenUsage): void { - (into as any).inputOther += src.inputOther; - (into as any).output += src.output; - (into as any).inputCacheRead += src.inputCacheRead; - (into as any).inputCacheCreation += src.inputCacheCreation; + into.inputOther += src.inputOther; + into.output += src.output; + into.inputCacheRead += src.inputCacheRead; + into.inputCacheCreation += src.inputCacheCreation; } const MICRO_TRUNCATED_MARKER = '[Old tool result content cleared]'; diff --git a/packages/i18n-shared/src/web.ts b/packages/i18n-shared/src/web.ts index 7cfd1558981..8d12f4b6d6c 100644 --- a/packages/i18n-shared/src/web.ts +++ b/packages/i18n-shared/src/web.ts @@ -113,15 +113,19 @@ export function createI18n>( // Lazily import React's hooks so this module doesn't hard-depend on React // (non-React apps like vanilla TS can still use `createI18n`). + type ReactGlobal = { + useState?: (initial: S) => [S, (value: S) => void]; + useEffect?: (effect: () => void | (() => void), deps?: readonly unknown[]) => void; + useCallback?: (fn: F, deps: readonly unknown[]) => F; + }; + function useLocale(): { locale: Locale; set: (l: Locale) => void } { // Dynamic hook resolution — works when React is available. - const React = (globalThis as any).React; - if (!React || !React.useState || !React.useEffect || !React.useCallback) { + const { useState, useEffect, useCallback } = (globalThis as { React?: ReactGlobal }).React ?? {}; + if (!useState || !useEffect || !useCallback) { // Non-React environment: return a static snapshot. return { locale: currentLocale, set: setLocale }; } - - const { useState, useEffect, useCallback } = React; const [locale, setLocaleState] = useState(currentLocale); const set = useCallback((l: Locale) => { diff --git a/packages/kap-server/src/transport/channelRegistry.ts b/packages/kap-server/src/transport/channelRegistry.ts index 18cfd434383..06a4b8fe2ef 100644 --- a/packages/kap-server/src/transport/channelRegistry.ts +++ b/packages/kap-server/src/transport/channelRegistry.ts @@ -93,7 +93,7 @@ function extractParams(fn: (...args: never[]) => unknown): string { } function describeMethods( - ctor: new (...args: any[]) => unknown, + ctor: new (...args: never[]) => unknown, ): readonly ChannelMethodDescriptor[] { const methods = new Map(); let proto: object | null = ctor.prototype; diff --git a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts index 17a666631a6..97aff7d15ba 100644 --- a/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts +++ b/packages/kap-server/src/transport/ws/v1/sessionEventBroadcaster.ts @@ -747,7 +747,7 @@ export class SessionEventBroadcaster { return state; } - private onCoreEvent(event: Event2): void { + private onCoreEvent(event: Event2): void { const corePayload = (event as { readonly payload?: unknown }).payload; if (event.type === 'event.session.created') { const payload = sessionCreatedPayload(corePayload); @@ -983,16 +983,16 @@ export class SessionEventBroadcaster { this.onAgentEvent(sessionId, MAIN_AGENT_ID, { type: 'agent.status.updated', ...snapshot, - } as unknown as Event2); + } as unknown as Event2); }; const disposables: IDisposable[] = [ eventBus.subscribe((event) => { - let projected: Event2 = event; + let projected: Event2 = event; if (event.type === 'agent.status.updated') { const snapshot = readLegacyStatus(handle); if (snapshot !== undefined) { lastLegacyStatus = JSON.stringify(snapshot); - projected = Object.assign({}, event, snapshot) as unknown as Event2; + projected = Object.assign({}, event, snapshot) as unknown as Event2; } } if (handle.id === MAIN_AGENT_ID && event.type === 'context.spliced') { @@ -1005,7 +1005,7 @@ export class SessionEventBroadcaster { return { dispose: () => disposables.forEach((disposable) => disposable.dispose()) }; } - private onAgentEvent(sessionId: string, agentId: string, event: Event2): void { + private onAgentEvent(sessionId: string, agentId: string, event: Event2): void { const state = this.sessions.get(sessionId); if (state === undefined) return; @@ -1245,7 +1245,11 @@ function isVolatileSignal(type: string): boolean { return volatileSignalTypeSet.has(type); } -function legacyTaskEvent(event: Event2, agentId: string, sessionId: string): Event | undefined { +function legacyTaskEvent( + event: Event2, + agentId: string, + sessionId: string, +): Event | undefined { if (event.type !== 'task.started' && event.type !== 'task.terminated') return undefined; const legacyType = event.type === 'task.started' ? 'background.task.started' : 'background.task.terminated'; diff --git a/packages/node-sdk/src/v2/session-wiring.ts b/packages/node-sdk/src/v2/session-wiring.ts index 35ccbbfa158..53fc1e7ac42 100644 --- a/packages/node-sdk/src/v2/session-wiring.ts +++ b/packages/node-sdk/src/v2/session-wiring.ts @@ -270,7 +270,7 @@ export class SessionEventWiring { * two client-facing packages so the core engine stays free of v1 * wire-compatibility concerns. */ -function withStatusSnapshot(agent: IAgentScopeHandle, event: Event2): Event2 { +function withStatusSnapshot(agent: IAgentScopeHandle, event: Event2): Event2 { const profile = agent.accessor.get(IAgentProfileService) as IAgentProfileService | undefined; const usageService = agent.accessor.get(ISessionUsageService) as ISessionUsageService | undefined; const tokenCounting = agent.accessor.get(ISessionTokenCountingService) as @@ -290,5 +290,5 @@ function withStatusSnapshot(agent: IAgentScopeHandle, event: Event2): Event contextTokens, maxContextTokens, model: profile.getModel(), - }) as unknown as Event2; + }) as unknown as Event2; } From 2959f2d9be5aa66e2de71fba4900224feda38b3a Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 00:50:45 +0800 Subject: [PATCH 12/42] fix: surface the real error when agent creation fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The failure path deactivated the agent context before disposing the scope, so the activity view's final dispose-time publish hit the event bus with no active lifecycle context and crashed teardown — masking whatever caused the failure. Dispose first, then deactivate, matching the order the normal close path already uses. Verified end to end: a headless prompt against a mock OpenAI-compatible endpoint now reports its actual configuration error instead of crashing, and completes cleanly under both Node and Bun once configured. --- .../src/session/agentLifecycle/agentLifecycleService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts index 02bcf46d5bd..2eba150522f 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts @@ -192,10 +192,10 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle return handle; } catch (error) { if (this.handles.get(agentId) === handle) this.handles.delete(agentId); - eventBus?.deactivateAgent(agent); try { handle.dispose(); } catch { } + eventBus?.deactivateAgent(agent); this.onDidDisposeEmitter.fire(agent); throw error; } From d5141eee8b42a7fd287c6e1fe135ca61b53b574d Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 01:39:20 +0800 Subject: [PATCH 13/42] test: pin headless prompt error surfacing via e2e Config errors during agent creation must surface their real semantics (a missing max_context_size) rather than being masked. The lifecycle ordering fix is verified manually against the CLI spawn path; this suite locks the SDK-level contract. --- .../e2e/headless-error-surfacing.e2e.test.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 apps/kimi-code/test/e2e/headless-error-surfacing.e2e.test.ts diff --git a/apps/kimi-code/test/e2e/headless-error-surfacing.e2e.test.ts b/apps/kimi-code/test/e2e/headless-error-surfacing.e2e.test.ts new file mode 100644 index 00000000000..1fe1baeb0fa --- /dev/null +++ b/apps/kimi-code/test/e2e/headless-error-surfacing.e2e.test.ts @@ -0,0 +1,79 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { createKimiHarnessV2, type KimiHarness } from '@moonshot-ai/kimi-code-sdk'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { createKimiCodeHostIdentity } from '#/cli/version'; + +const ENABLED = process.env['KIMI_E2E'] === '1'; + +let homeDir: string; +let workDir: string; +let oldHome: string | undefined; +let harness: KimiHarness | undefined; + +beforeEach(async () => { + homeDir = await mkdtemp(join(tmpdir(), 'kimi-cli-prompt-home-')); + workDir = await mkdtemp(join(tmpdir(), 'kimi-cli-prompt-work-')); + oldHome = process.env['KIMI_CODE_HOME']; + process.env['KIMI_CODE_HOME'] = homeDir; +}); + +afterEach(async () => { + if (oldHome === undefined) { + delete process.env['KIMI_CODE_HOME']; + } else { + process.env['KIMI_CODE_HOME'] = oldHome; + } + await harness?.close().catch(() => {}); + harness = undefined; + await rm(homeDir, { recursive: true, force: true }); + await rm(workDir, { recursive: true, force: true }); +}); + +async function writeConfig(modelBody: string): Promise { + await writeFile( + join(homeDir, 'config.toml'), + `defaultProvider = "mock"\ndefaultModel = "mock-model"\n\n[providers.mock]\ntype = "openai"\nbase_url = "http://127.0.0.1:9/v1"\napi_key = "sk-mock"\n\n[models."mock-model"]\nprovider = "mock"\nmodel = "gpt-mock"\n${modelBody}\n`, + 'utf-8', + ); +} + +describe.skipIf(!ENABLED)('headless prompt error surfacing e2e', () => { + it('surfaces the real model configuration error instead of a teardown crash', async () => { + await writeConfig(''); + harness = createKimiHarnessV2({ + homeDir, + identity: createKimiCodeHostIdentity('0.0.0-e2e'), + }); + const session = await harness.createSession({ workDir }); + + const failure = await session.prompt('hi').then( + () => undefined, + (error: unknown) => error as Error, + ); + + expect(failure).toBeInstanceOf(Error); + expect(failure?.message).toContain('max_context_size'); + expect(failure?.message).not.toContain('no active lifecycle context'); + }, 30_000); + + it('accepts a complete model config and reaches the provider endpoint', async () => { + await writeConfig('max_context_size = 100000'); + harness = createKimiHarnessV2({ + homeDir, + identity: createKimiCodeHostIdentity('0.0.0-e2e'), + }); + const session = await harness.createSession({ workDir }); + + const failure = await session.prompt('hi').then( + () => undefined, + (error: unknown) => error as Error, + ); + + expect(failure?.message ?? '').not.toContain('max_context_size'); + expect(failure?.message ?? '').not.toContain('no active lifecycle context'); + }, 30_000); +}); From e0c15dc1ee0a7071973dadb9e09e65fbe2965563 Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 02:10:27 +0800 Subject: [PATCH 14/42] fix: guard wire-dispatch and provider-input paths in klient Replace non-null assertions with the package's existing validation and error conventions: kosong.addProvider validates through the facade's zod schema, and memory-transport stream dispatch raises REQUEST_INVALID when a source fails to start. A node-sdk lazy-init switches to ??=. --- packages/klient/src/core/facade/global.ts | 26 ++++++++++++++++++- .../src/transports/memory/dispatcher.ts | 16 ++++++++++-- packages/node-sdk/src/sdk-rpc-client-v2.ts | 10 +++---- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/klient/src/core/facade/global.ts b/packages/klient/src/core/facade/global.ts index c4ae72fc75e..c60493cf39f 100644 --- a/packages/klient/src/core/facade/global.ts +++ b/packages/klient/src/core/facade/global.ts @@ -6,6 +6,8 @@ * code never sees service tokens, scope routing, or transport details. */ +import { z } from 'zod'; + import type { SessionListQuery, SessionSummary, @@ -38,6 +40,7 @@ import type { IModelCatalog } from '@moonshot-ai/agent-core-v2/kosong/model/cata import type { IProviderDiscoveryService } from '@moonshot-ai/agent-core-v2/app/kosongConfig/discovery'; import type { McpServerConfig } from '../../contract/mcp.js'; +import { KlientValidationError } from '../validation.js'; import type { AnonymousProviderInput, GenerateEvent, GenerateInput, GenerateParams, ProviderInput } from './kosong-types.js'; import type { PluginCommandDef, @@ -94,6 +97,18 @@ export type RefreshProviderModelsOptions = NonNullable< /** String-literal form of the engine's `ConfigTarget` enum, so consumers never import the enum value. */ export type ConfigTargetLiteral = `${ConfigTarget}`; +const providerAuthSchema = z.discriminatedUnion('method', [ + z.object({ method: z.literal('api-key'), apiKey: z.string() }), + z.object({ method: z.literal('oauth') }), +]); + +const providerInputSchema = z.object({ + type: z.string(), + baseUrl: z.string().optional(), + auth: providerAuthSchema, + defaultModel: z.string().optional(), +}); + // --------------------------------------------------------------------------- // Facade interfaces // --------------------------------------------------------------------------- @@ -388,7 +403,16 @@ export function createGlobalFacade(scoped: ScopedCaller, scopedStream: ScopedStr ): Promise => { if (typeof idOrConfig === 'string') { // Named provider — map ProviderInput to ProviderConfig wire shape. - const config = maybeConfig!; + const parsed = providerInputSchema.safeParse(maybeConfig); + if (!parsed.success) { + throw new KlientValidationError( + 'input', + 'kosong.addProvider', + parsed.error.issues, + maybeConfig, + ); + } + const config = parsed.data; const wire: ProviderConfig = { type: config.type, baseUrl: config.baseUrl, diff --git a/packages/klient/src/transports/memory/dispatcher.ts b/packages/klient/src/transports/memory/dispatcher.ts index 8ac0f6c3039..4a3fd3e4029 100644 --- a/packages/klient/src/transports/memory/dispatcher.ts +++ b/packages/klient/src/transports/memory/dispatcher.ts @@ -293,7 +293,13 @@ export function createMemoryDispatcher(root: ScopeLike): MemoryDispatcher { return { async next() { await ensureStarted(); - const result = await source!.next(); + if (source === undefined) { + throw new RPCError( + REQUEST_INVALID, + `stream failed to start: ${service}.${method}`, + ); + } + const result = await source.next(); if (result.done) return { done: true, value: undefined }; return { done: false, value: wireClone(result.value) }; }, @@ -343,7 +349,13 @@ export function createMemoryDispatcher(root: ScopeLike): MemoryDispatcher { return { async next() { await ensureStarted(); - const result = await source!.next(); + if (source === undefined) { + throw new RPCError( + REQUEST_INVALID, + `stream failed to start: ${service}.${method}`, + ); + } + const result = await source.next(); if (result.done) return { done: true, value: undefined }; return { done: false, value: wireClone(result.value) }; }, diff --git a/packages/node-sdk/src/sdk-rpc-client-v2.ts b/packages/node-sdk/src/sdk-rpc-client-v2.ts index 5fa0db52011..f2a22c3cb5c 100644 --- a/packages/node-sdk/src/sdk-rpc-client-v2.ts +++ b/packages/node-sdk/src/sdk-rpc-client-v2.ts @@ -2414,12 +2414,10 @@ export class SDKRpcClientV2 extends SDKRpcClientBase { */ private async globalMcpOAuthService(): Promise { await this.engineAccessor.get(IAgentIdentity).resolved(); - if (this.globalMcpOAuth === undefined) { - this.globalMcpOAuth = new McpOAuthService({ - store: createMcpOAuthStore(this.engineAccessor.get(IAtomicDocumentStore)), - resolveClientName: () => this.resolveMcpClientName(), - }); - } + this.globalMcpOAuth ??= new McpOAuthService({ + store: createMcpOAuthStore(this.engineAccessor.get(IAtomicDocumentStore)), + resolveClientName: () => this.resolveMcpClientName(), + }); return this.globalMcpOAuth; } From 906eb30317bf029e74c6ac655765c59956855c8b Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 14:13:32 +0800 Subject: [PATCH 15/42] refactor: tighten agent-core-v2 typings and hoist notes onto exports Narrow ServiceIdentifier maps to unknown, type Promisify/Promisable via Promise, give provideUnit's overload a precise second-parameter type, and lift implementation notes from inline comments onto the exported symbols they document (kosong error classifiers, FetchURLTool, event dispatcher folds). --- packages/agent-core-v2/src/_base/di/fiber.ts | 66 ++++--- .../src/_base/di/instantiationService.ts | 171 +++++++++--------- .../agent-core-v2/src/_base/di/service.ts | 12 +- .../src/_base/di/serviceCollection.ts | 23 +-- .../src/_base/di/testInstantiationService.ts | 4 +- .../agent-core-v2/src/_base/utils/types.ts | 4 +- .../src/agent/tools/fetch-url/fetchUrlTool.ts | 13 +- .../src/app/feature/featureManagerService.ts | 2 +- .../src/kosong/contract/errors.ts | 18 +- .../src/state/eventDispatcherService.ts | 48 ++--- 10 files changed, 185 insertions(+), 176 deletions(-) diff --git a/packages/agent-core-v2/src/_base/di/fiber.ts b/packages/agent-core-v2/src/_base/di/fiber.ts index aed277c5662..7735c993d40 100644 --- a/packages/agent-core-v2/src/_base/di/fiber.ts +++ b/packages/agent-core-v2/src/_base/di/fiber.ts @@ -1,4 +1,3 @@ -import type { IDisposable } from './lifecycle'; import type { Emitter } from '../event'; import { isPromiseLike, type EffectBody } from '../lifecycle/disposer'; import { Ledger, type LedgerEntry } from '../lifecycle/ledger'; @@ -16,6 +15,7 @@ import { type LiveRef, type ServiceIdentifier, } from './instantiation'; +import type { IDisposable } from './lifecycle'; export enum FiberState { Pending = 0, @@ -38,26 +38,15 @@ export interface RecipeStatics { readonly meta?: Record; } -export type ServiceClassRecipe = - (new (...args: any[]) => unknown) & RecipeStatics; +export type ServiceClassRecipe = (new (...args: any[]) => unknown) & RecipeStatics; -export type ServiceFunctionRecipe = (( - fiber: Fiber, - config?: any, -) => any) & - RecipeStatics; +export type ServiceFunctionRecipe = ((fiber: Fiber, config?: any) => any) & RecipeStatics; export type ServiceObjectRecipe = { - apply( - fiber: Fiber, - config?: any, - ): any; + apply(fiber: Fiber, config?: any): any; } & RecipeStatics; -export type ServiceRecipe = - | ServiceClassRecipe - | ServiceFunctionRecipe - | ServiceObjectRecipe; +export type ServiceRecipe = ServiceClassRecipe | ServiceFunctionRecipe | ServiceObjectRecipe; export interface FiberProvideOptions { readonly config?: unknown; @@ -136,10 +125,7 @@ export function isServiceRecipe(ctor: any): ctor is ServiceClassRecipe { } export function isClassRecipe(recipe: unknown): recipe is ServiceClassRecipe { - return ( - typeof recipe === 'function' && - Object.prototype.hasOwnProperty.call(recipe, 'prototype') - ); + return typeof recipe === 'function' && Object.prototype.hasOwnProperty.call(recipe, 'prototype'); } export type BufferedOp = (runtime: Fiber) => void; @@ -196,12 +182,15 @@ export function setFiberEventResolver(resolver: FiberEventResolver | undefined): _eventResolver = resolver; } -export function bindServiceUnit(instance: UnitInternals & IDisposable, frame: ConstructionFrame): void { +export function bindServiceUnit( + instance: UnitInternals & IDisposable, + frame: ConstructionFrame, +): void { const buffer = instance.takeUnitBuffer(); if (buffer === null) { return; } - const ctor = (instance as any).constructor as ServiceClassRecipe; + const ctor = instance.constructor as ServiceClassRecipe; const runtime = new FiberRuntime( frame.host, instance.unitBook, @@ -312,7 +301,10 @@ export class FiberRuntime implements Fiber { } return this._provideTokenInstance(first, second); } - return this._provideAnonymous(first as ServiceRecipe, second as FiberProvideOptions | undefined); + return this._provideAnonymous( + first as ServiceRecipe, + second as FiberProvideOptions | undefined, + ); } effect(body: EffectBody, label?: string): FiberHandle { @@ -348,7 +340,8 @@ export class FiberRuntime implements Fiber { } else { throw new FiberProtocolError(`unsupported event source for unit '${this.name}'`); } - const label = typeof event === 'string' ? `on:${event}` : `on:${event.constructor?.name ?? 'emitter'}`; + const label = + typeof event === 'string' ? `on:${event}` : `on:${event.constructor?.name ?? 'emitter'}`; const entry = this._book.register(() => { subscription.dispose(); }, label); @@ -435,14 +428,20 @@ export class FiberRuntime implements Fiber { }); } - private _provideAnonymous(recipe: ServiceRecipe, opts: FiberProvideOptions | undefined): FiberHandle { + private _provideAnonymous( + recipe: ServiceRecipe, + opts: FiberProvideOptions | undefined, + ): FiberHandle { if (isClassRecipe(recipe)) { return this._provideAnonymousClass(recipe, opts); } return this._provideFunction(recipe, opts); } - private _provideAnonymousClass(recipe: ServiceClassRecipe, opts: FiberProvideOptions | undefined): FiberHandle { + private _provideAnonymousClass( + recipe: ServiceClassRecipe, + opts: FiberProvideOptions | undefined, + ): FiberHandle { const name = recipeName(recipe); let config = validateConfig(recipe.Config, opts?.config, name); let state = FiberState.Activating; @@ -465,7 +464,9 @@ export class FiberRuntime implements Fiber { failure = error; throw error; } - for (const dependency of _util.getInstanceDependencies(recipe as unknown as _util.DI_TARGET_OBJ)) { + for (const dependency of _util.getInstanceDependencies( + recipe as unknown as _util.DI_TARGET_OBJ, + )) { this._host.recordInstanceEdge(this._edgeNode, dependency.id); } return new BasicFiberHandle({ @@ -494,7 +495,10 @@ export class FiberRuntime implements Fiber { }); } - private _provideFunction(recipe: ServiceFunctionRecipe | ServiceObjectRecipe, opts: FiberProvideOptions | undefined): FiberHandle { + private _provideFunction( + recipe: ServiceFunctionRecipe | ServiceObjectRecipe, + opts: FiberProvideOptions | undefined, + ): FiberHandle { const name = recipeName(recipe); const config = validateConfig(recipe.Config, opts?.config, name); const book = new Ledger(`unit:${name}`); @@ -604,7 +608,11 @@ class BasicFiberHandle implements FiberHandle { onfulfilled?: ((value: FiberHandle) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): PromiseLike { - return thenSettle(this._parts.whenActive().then(() => settledView(this)), onfulfilled, onrejected); + return thenSettle( + this._parts.whenActive().then(() => settledView(this)), + onfulfilled, + onrejected, + ); } } diff --git a/packages/agent-core-v2/src/_base/di/instantiationService.ts b/packages/agent-core-v2/src/_base/di/instantiationService.ts index 32bab205248..7689b471ab9 100644 --- a/packages/agent-core-v2/src/_base/di/instantiationService.ts +++ b/packages/agent-core-v2/src/_base/di/instantiationService.ts @@ -1,4 +1,7 @@ -import { SyncDescriptor } from './descriptors'; +import { onUnexpectedError } from '../errors/unexpectedError'; +import { Emitter } from '../event'; +import type { Disposer } from '../lifecycle/disposer'; +import { Ledger, type LedgerEntry } from '../lifecycle/ledger'; import { CascadeEngine, CascadeTree, type CascadeChange, type CascadeHost } from './cascadeEngine'; import { CollectionStore, @@ -7,6 +10,7 @@ import { type CollectionViewImpl, } from './collection'; import { DependencyGraph } from './dependencyGraph'; +import { SyncDescriptor } from './descriptors'; import { CascadeConflictError, CyclicDependencyError } from './errors'; import { bindServiceUnit, @@ -31,10 +35,6 @@ import { type ServicesAccessor, } from './instantiation'; import { isDisposable, type DisposableStore, type IDisposable } from './lifecycle'; -import { onUnexpectedError } from '../errors/unexpectedError'; -import { Emitter } from '../event'; -import { Ledger, type LedgerEntry } from '../lifecycle/ledger'; -import type { Disposer } from '../lifecycle/disposer'; import { ServiceCollection } from './serviceCollection'; const enum TraceType { @@ -47,13 +47,17 @@ const enum TraceType { export class Trace { static readonly all = new Set(); - private static readonly _None = new class extends Trace { - constructor() { super(TraceType.None, null); } - override stop() { } - override branch() { return this; } - }; + private static readonly _None = new (class extends Trace { + constructor() { + super(TraceType.None, null); + } + override stop() {} + override branch() { + return this; + } + })(); - static traceInvocation(_enableTracing: boolean, fn: any): Trace { + static traceInvocation(_enableTracing: boolean, fn: Function): Trace { return !_enableTracing ? Trace._None : new Trace( @@ -62,7 +66,7 @@ export class Trace { ); } - static traceCreation(_enableTracing: boolean, ctor: any): Trace { + static traceCreation(_enableTracing: boolean, ctor: Function): Trace { return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name); } @@ -72,8 +76,8 @@ export class Trace { private constructor( readonly type: TraceType, - readonly name: string | null - ) { } + readonly name: string | null, + ) {} branch(id: ServiceIdentifier, first: boolean): Trace { const child = new Trace(TraceType.Branch, id.toString()); @@ -115,7 +119,6 @@ export class Trace { Trace.all.add(lines.join('\n')); } } - } export class InstantiationService implements IInstantiationService { @@ -140,7 +143,7 @@ export class InstantiationService implements IInstantiationService { private readonly _instanceEntries = new Map(); private readonly _provideEntries = new Map< - ServiceIdentifier, + ServiceIdentifier, { readonly entry: LedgerEntry; readonly core: TokenProvideCore } >(); @@ -148,16 +151,13 @@ export class InstantiationService implements IInstantiationService { protected readonly _children = new Set(); - private readonly _inProgress: ServiceIdentifier[] = []; + private readonly _inProgress: ServiceIdentifier[] = []; - private readonly _activeInstantiations = new Set>(); + private readonly _activeInstantiations = new Set>(); private readonly _collectionStore: CollectionStore; - private readonly _collectionViews = new Map< - CollectionToken, - CollectionViewImpl - >(); + private readonly _collectionViews = new Map, CollectionViewImpl>(); debugLabel: string | undefined; @@ -172,7 +172,7 @@ export class InstantiationService implements IInstantiationService { protected readonly _enableTracing: boolean = false, ) { this._parent = parent; - this._globalGraph = _enableTracing ? parent?._globalGraph ?? new Graph(e => e) : undefined; + this._globalGraph = _enableTracing ? (parent?._globalGraph ?? new Graph((e) => e)) : undefined; this._services.set(IInstantiationServiceDecorator, this); this._tree = parent?._tree ?? new CascadeTree(new DependencyGraph()); this._collectionStore = @@ -188,10 +188,7 @@ export class InstantiationService implements IInstantiationService { materialize: (token) => { this._cascadeResolving = true; try { - return this._getOrCreateServiceInstance( - token, - Trace.traceCreation(false, CascadeEngine), - ); + return this._getOrCreateServiceInstance(token, Trace.traceCreation(false, CascadeEngine)); } finally { this._cascadeResolving = false; } @@ -227,7 +224,7 @@ export class InstantiationService implements IInstantiationService { return (this._parent?.cascadeDepth ?? -1) + 1; } - private _ownerOf(id: ServiceIdentifier): InstantiationService | undefined { + private _ownerOf(id: ServiceIdentifier): InstantiationService | undefined { if (this._services.has(id)) { return this; } @@ -270,9 +267,12 @@ export class InstantiationService implements IInstantiationService { ): ProvideHandle { this._assertNotDisposed(); const core = this._provideCore(id, instanceOrDescriptor, options); - const entry = this._ledger.register(() => { - void core.dispose(); - }, `provide:${String(id)}`); + const entry = this._ledger.register( + () => { + void core.dispose(); + }, + `provide:${String(id)}`, + ); this._provideEntries.set(id, { entry, core }); return { get uid(): number { @@ -304,11 +304,14 @@ export class InstantiationService implements IInstantiationService { if (uid === undefined) { continue; } - const entry = this._ledger.register(() => { - if (this._services.uidOf(id) === uid) { - void this._unprovideCore(id); - } - }, `provide:${String(id)}`); + const entry = this._ledger.register( + () => { + if (this._services.uidOf(id) === uid) { + void this._unprovideCore(id); + } + }, + `provide:${String(id)}`, + ); this._provideEntries.set(id, { entry, core: { @@ -417,7 +420,7 @@ export class InstantiationService implements IInstantiationService { void this._unprovideCore(id); } - private _releaseProvideEntry(id: ServiceIdentifier): void { + private _releaseProvideEntry(id: ServiceIdentifier): void { const prev = this._provideEntries.get(id); if (prev !== undefined) { this._provideEntries.delete(id); @@ -567,12 +570,15 @@ export class InstantiationService implements IInstantiationService { change.fire(); }), ); - this._ledger.register(() => { - for (const subscription of subscriptions) { - subscription.dispose(); - } - change.dispose(); - }, `ref:${String(id)}`); + this._ledger.register( + () => { + for (const subscription of subscriptions) { + subscription.dispose(); + } + change.dispose(); + }, + `ref:${String(id)}`, + ); const current = (): T | undefined => this._materializedInstanceOf(id); const ref: LiveRef = { get current(): T | undefined { @@ -594,11 +600,7 @@ export class InstantiationService implements IInstantiationService { private _scopePath(): string { const labels: string[] = [this.debugLabel ?? `#${this._tree.seqOf(this)}`]; - for ( - let c: InstantiationService | undefined = this._parent; - c !== undefined; - c = c._parent - ) { + for (let c: InstantiationService | undefined = this._parent; c !== undefined; c = c._parent) { labels.unshift(c.debugLabel ?? `#${this._tree.seqOf(c)}`); } return labels.join('/'); @@ -631,9 +633,7 @@ export class InstantiationService implements IInstantiationService { createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService { this._assertNotDisposed(); if (!(services instanceof ServiceCollection)) { - throw new TypeError( - 'createChild requires a ServiceCollection instance (got something else)', - ); + throw new TypeError('createChild requires a ServiceCollection instance (got something else)'); } const child = this._createChildService(services); this._children.add(child); @@ -676,29 +676,38 @@ export class InstantiationService implements IInstantiationService { } } - private _createInstance(ctor: any, args: unknown[], _trace: Trace, unit?: { - id?: ServiceIdentifier; - config?: unknown; - }): T { - const serviceDependencies = _util.getServiceDependencies(ctor).toSorted((a, b) => a.index - b.index); + private _createInstance( + ctor: any, + args: unknown[], + _trace: Trace, + unit?: { + id?: ServiceIdentifier; + config?: unknown; + }, + ): T { + const serviceDependencies = _util + .getServiceDependencies(ctor) + .toSorted((a, b) => a.index - b.index); const serviceArgs: unknown[] = []; for (const dependency of serviceDependencies) { const kind = dependency.kind ?? 'instance'; if (kind === 'collection') { - serviceArgs.push(this._collectionView(dependency.id as unknown as CollectionToken)); + serviceArgs.push( + this._collectionView(dependency.id as unknown as CollectionToken), + ); continue; } if (kind === 'ref') { serviceArgs.push(this._liveRef(dependency.id)); continue; } - const service = this._getOrCreateServiceInstance(dependency.id, _trace); - if (!service) { - this._throwIfStrict( - `[createInstance] ${ctor.name} depends on UNKNOWN service ${String(dependency.id)}.`, - false, - ); - } + const service = this._getOrCreateServiceInstance(dependency.id, _trace); + if (!service) { + this._throwIfStrict( + `[createInstance] ${ctor.name} depends on UNKNOWN service ${String(dependency.id)}.`, + false, + ); + } serviceArgs.push(service); } @@ -789,7 +798,7 @@ export class InstantiationService implements IInstantiationService { _trace: Trace, ): T { type Triple = { id: ServiceIdentifier; desc: SyncDescriptor; _trace: Trace }; - const graph = new Graph(data => data.id.toString()); + const graph = new Graph((data) => data.id.toString()); let cycleCount = 0; const stack: Triple[] = [{ id, desc, _trace }]; @@ -868,12 +877,7 @@ export class InstantiationService implements IInstantiationService { return this._createServiceInstance(id, ctor, args, _trace); } if (this._parent) { - return this._parent._createServiceInstanceWithOwner( - id, - ctor, - args, - _trace, - ); + return this._parent._createServiceInstanceWithOwner(id, ctor, args, _trace); } throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`); } @@ -914,15 +918,18 @@ export class InstantiationService implements IInstantiationService { ); } } - const entry = this._ledger.register(() => { - this._instanceEntries.delete(result); - this.dependencyGraph.removeInstance(result as object); - if (isDisposable(result)) { - const out = result.dispose() as unknown as void | Promise; - return out; - } - return undefined; - }, `service:${String(id)}`); + const entry = this._ledger.register( + () => { + this._instanceEntries.delete(result); + this.dependencyGraph.removeInstance(result as object); + if (isDisposable(result)) { + const out = result.dispose() as unknown as void | Promise; + return out; + } + return undefined; + }, + `service:${String(id)}`, + ); this._instanceEntries.set(result, entry); this.cascade.observedMaterialization(id); return result; @@ -940,9 +947,7 @@ export class InstantiationService implements IInstantiationService { } else if (this._parent) { this._parent._setCreatedServiceInstance(id, instance); } else { - throw new Error( - `illegal state - setting UNKNOWN service instance '${String(id)}'`, - ); + throw new Error(`illegal state - setting UNKNOWN service instance '${String(id)}'`); } } diff --git a/packages/agent-core-v2/src/_base/di/service.ts b/packages/agent-core-v2/src/_base/di/service.ts index 66d61583ad1..1a588818f21 100644 --- a/packages/agent-core-v2/src/_base/di/service.ts +++ b/packages/agent-core-v2/src/_base/di/service.ts @@ -34,7 +34,7 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals const frame = currentConstruction(); if ( frame !== undefined && - frame.ctor === (new.target as unknown as new (...args: any[]) => any) + frame.ctor === (new.target as unknown as new (...args: any[]) => unknown) ) { this.__unitBuffer = []; this.config = frame.config; @@ -42,7 +42,7 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals this.__unitBuffer = null; this.config = undefined; } - this.name = (this.constructor as any).name || 'anonymous'; + this.name = this.constructor.name || 'anonymous'; } provide( @@ -53,11 +53,7 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals provide(id: ServiceIdentifier, instance: T): FiberHandle; provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle; provide(token: CollectionToken, value: T): FiberHandle; - provide( - first: any, - second?: any, - third?: FiberProvideOptions, - ): FiberHandle { + provide(first: any, second?: any, third?: FiberProvideOptions): FiberHandle { if (this.__unitBuffer !== null) { const pending = new PendingFiberHandle(this._pendingName(first)); this.__unitBuffer.push((runtime) => { @@ -127,7 +123,7 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals return this.__unitRuntime; } - private _pendingName(first: any): string { + private _pendingName(first: unknown): string { if (typeof first === 'function') { return (first as RecipeStatics).name ?? String(first); } diff --git a/packages/agent-core-v2/src/_base/di/serviceCollection.ts b/packages/agent-core-v2/src/_base/di/serviceCollection.ts index ba8550eb6ba..a1345061a00 100644 --- a/packages/agent-core-v2/src/_base/di/serviceCollection.ts +++ b/packages/agent-core-v2/src/_base/di/serviceCollection.ts @@ -16,16 +16,14 @@ export interface AvailabilityChange { } export class ServiceCollection { - private readonly _entries = new Map, ServiceCollectionEntry>(); - private readonly _emitters = new Map< - ServiceIdentifier, - Emitter + private readonly _entries = new Map< + ServiceIdentifier, + ServiceCollectionEntry >(); + private readonly _emitters = new Map, Emitter>(); private _nextUid = 0; - constructor( - ...entries: ReadonlyArray, unknown]> - ) { + constructor(...entries: ReadonlyArray, unknown]>) { for (const [id, value] of entries) { this.set(id, value); } @@ -117,13 +115,10 @@ export class ServiceCollection { return this._entries.get(id)?.value as T | SyncDescriptor | undefined; } - forEach( - callback: ( - id: ServiceIdentifier, - value: unknown, - ) => void, - ): void { - this._entries.forEach((entry, id) => { callback(id, entry.value); }); + forEach(callback: (id: ServiceIdentifier, value: unknown) => void): void { + this._entries.forEach((entry, id) => { + callback(id, entry.value); + }); } dispose(): void { diff --git a/packages/agent-core-v2/src/_base/di/testInstantiationService.ts b/packages/agent-core-v2/src/_base/di/testInstantiationService.ts index a398a030a19..dd2a07ddc8f 100644 --- a/packages/agent-core-v2/src/_base/di/testInstantiationService.ts +++ b/packages/agent-core-v2/src/_base/di/testInstantiationService.ts @@ -239,7 +239,7 @@ export class TestInstantiationService return Boolean(service?.sinonOptions?.[key]); } - private _isServiceMock(arg: any): arg is IServiceMock { + private _isServiceMock(arg: unknown): arg is IServiceMock { return typeof arg === 'object' && arg !== null && 'id' in arg; } @@ -283,7 +283,7 @@ export function createServices( options: CreateServicesOptions = {}, ): TestInstantiationService { const serviceCollection = new ServiceCollection(); - const instanceIds = new Set>(); + const instanceIds = new Set>(); const register = ( id: ServiceIdentifier, diff --git a/packages/agent-core-v2/src/_base/utils/types.ts b/packages/agent-core-v2/src/_base/utils/types.ts index 45a0d1c9dac..9b99171397e 100644 --- a/packages/agent-core-v2/src/_base/utils/types.ts +++ b/packages/agent-core-v2/src/_base/utils/types.ts @@ -1,11 +1,11 @@ -export type Promisify = [T] extends [Promise] ? T : Promise; +export type Promisify = [T] extends [Promise] ? T : Promise; export type PromisifyMethods = { [K in keyof T]: T[K] extends (...args: infer Args) => infer Return ? (...args: Args) => Promisify : never; }; -export type Promisable = [T] extends [Promise] ? T | Awaited : T | Promise; +export type Promisable = [T] extends [Promise] ? T | Awaited : T | Promise; export type PromisableMethods = { [K in keyof T]: T[K] extends (...args: infer Args) => infer Return ? (...args: Args) => Promisable diff --git a/packages/agent-core-v2/src/agent/tools/fetch-url/fetchUrlTool.ts b/packages/agent-core-v2/src/agent/tools/fetch-url/fetchUrlTool.ts index 9de99521ade..bf4f1f9b870 100644 --- a/packages/agent-core-v2/src/agent/tools/fetch-url/fetchUrlTool.ts +++ b/packages/agent-core-v2/src/agent/tools/fetch-url/fetchUrlTool.ts @@ -15,6 +15,13 @@ import { HttpFetchError } from '#/app/web/tools/fetch-url-types'; import { FetchURLInputSchema, IFetchURLTool, type FetchURLInput } from './fetch-url'; import DESCRIPTION from './fetch-url.md?raw'; +/** + * Built-in web-fetching tool whose network errors stay diagnosable from the + * output alone: undici reports every transport failure as a bare + * `TypeError: fetch failed`, with the actionable detail (DNS, connect + * timeout, TLS, reset) on the `cause` chain, which is flattened into the + * returned error text. + */ export class FetchURLTool implements IFetchURLTool { declare readonly _serviceBrand: undefined; readonly name = 'FetchURL' as const; @@ -81,12 +88,6 @@ export class FetchURLTool implements IFetchURLTool { } } -/** - * Undici reports every transport failure as a bare `TypeError: fetch failed`; - * the actionable detail (DNS, connect timeout, TLS, reset) lives on the - * `cause` chain. Flatten it so the failure is diagnosable from the tool - * output alone. - */ function describeErrorCause(error: unknown): string { const parts: string[] = []; let current: unknown = error; diff --git a/packages/agent-core-v2/src/app/feature/featureManagerService.ts b/packages/agent-core-v2/src/app/feature/featureManagerService.ts index f44514976cc..9969ab02f83 100644 --- a/packages/agent-core-v2/src/app/feature/featureManagerService.ts +++ b/packages/agent-core-v2/src/app/feature/featureManagerService.ts @@ -46,7 +46,7 @@ export class FeatureManagerService extends Service implements IFeatureManager { ): FiberHandle; provideUnit( first: ServiceRecipe | ServiceIdentifier, - second?: any, + second?: ServiceClassRecipe | FiberProvideOptions, third?: FiberProvideOptions, ): FiberHandle { const handle = isServiceIdentifier(first) diff --git a/packages/agent-core-v2/src/kosong/contract/errors.ts b/packages/agent-core-v2/src/kosong/contract/errors.ts index 800e9eb9159..97239e9e8ec 100644 --- a/packages/agent-core-v2/src/kosong/contract/errors.ts +++ b/packages/agent-core-v2/src/kosong/contract/errors.ts @@ -432,20 +432,22 @@ export function isRecoverableRequestStructureError(error: unknown): boolean { return STRUCTURAL_REQUEST_MESSAGE_PATTERNS.some((pattern) => pattern.test(lowerMessage)); } -// Strict OpenAI-compatible endpoints reject the optional `prompt_cache_key` -// affinity hint as an unknown request field (observed verbatim from a -// DeepSeek-backed relay as "400 未知请求字段:prompt_cache_key"). The rejection -// is deterministic per endpoint, and the only recovery is to resend without -// the field, so it is classified narrowly: a 400/422 that names the field -// AND words the rejection as an unknown/unrecognized one. Generic -// "invalid parameter" wordings without a named field stay unclassified — -// there is nothing safe to drop for those. const UNKNOWN_CACHE_KEY_FIELD_MESSAGE_PATTERNS = [ /未知请求字段/, /unknown (?:request )?(?:field|parameter|argument)/, /unrecognized (?:request )?(?:field|parameter|keyword argument)/, ] as const; +/** + * Whether a strict OpenAI-compatible endpoint rejected the optional + * `prompt_cache_key` affinity hint as an unknown request field (observed + * verbatim from a DeepSeek-backed relay as "400 未知请求字段:prompt_cache_key"). + * The rejection is deterministic per endpoint and the only recovery is to + * resend without the field, so classification stays narrow: a 400/422 that + * names the field AND words the rejection as an unknown/unrecognized one. + * Generic "invalid parameter" wordings without a named field stay + * unclassified — there is nothing safe to drop for those. + */ export function isUnknownCacheKeyFieldError(error: unknown): boolean { if (!(error instanceof APIStatusError)) return false; if (error instanceof APIContextOverflowError) return false; diff --git a/packages/agent-core-v2/src/state/eventDispatcherService.ts b/packages/agent-core-v2/src/state/eventDispatcherService.ts index 09a648942c0..f9c9e2b2991 100644 --- a/packages/agent-core-v2/src/state/eventDispatcherService.ts +++ b/packages/agent-core-v2/src/state/eventDispatcherService.ts @@ -1,11 +1,10 @@ import { applyPatches, produceWithPatches } from 'immer'; -import { BugIndicatingError } from '#/_base/errors/errors'; -import { onUnexpectedError } from '#/_base/errors/unexpectedError'; -import { Service } from '#/_base/di/service'; import { type CollectionView } from '#/_base/di/collection'; -import { LifecycleScope } from '#/app/scopes'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; +import { Service } from '#/_base/di/service'; +import { BugIndicatingError } from '#/_base/errors/errors'; +import { onUnexpectedError } from '#/_base/errors/unexpectedError'; import { AgentSpaceImpl, type AgentSpaceHost } from '#/agent/agentContext/agentSpace'; import { IAgentBlobService } from '#/agent/blob/agentBlobService'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; @@ -17,11 +16,12 @@ import { type Event2Class, } from '#/app/event/event2'; import { IEventBus } from '#/app/event/eventBus'; -import type { ContentPart } from '#/kosong/contract/message'; +import { LifecycleScope } from '#/app/scopes'; import { OrderedHookSlot } from '#/hooks'; -import { IWireService } from '#/wire/wire'; +import type { ContentPart } from '#/kosong/contract/message'; import { WireError, WireErrors } from '#/wire/errors'; import type { PartsTransformer } from '#/wire/record'; +import { IWireService } from '#/wire/wire'; import { AgentModelContribution, @@ -29,8 +29,8 @@ import { type AgentModel, type AgentModelDefinition, } from './agentModel'; -import { IEventDispatcher, type ModelCheckpointDepth } from './eventDispatcher'; import { StateError, StateErrors } from './errors'; +import { IEventDispatcher, type ModelCheckpointDepth } from './eventDispatcher'; import { expandedModelAppliers, keepsUndoCheckpoints, @@ -50,7 +50,10 @@ const MAX_DRAIN = 100; const HISTORY_TAIL = 500; export class CycleError extends StateError { - constructor(readonly depth: number, readonly eventTypes: readonly string[]) { + constructor( + readonly depth: number, + readonly eventTypes: readonly string[], + ) { super( StateErrors.codes.STATE_CYCLE, `Event dispatch cascade exceeded MAX_DRAIN (${depth}); possible event cycle`, @@ -76,7 +79,7 @@ interface PreparedFold { readonly key: ReplayableStateKey; readonly meta: StateMeta; readonly ctx: FoldContextImpl; - readonly next: any; + readonly next: unknown; readonly patches: PatchEntry['patches']; readonly inversePatches: PatchEntry['inversePatches']; } @@ -92,7 +95,7 @@ interface ModelAttachment { interface PreparedModel { readonly attachment: ModelAttachment; readonly ctx: FoldContextImpl; - readonly next: any; + readonly next: unknown; readonly patches: PatchEntry['patches']; readonly inversePatches: PatchEntry['inversePatches']; } @@ -154,8 +157,7 @@ export class EventDispatcherService extends Service implements IEventDispatcher private readonly attachments = new Map, ModelAttachment>(); private readonly spaceHost: AgentSpaceHost = { - isActiveModelDefinition: (definition) => - this.activeModelDefs.get(definition.id) === definition, + isActiveModelDefinition: (definition) => this.activeModelDefs.get(definition.id) === definition, registerModel: (definition, model) => this.registerModel(definition, model), dispatchModelEvent: (event) => this.dispatch(event), readLegacyState: (key) => this.agentState.get(key), @@ -281,10 +283,7 @@ export class EventDispatcherService extends Service implements IEventDispatcher } } - private registerModel( - definition: AgentModelDefinition, - model: AgentModel, - ): void { + private registerModel(definition: AgentModelDefinition, model: AgentModel): void { if (this.attachments.has(definition)) return; const domainAppliers = new Map, EventApplier>(); for (const [cls, applier] of model._appliersTable()) { @@ -426,9 +425,9 @@ export class EventDispatcherService extends Service implements IEventDispatcher for (const { key, fold } of folds) { const meta = this.ensureMeta(key); const ctx = new FoldContextImpl(this, silent); - const [next, patches, inversePatches] = produceWithPatches( + const [next, patches, inversePatches] = produceWithPatches( this.agentState.get(key), - (draft: any) => fold(draft, event, ctx) as any, + (draft) => fold(draft, event, ctx), ); if (ctx.pendingUndo !== undefined && patches.length > 0) { throw new BugIndicatingError( @@ -447,9 +446,9 @@ export class EventDispatcherService extends Service implements IEventDispatcher const applier = attachment.appliers.get(event.constructor as Event2Class); if (applier === undefined) continue; const ctx = new FoldContextImpl(this, silent); - const [next, patches, inversePatches] = produceWithPatches( + const [next, patches, inversePatches] = produceWithPatches( attachment.model._state(), - (draft: any) => { + (draft) => { attachment.model._enterWindow(draft, ctx); let windowResult: ReturnType['_exitWindow']>; try { @@ -502,7 +501,7 @@ export class EventDispatcherService extends Service implements IEventDispatcher meta: StateMeta, ctx: FoldContextImpl, event: Event2, - next: any, + next: unknown, patches: PatchEntry['patches'], inversePatches: PatchEntry['inversePatches'], ): void { @@ -539,7 +538,7 @@ export class EventDispatcherService extends Service implements IEventDispatcher attachment: ModelAttachment, ctx: FoldContextImpl, event: Event2, - next: any, + next: unknown, patches: PatchEntry['patches'], inversePatches: PatchEntry['inversePatches'], ): void { @@ -703,7 +702,10 @@ export class EventDispatcherService extends Service implements IEventDispatcher for (const key of this.folded.states) { const codec = key.replayable.blobs; if (codec?.rehydrate === undefined) continue; - this.agentState.set(key, Object.freeze(await codec.rehydrate(this.agentState.get(key), transform))); + this.agentState.set( + key, + Object.freeze(await codec.rehydrate(this.agentState.get(key), transform)), + ); } } From fa7503867c5fe0705e72602bdb6f5927e7252c2d Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 14:14:32 +0800 Subject: [PATCH 16/42] fix: stream-decode host terminal output on the Bun runtime Bun.Terminal delivers raw bytes, so decode them with a streaming TextDecoder instead of assuming chunk-aligned UTF-8; flush the decoder before the exit event so a trailing truncated sequence surfaces as U+FFFD like node-pty would report it, and stop emitting empty strings for chunks that only carry part of a multi-byte character. --- .changeset/bun-terminal-utf8.md | 5 + .../node-local/hostTerminalService.ts | 12 +- .../session/terminal/terminalService.test.ts | 132 +++++++++++++++++- 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 .changeset/bun-terminal-utf8.md diff --git a/.changeset/bun-terminal-utf8.md b/.changeset/bun-terminal-utf8.md new file mode 100644 index 00000000000..3a4e17fbb76 --- /dev/null +++ b/.changeset/bun-terminal-utf8.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix garbled multi-byte characters in terminal session output when running on the Bun runtime. diff --git a/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts b/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts index f228366c0ed..4a3d46905b2 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/hostTerminalService.ts @@ -20,6 +20,7 @@ interface BunSubprocessWithTerminal { } interface BunGlobalLike { + readonly Terminal?: unknown; spawn( command: readonly string[], options: { @@ -36,7 +37,9 @@ interface BunGlobalLike { } function currentBun(): BunGlobalLike | undefined { - return (globalThis as unknown as { Bun?: BunGlobalLike }).Bun; + const bun = (globalThis as unknown as { Bun?: BunGlobalLike }).Bun; + if (bun === undefined || typeof bun.Terminal !== 'function') return undefined; + return bun; } export class HostTerminalService extends Service implements IHostTerminalService { @@ -61,10 +64,15 @@ export class HostTerminalService extends Service implements IHostTerminalService name: 'xterm-256color', cols: options.cols, rows: options.rows, - data: (_terminal, data) => dataEmitter.fire(decoder.decode(data)), + data: (_terminal, data) => { + const text = decoder.decode(data, { stream: true }); + if (text.length > 0) dataEmitter.fire(text); + }, }, }); void proc.exited.then((exitCode) => { + const rest = decoder.decode(); + if (rest.length > 0) dataEmitter.fire(rest); exitEmitter.fire({ exitCode }); dataEmitter.dispose(); exitEmitter.dispose(); diff --git a/packages/agent-core-v2/test/session/terminal/terminalService.test.ts b/packages/agent-core-v2/test/session/terminal/terminalService.test.ts index 0ac42f89922..97aa06abc03 100644 --- a/packages/agent-core-v2/test/session/terminal/terminalService.test.ts +++ b/packages/agent-core-v2/test/session/terminal/terminalService.test.ts @@ -1,5 +1,6 @@ import { resolve } from 'node:path'; +import type { IPty } from 'node-pty'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; @@ -360,7 +361,7 @@ describe('HostTerminalService (App scope)', () => { resize: vi.fn(), kill: vi.fn(), }; - vi.mocked(spawn).mockReturnValue(mockPty as unknown as import('node-pty').IPty); + vi.mocked(spawn).mockReturnValue(mockPty as unknown as IPty); const svc = ix.get(IHostTerminalService); const proc = await svc.spawn({ cwd: '/ws', shell: '/bin/sh', cols: 80, rows: 24 }); @@ -397,3 +398,132 @@ describe('HostTerminalService (App scope)', () => { expect(mockPty.kill).toHaveBeenCalled(); }); }); + +interface FakeBunSpawnOptions { + cwd?: string; + env?: Record; + terminal: { + name?: string; + cols?: number; + rows?: number; + data(terminal: unknown, data: Uint8Array): void; + }; +} + +interface FakeBunTerminalHandle { + write: ReturnType; + resize: ReturnType; + close: ReturnType; +} + +describe('HostTerminalService bun runtime', () => { + const originalBun = (globalThis as unknown as { Bun?: unknown }).Bun; + let disposables: DisposableStore; + let ix: TestInstantiationService; + + function installFakeBun(bun: unknown): void { + (globalThis as unknown as { Bun?: unknown }).Bun = bun; + } + + beforeEach(() => { + disposables = new DisposableStore(); + ix = createServices(disposables, { + additionalServices: (reg) => { + reg.define(IHostTerminalService, HostTerminalService); + }, + }); + }); + afterEach(() => { + installFakeBun(originalBun); + disposables.dispose(); + }); + + it('routes to bun.spawn when Bun.Terminal is available and streams UTF-8 across chunks', async () => { + const handle: FakeBunTerminalHandle = { write: vi.fn(), resize: vi.fn(), close: vi.fn() }; + let fireData: ((data: Uint8Array) => void) | undefined; + let spawnOptions: FakeBunSpawnOptions | undefined; + installFakeBun({ + Terminal: function bunTerminal() {}, + spawn: (_command: readonly string[], options: FakeBunSpawnOptions) => { + spawnOptions = options; + fireData = (data) => options.terminal.data(handle, data); + return { terminal: handle, exited: new Promise(() => {}), kill: vi.fn() }; + }, + }); + + const svc = ix.get(IHostTerminalService); + const proc = await svc.spawn({ cwd: '/ws', shell: '/bin/bash', cols: 100, rows: 30 }); + + expect(spawnOptions?.cwd).toBe('/ws'); + expect(spawnOptions?.terminal.name).toBe('xterm-256color'); + expect(spawnOptions?.terminal.cols).toBe(100); + expect(spawnOptions?.terminal.rows).toBe(30); + + const chunks: string[] = []; + proc.onProcessData((data) => chunks.push(data)); + const encoded = new TextEncoder().encode('héllo'); + fireData!(encoded.slice(0, 2)); + fireData!(encoded.slice(2)); + expect(chunks.join('')).toBe('héllo'); + + proc.write('ls\n'); + proc.resize(120, 40); + proc.kill(); + expect(handle.write).toHaveBeenCalledWith('ls\n'); + expect(handle.resize).toHaveBeenCalledWith(120, 40); + expect(handle.close).toHaveBeenCalled(); + }); + + it('falls back to node-pty when Bun exists but lacks Bun.Terminal', async () => { + const bunSpawn = vi.fn(); + installFakeBun({ spawn: bunSpawn }); + const { spawn } = await import('node-pty'); + vi.mocked(spawn).mockReturnValue({} as unknown as IPty); + + const svc = ix.get(IHostTerminalService); + await svc.spawn({ cwd: '/ws', shell: '/bin/sh', cols: 80, rows: 24 }); + + expect(bunSpawn).not.toHaveBeenCalled(); + expect(spawn).toHaveBeenCalledWith('/bin/sh', [], { + name: 'xterm-256color', + cwd: '/ws', + cols: 80, + rows: 24, + env: process.env, + }); + }); + + it('flushes a trailing truncated sequence before the exit event', async () => { + const handle: FakeBunTerminalHandle = { write: vi.fn(), resize: vi.fn(), close: vi.fn() }; + let fireData: ((data: Uint8Array) => void) | undefined; + let resolveExit: ((code: number | null) => void) | undefined; + installFakeBun({ + Terminal: function bunTerminal() {}, + spawn: (_command: readonly string[], options: FakeBunSpawnOptions) => { + fireData = (data) => options.terminal.data(handle, data); + return { + terminal: handle, + exited: new Promise((resolve) => { + resolveExit = resolve; + }), + kill: () => resolveExit?.(0), + }; + }, + }); + + const svc = ix.get(IHostTerminalService); + const proc = await svc.spawn({ cwd: '/ws', shell: '/bin/bash', cols: 80, rows: 24 }); + + const events: Array<{ kind: 'data' | 'exit'; text?: string }> = []; + proc.onProcessData((data) => events.push({ kind: 'data', text: data })); + proc.onProcessExit((event) => events.push({ kind: 'exit' })); + fireData!(new TextEncoder().encode('é').slice(0, 1)); + proc.kill(); + + await vi.waitFor(() => expect(events.at(-1)?.kind).toBe('exit')); + expect(events).toEqual([ + { kind: 'data', text: '\uFFFD' }, + { kind: 'exit' }, + ]); + }); +}); From 5af1cfddb1f176f5e6dc07253e1f532055b14a37 Mon Sep 17 00:00:00 2001 From: luozihao Date: Mon, 24 Aug 2026 14:14:46 +0800 Subject: [PATCH 17/42] docs: spell out the AgentSwarm resume_agent_ids record shape Describe resume_agent_ids as the flat agent_id-to-prompt record it is, warn against arrays and {{item}} keys, and refresh the toolset hash snapshots plus pinned token counts that the longer description shifts. --- .../features/swarm/tools/agent-swarm/agent-swarm.md | 2 +- .../features/swarm/tools/agent-swarm/agent-swarm.ts | 2 +- .../test/agent/fullCompaction/fullCompaction.test.ts | 8 ++++---- packages/agent-core-v2/test/agent/loop/loop.test.ts | 4 ++-- packages/agent-core-v2/test/tool/tool.test.ts | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.md b/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.md index 62e9ccecd7c..fae1c1fd2a3 100644 --- a/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.md +++ b/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.md @@ -2,7 +2,7 @@ Launch multiple subagents from one prompt template, existing agent resumes, or b Use AgentSwarm when many subagents should run the same kind of task over different inputs. The placeholder is exactly `{{item}}`. For example, with `prompt_template` set to `Review {{item}} for likely regressions.` and `items` set to `["src/a.ts", "src/b.ts"]`, AgentSwarm launches two new subagents with those two concrete prompts. For a few differently-shaped tasks, make separate `Agent` calls in one message instead. -Use `resume_agent_ids` to continue subagents that already exist from earlier work, such as ones that failed or timed out: map each agent id to the prompt for that resumed subagent (usually `continue` if no extra information is needed). You may combine `resume_agent_ids` with `items` in the same call to resume existing subagents and launch new ones. Do not duplicate resumed work in `items`. +Use `resume_agent_ids` to continue subagents that already exist from earlier work, such as ones that failed or timed out. Pass a flat object: keys are existing `agent_id` strings (as reported in a previous swarm's `` output), values are the continuation prompt for that subagent (usually `"continue"` if no extra information is needed). For example, with `resume_agent_ids` set to `{"agent-coder-1": "continue", "agent-coder-2": "focus on the imports"}`, AgentSwarm resumes those two existing subagents and spawns no new ones. You may combine `resume_agent_ids` with `items` to resume some and spawn others; resumed agents always run before fresh spawns and keep their original `subagent_type` and model. `resume_agent_ids` is a flat record — do not pass an array or a list of `{item, prompt}` objects, and do not use `{{item}}` as a key: that placeholder only applies to `prompt_template`. Each of these is enforced — a violation is rejected before any subagent starts: provide at least 2 `items` unless you pass `resume_agent_ids`; whenever `items` are present, `prompt_template` is required and must contain `{{item}}`; and the filled-in prompts must be distinct (two items that expand to the same prompt are rejected). diff --git a/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.ts b/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.ts index ab40a920549..aacaec5e9e1 100644 --- a/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.ts +++ b/packages/agent-core-v2/src/features/swarm/tools/agent-swarm/agent-swarm.ts @@ -40,7 +40,7 @@ export const AgentSwarmToolInputSchema = z .record(z.string().trim().min(1), z.string().trim().min(1)) .optional() .describe( - 'Map of existing subagent agent_id to the prompt used to resume that subagent. These resumed subagents are launched before new item-based subagents.', + 'Flat object: keys are existing subagent agent_id strings (from a previous swarm\'s `` result), values are the continuation prompt for that subagent (e.g. "continue"). Resumed subagents run before new item-based subagents. Do not pass an array or a list of {item, prompt} objects.', ), fork: z .boolean() diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index ffe44967bab..617608644bc 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -290,7 +290,7 @@ describe('FullCompaction', () => { properties: expect.objectContaining({ agent_id: 'main', source: 'manual', - tokens_before: 3_516, + tokens_before: 3_688, tokens_after: expect.any(Number), duration_ms: expect.any(Number), compacted_count: 6, @@ -569,7 +569,7 @@ describe('FullCompaction', () => { session_id: 'test-session', cwd: dir, trigger: 'auto', - token_count: 3_516, + token_count: 3_688, }); expect(post).toMatchObject({ hook_event_name: 'PostCompact', @@ -1667,8 +1667,8 @@ describe('FullCompaction', () => { event: 'compaction_finished', properties: expect.objectContaining({ source: 'auto', - tokens_before: 3_523, - tokens_after: 3_507, + tokens_before: 3_695, + tokens_after: 3_679, compacted_count: 7, retry_count: 0, }), diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index 72421298f8b..54a48324173 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -133,8 +133,8 @@ describe('Agent loop', () => { [emit] turn.step.started { "time": "