Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
56f1a5a
feat: multi-instance session lease, write fencing, and fs/watch relia…
7Sageer Jul 19, 2026
215ca73
fix: close lock and session lifecycle review gaps
7Sageer Jul 20, 2026
75473fc
fix: isolate session redirects and fence task persistence
7Sageer Jul 20, 2026
304910e
fix: harden fs watch and session lifecycle races
7Sageer Jul 20, 2026
d1ff815
refactor: replace session lease heartbeat protocol with kernel-held f…
7Sageer Jul 20, 2026
0b65ef4
fix: harden filesystem coordination and shutdown
7Sageer Jul 20, 2026
aa4dcfe
refactor: drop multi-server flag and dedupe session ownership classif…
7Sageer Jul 21, 2026
2137288
Merge branch 'main' into refactor-fs-watch
7Sageer Jul 21, 2026
bb67e71
fix(agent-core-v2): satisfy expect-expect and only-throw-error lint r…
7Sageer Jul 21, 2026
f13ced9
chore(nix): update pnpmDeps hash for kernel-file-lock dependencies
7Sageer Jul 21, 2026
d63a7e9
Merge branch 'main' into refactor-fs-watch
7Sageer Jul 21, 2026
6f61cce
ci: drop kernel-file-lock Node 26 job and Node engines caps
7Sageer Jul 21, 2026
f5b8af1
chore: streamline user-facing changeset entries
7Sageer Jul 21, 2026
d34c8e5
chore: restore changeset entries owned by main
7Sageer Jul 21, 2026
6a01fd2
fix: close session event journals before lease release
7Sageer Jul 21, 2026
e75df4e
Merge branch 'main' into refactor-fs-watch
7Sageer Jul 21, 2026
03be83f
refactor: simplify write fencing to stat-only detection
7Sageer Jul 21, 2026
c469746
fix: harden multi-server session transitions
7Sageer Jul 21, 2026
0b4707e
refactor: remove obsolete watch and lock state
7Sageer Jul 21, 2026
f2d99b3
refactor: drop dead ownership surface and dedupe PR helpers
7Sageer Jul 21, 2026
b412d78
refactor: dedupe lock, fencing, and watch helpers
7Sageer Jul 21, 2026
02799c3
test: prune redundant and low-value coverage
7Sageer Jul 21, 2026
6a0d40e
fix(agent-core-v2): enforce session lease domain layer
7Sageer Jul 21, 2026
de7faaa
fix(agent-core-v2): keep partial reads behind write fence
7Sageer Jul 21, 2026
725df34
refactor: route skill root probes through host fs
7Sageer Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/journal-fswatch-reliability-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix `.gitignore` edits not taking effect until restart for file listing and file watching.
5 changes: 5 additions & 0 deletions .changeset/multi-instance-session-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: When several server instances share one home, opening a session held by another instance now redirects to that instance, and the session list refreshes automatically when a peer adds or removes sessions.
5 changes: 5 additions & 0 deletions .changeset/session-event-release-barrier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

web: Restore live event delivery after archiving and reopening a session.
5 changes: 5 additions & 0 deletions .changeset/session-lease-write-fencing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Opening the same session from a second instance now fails with a clear ownership error instead of silently interleaving writes.
5 changes: 5 additions & 0 deletions .changeset/skill-hot-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Newly added or updated skills now take effect on the next turn without restarting, and the web skill list refreshes automatically.
5 changes: 5 additions & 0 deletions .changeset/stale-write-fencing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

The agent now refuses to edit a file that changed on disk since it was last read in the session, instead of silently overwriting external changes.
2 changes: 1 addition & 1 deletion apps/kimi-code/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ agents/
src/generated/vis-web-asset.ts

# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
native/
/native/
4 changes: 4 additions & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
"e2e:real": "pnpm -w run build:packages && KIMI_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts",
"postinstall": "node scripts/postinstall.mjs"
},
"dependencies": {
"fs-native-extensions": "1.5.0"
},
"optionalDependencies": {
"@mariozechner/clipboard": "^0.3.9",
"node-pty": "^1.1.0"
Expand All @@ -85,6 +88,7 @@
"@moonshot-ai/acp-adapter": "workspace:^",
"@moonshot-ai/agent-core-v2": "workspace:^",
"@moonshot-ai/kap-server": "workspace:^",
"@moonshot-ai/kernel-file-lock": "workspace:^",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
"@moonshot-ai/kimi-code-sdk": "workspace:^",
"@moonshot-ai/kimi-telemetry": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
const handledNativeRuntimeRequires = new Set();
const handledNativeRuntimeRequires = new Set(['fs-native-extensions']);

function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
Expand Down
16 changes: 16 additions & 0 deletions apps/kimi-code/scripts/native/native-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ const piTuiNativeFileByTarget = Object.freeze({
'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'],
});

const fsNativeExtensionsFileByTarget = Object.freeze(
Object.fromEntries(
SUPPORTED_TARGETS.map((target) => [
target,
[`prebuilds/${target}/fs-native-extensions.node`],
]),
),
);

export function isSupportedTarget(target) {
return SUPPORTED_TARGETS.includes(target);
}
Expand Down Expand Up @@ -84,6 +93,13 @@ export const nativeDeps = Object.freeze([
parent: null,
nativeFileRelatives: (target) => piTuiNativeFileByTarget[target] ?? [],
},
{
id: 'fs-native-extensions',
name: () => 'fs-native-extensions',
collect: 'native-file-only',
parent: null,
nativeFileRelatives: (target) => fsNativeExtensionsFileByTarget[target] ?? [],
},
]);

/**
Expand Down
2 changes: 2 additions & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { runUpdatePreflight } from './cli/update/preflight';
import { createKimiCodeHostIdentity, getVersion } from './cli/version';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, PROCESS_NAME } from './constant/app';
import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
import { installKernelFileLockNativeBinding } from './native/kernel-file-lock';
import { installNativeModuleHook } from './native/module-hook';
import { runNativeAssetSmokeIfRequested } from './native/smoke';

Expand Down Expand Up @@ -142,6 +143,7 @@ export function main(): void {
// invalid proxy URL is reported and ignored rather than aborting startup.
installGlobalProxyDispatcher();
installNativeModuleHook();
installKernelFileLockNativeBinding();
if (runNativeAssetSmokeIfRequested()) return;

// Start the background cleanup of stale native cache. Fire-and-forget; must not block startup or throw.
Expand Down
44 changes: 44 additions & 0 deletions apps/kimi-code/src/native/kernel-file-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { join } from 'node:path';

import {
setKernelFileLockBindingLoader,
type KernelFileLockBinding,
} from '@moonshot-ai/kernel-file-lock';

import { loadNativePackageFile } from './native-require';

interface NativeFileLockBinding {
tryLock(fd: number, offset: number, length: number, exclusive: boolean): void;
unlock(fd: number, offset: number, length: number): void;
}

function errorCode(error: unknown): string | undefined {
return (error as NodeJS.ErrnoException | undefined)?.code;
}

export function loadKernelFileLockNativeBinding(): KernelFileLockBinding | undefined {
const target = `${process.platform}-${process.arch}`;
const binding = loadNativePackageFile<NativeFileLockBinding>(
'fs-native-extensions',
join('prebuilds', target, 'fs-native-extensions.node'),
);
if (binding === null) return undefined;
return {
tryLock: (fd) => {
try {
binding.tryLock(fd, 0, 0, true);
return true;
} catch (error) {
if (errorCode(error) === 'EAGAIN') return false;
throw error;
}
},
unlock: (fd) => {
binding.unlock(fd, 0, 0);
},
};
}

export function installKernelFileLockNativeBinding(): void {
setKernelFileLockBindingLoader(loadKernelFileLockNativeBinding);
}
10 changes: 10 additions & 0 deletions apps/kimi-code/src/native/native-require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ export function loadNativePackage<T>(
if (nativeRequire === null) return null;
return nativeRequire(packageName) as T;
}

export function loadNativePackageFile<T>(
packageName: string,
relativePath: string,
options: NativeAssetOptions = {},
): T | null {
const packageRoot = getNativePackageRoot(packageName, options);
if (packageRoot === null) return null;
return createRequire(import.meta.url)(join(packageRoot, relativePath)) as T;
}
11 changes: 10 additions & 1 deletion apps/kimi-code/src/native/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';

import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets';
import { loadKernelFileLockNativeBinding } from './kernel-file-lock';

const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui'];
const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui', 'fs-native-extensions'];

// Verify pi-tui's native helper can actually be loaded through the module hook.
// pi-tui computes native helper paths from process.execPath and require()s them;
Expand Down Expand Up @@ -34,6 +35,13 @@ function smokePiTuiNativeLoad(): void {
}
}

function smokeKernelFileLockNativeLoad(): void {
const binding = loadKernelFileLockNativeBinding();
if (binding === undefined) {
throw new Error('fs-native-extensions binding is unavailable.');
}
}

export function runNativeAssetSmokeIfRequested(): boolean {
if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false;

Expand All @@ -49,6 +57,7 @@ export function runNativeAssetSmokeIfRequested(): boolean {
}
}
smokePiTuiNativeLoad();
smokeKernelFileLockNativeLoad();
process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`);
process.exit(0);
} catch (error) {
Expand Down
28 changes: 27 additions & 1 deletion apps/kimi-code/test/native/native-assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type NativeAssetManifest,
type NativeAssetSource,
} from '#/native/native-assets';
import { loadNativePackage } from '#/native/native-require';
import { loadNativePackage, loadNativePackageFile } from '#/native/native-require';

function sha256(bytes: Buffer | string): string {
return createHash('sha256').update(bytes).digest('hex');
Expand Down Expand Up @@ -125,4 +125,30 @@ describe('native assets', () => {
rmSync(dir, { recursive: true, force: true });
}
});

it('loads a package file from extracted native assets', () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-native-file-require-'));
try {
const { manifest, source } = fakeManifest({
'node_modules/fake-native/package.json': '{}',
'node_modules/fake-native/prebuilds/test-target/addon.cjs':
"module.exports = { value: 'native' };\n",
});

const addon = loadNativePackageFile<{ value: string }>(
'fake-native',
join('prebuilds', 'test-target', 'addon.cjs'),
{
cacheBase: dir,
manifest,
source,
version: 'test',
},
);

expect(addon).toEqual({ value: 'native' });
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
16 changes: 16 additions & 0 deletions apps/kimi-code/test/scripts/native/native-deps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('resolveTargetDeps', () => {
expect(names).toContain('@mariozechner/clipboard');
expect(names).toContain('@mariozechner/clipboard-darwin-arm64');
expect(names).toContain('@moonshot-ai/pi-tui');
expect(names).toContain('fs-native-extensions');
});

it('picks the right clipboard subpackage per target', () => {
Expand Down Expand Up @@ -75,6 +76,15 @@ describe('resolveTargetDeps', () => {
]);
});

it('encodes fs-native-extensions prebuild path per target', () => {
const fsNative = resolveTargetDeps('linux-x64').find(
(d) => d.resolvedName === 'fs-native-extensions',
);
expect(fsNative?.nativeFileRelatives).toEqual([
'prebuilds/linux-x64/fs-native-extensions.node',
]);
});

it('throws on unsupported target', () => {
expect(() => resolveTargetDeps('linux-x64-musl')).toThrow(/unsupported/i);
});
Expand All @@ -97,4 +107,10 @@ describe('nativeDeps registry shape', () => {
expect(piTui?.collect).toBe('native-file-only');
expect(piTui?.parent).toBe(null);
});

it('has fs-native-extensions (collect=native-file-only, no parent)', () => {
const fsNative = nativeDeps.find((d) => d.id === 'fs-native-extensions');
expect(fsNative?.collect).toBe('native-file-only');
expect(fsNative?.parent).toBe(null);
});
});
2 changes: 1 addition & 1 deletion apps/kimi-code/tsdown.native.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const builtins = new Set([
...builtinModules,
...builtinModules.map((name) => `node:${name}`),
]);
const optionalNativeDependencies = new Set(['cpu-features']);
const optionalNativeDependencies = new Set(['cpu-features', 'fs-native-extensions']);

function shouldAlwaysBundle(id: string): boolean {
if (builtins.has(id) || id.startsWith('node:')) return false;
Expand Down
12 changes: 10 additions & 2 deletions apps/kimi-web/src/api/daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1463,8 +1463,16 @@ export class DaemonKimiWebApi implements KimiWebApi {
handlers.onConnectionChange(connected);
},

onError: (code: number, msg: string, fatal: boolean) => {
handlers.onError(code, msg, fatal);
onError: (code: number, msg: string, fatal: boolean, details?: unknown) => {
handlers.onError(code, msg, fatal, details);
},

onSessionListChanged: () => {
handlers.onSessionListChanged?.();
},

onSkillCatalogChanged: (sessionId: string) => {
handlers.onSkillCatalogChanged?.(sessionId);
},

onTerminalOutput: (sessionId, terminalId, data, seq) => {
Expand Down
16 changes: 14 additions & 2 deletions apps/kimi-web/src/api/daemon/serverAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

const STORAGE_KEY = 'kimi-web.server-credential';
const FRAGMENT_PARAM = 'token';
const REDIRECT_HASH_PARAM = 'redirect_hash';
const CREDENTIAL_TTL_MS = 7 * 24 * 60 * 60 * 1000;

interface StoredCredential {
Expand All @@ -38,18 +39,29 @@ function readFragmentToken(): string | undefined {
const params = new URLSearchParams(hash.slice(1));
const token = params.get(FRAGMENT_PARAM);
if (!token) return undefined;
const redirectHash = params.get(REDIRECT_HASH_PARAM);
// Scrub the fragment (keep path + query) so the token is not left in the
// address bar, browser history, or any screenshot of the window.
const url = new URL(window.location.href);
url.hash = '';
url.hash = redirectHash ?? '';
window.history.replaceState(
window.history.state,
'',
`${url.pathname}${url.search}`,
`${url.pathname}${url.search}${url.hash}`,
);
return token;
}

export function withServerCredentialFragment(targetUrl: string, credential: string): string {
const url = new URL(targetUrl);
const redirectHash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash;
const params = new URLSearchParams();
params.set(FRAGMENT_PARAM, credential);
if (redirectHash !== '') params.set(REDIRECT_HASH_PARAM, redirectHash);
url.hash = params.toString();
return url.toString();
}

function createStoredCredential(credential: string): StoredCredential {
return {
version: 1,
Expand Down
Loading
Loading