Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
47cd55b
feat: multi-instance session lease, write fencing, and fs/watch relia…
7Sageer Jul 19, 2026
82b8da6
fix: close lock and session lifecycle review gaps
7Sageer Jul 20, 2026
1f76353
fix: isolate session redirects and fence task persistence
7Sageer Jul 20, 2026
4845ee1
fix: harden fs watch and session lifecycle races
7Sageer Jul 20, 2026
2d76c12
refactor: replace session lease heartbeat protocol with kernel-held f…
7Sageer Jul 20, 2026
3d52ba5
fix: harden filesystem coordination and shutdown
7Sageer Jul 20, 2026
2762bfc
refactor: drop multi-server flag and dedupe session ownership classif…
7Sageer Jul 21, 2026
dccb293
fix(agent-core-v2): satisfy expect-expect and only-throw-error lint r…
7Sageer Jul 21, 2026
8bf9592
chore(nix): update pnpmDeps hash for kernel-file-lock dependencies
7Sageer Jul 21, 2026
cfaf8eb
ci: drop kernel-file-lock Node 26 job and Node engines caps
7Sageer Jul 21, 2026
5d2d71e
chore: streamline user-facing changeset entries
7Sageer Jul 21, 2026
37e243d
fix: close session event journals before lease release
7Sageer Jul 21, 2026
c4030ef
refactor: simplify write fencing to stat-only detection
7Sageer Jul 21, 2026
6d6a4ad
fix: harden multi-server session transitions
7Sageer Jul 21, 2026
225f7f9
refactor: remove obsolete watch and lock state
7Sageer Jul 21, 2026
fd1cdad
refactor: drop dead ownership surface and dedupe PR helpers
7Sageer Jul 21, 2026
eb7091b
refactor: dedupe lock, fencing, and watch helpers
7Sageer Jul 21, 2026
248e47f
test: prune redundant and low-value coverage
7Sageer Jul 21, 2026
68815ca
fix(agent-core-v2): enforce session lease domain layer
7Sageer Jul 21, 2026
ccd304c
fix(agent-core-v2): keep partial reads behind write fence
7Sageer Jul 21, 2026
504b878
refactor: route skill root probes through host fs
7Sageer Jul 21, 2026
912c039
fix(kap-server): pass host fs to workspace skill root probes
7Sageer Jul 22, 2026
95d92dc
test(agent-core-v2): stub identity realpath in plan fakes
7Sageer Jul 22, 2026
735bbdc
refactor(agent-core-v2): converge session release
7Sageer Jul 22, 2026
9222d02
refactor(agent-core-v2): clarify write admission interfaces
7Sageer Jul 22, 2026
41c1712
fix: handle capped reads, append writes, and IPv6 hosts
7Sageer Jul 22, 2026
d6f1976
refactor: rename session lifecycle properties for clarity and consist…
7Sageer Jul 22, 2026
b58214d
fix(agent-core-v2): isolate file baselines per agent
7Sageer Jul 22, 2026
83d93cd
fix: adapt to upstream veto-event tool API and reconcile fork copy or…
7Sageer Jul 22, 2026
0300890
refactor: re-export kernel file lock binding hook from agent-core-v2
7Sageer Jul 22, 2026
fab562b
fix: preserve plan fencing and journal ownership
7Sageer Jul 23, 2026
6b87894
Merge remote-tracking branch 'upstream/main' into refactor-fs-watch
7Sageer Jul 23, 2026
9d544ed
fix: unblock CI typecheck, lint, and failing tests
7Sageer Jul 23, 2026
7bdaa3e
Merge remote-tracking branch 'upstream/main' into refactor-fs-watch
7Sageer Jul 24, 2026
bd1228a
feat(agent-core-v2): add pure-JS cross-process lock behind KIMI_LOCK_…
7Sageer Jul 24, 2026
cae46c7
Merge remote-tracking branch 'upstream/main' into refactor-fs-watch
7Sageer Jul 27, 2026
99c51d1
refactor(agent-core-v2): drop the kernel lock, make the pure-JS lock …
7Sageer Jul 28, 2026
18827f8
Merge remote-tracking branch 'origin/refactor-fs-watch' into lock-con…
7Sageer Jul 28, 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/cold-journal-inspection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Prevent cold session reads from replacing an event journal owned by another server instance.
5 changes: 5 additions & 0 deletions .changeset/fix-ipv6-web-origin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix `kimi web` startup when binding to an IPv6 host.
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, while shutdown blocks late writes and releases ambiguous closes through a dirty fallback.
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
---

Each agent now tracks its own file-read baselines and refuses to overwrite files, including active plan files, that changed on disk after its last read.
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/
1 change: 1 addition & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"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": {},
"optionalDependencies": {
"@mariozechner/clipboard": "^0.3.9",
"node-pty": "^1.1.0"
Expand Down
2 changes: 0 additions & 2 deletions apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
const handledNativeRuntimeRequires = new Set();

function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
if (optionalRuntimeRequires.has(specifier)) return true;
if (handledNativeRuntimeRequires.has(specifier)) return true;
return false;
}

Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-code/src/cli/sub/web/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
DEFAULT_SERVER_HOST,
DEFAULT_SERVER_PORT,
parseServerOptions,
serverOrigin,
tryResolveServerToken,
VALID_LOG_LEVELS,
type ParsedServerOptions,
Expand Down Expand Up @@ -288,7 +289,7 @@ async function runServerInProcess(
});
logger.info('serving the REST/WS API and the bundled web UI');
running = {
address: `http://${v2.host}:${v2.port}`,
address: serverOrigin(v2.host, v2.port),
logger,
close: () => v2.close(),
};
Expand Down
9 changes: 3 additions & 6 deletions apps/kimi-code/src/cli/sub/web/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

import type { ServerLogLevel } from '@moonshot-ai/kap-server';
import { formatServerOrigin, type ServerLogLevel } from '@moonshot-ai/kap-server';

export const LOCAL_SERVER_HOST = '127.0.0.1';
export const DEFAULT_LAN_HOST = '0.0.0.0';
export const DEFAULT_SERVER_HOST = LOCAL_SERVER_HOST;
export const DEFAULT_SERVER_PORT = 58627;
export const DEFAULT_SERVER_ORIGIN = serverOrigin(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
export const DEFAULT_SERVER_ORIGIN = formatServerOrigin(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
export { formatServerOrigin as serverOrigin };

/** Filename (under KIMI_CODE_HOME) of the persistent server bearer token. */
export const SERVER_TOKEN_FILE = 'server.token';
Expand Down Expand Up @@ -112,10 +113,6 @@ export function parseLogLevel(raw: string | undefined): ServerLogLevel {
);
}

export function serverOrigin(host: string, port: number): string {
return `http://${host}:${port}`;
}

/** Strip `/api/v1` and trailing slashes so user-supplied origins are uniform. */
export function normalizeServerOrigin(value: string): string {
const url = new URL(value);
Expand Down
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/test/cli/web/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { registerWebCommand } from '#/cli/sub/web';
import type { LegacyKillDeps } from '#/cli/sub/web/legacy-kill';
import type { WebCommandDeps } from '#/cli/sub/web/run';
import type { ParsedServerOptions } from '#/cli/sub/web/shared';
import { serverOrigin, type ParsedServerOptions } from '#/cli/sub/web/shared';
import { darkColors } from '#/tui/theme/colors';

vi.mock('node:child_process', async (importOriginal) => {
Expand Down Expand Up @@ -952,6 +952,15 @@ describe('formatHostForUrl', () => {
});
});

describe('serverOrigin', () => {
it('bracket-wraps an IPv6 host so the origin remains parseable', () => {
const origin = serverOrigin('::1', 58627);

expect(origin).toBe('http://[::1]:58627');
expect(new URL(origin).port).toBe('58627');
});
});

describe('filterDisplayAddresses', () => {
it('drops IPv6 link-local, de-duplicates, and orders IPv4 before IPv6', async () => {
const { filterDisplayAddresses } = await import('#/cli/sub/web/networks');
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 });
}
});
});
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
78 changes: 78 additions & 0 deletions apps/kimi-web/src/api/daemon/sessionOwnership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// apps/kimi-web/src/api/daemon/sessionOwnership.ts
// Session-ownership (multi-instance) error details — local re-implementation
// of the `details` payload carried under envelope code 40922
// (`session.held_by_peer`). kimi-web must not depend on @moonshot-ai/agent-core
// or @moonshot-ai/protocol, so the zod schema in
// packages/protocol/src/session-ownership.ts is mirrored here as plain types
// plus structural narrowing.
//
// Wire semantics (from the protocol schema):
// - creating lease file observed mid-creation; retry shortly
// - routable holder is live and registered an address; redirect
// - held-by-local-instance holder has no address (local/embedded); terminal

import { isDaemonApiError } from '../errors';

/** Envelope `code` for `session.held_by_peer` (see kap-server error-handler). */
export const SESSION_HELD_BY_PEER_CODE = 40922;

export type SessionOwnershipPhase =
| 'creating'
| 'routable'
| 'held-by-local-instance';

export interface HeldByPeerDetails {
kind: 'held-by-peer';
phase: SessionOwnershipPhase;
/** Present only when phase === 'routable'. */
address?: string;
/** Retry hint (ms) for 'creating'. */
retry_after_ms?: number;
}

export type SessionOwnershipDetails = HeldByPeerDetails;

const PHASES: ReadonlySet<SessionOwnershipPhase> = new Set([
'creating',
'routable',
'held-by-local-instance',
]);

function isSessionOwnershipPhase(value: string): value is SessionOwnershipPhase {
return PHASES.has(value as SessionOwnershipPhase);
}

/** Structurally narrow an unknown envelope `details` payload. Returns undefined
* for anything that is not a well-formed ownership payload (defensive: the
* server contract is new, and a malformed payload must degrade to the generic
* error toast rather than a crash). */
export function narrowSessionOwnershipDetails(
details: unknown,
): SessionOwnershipDetails | undefined {
if (typeof details !== 'object' || details === null) return undefined;
const record = details as Record<string, unknown>;
if (record['kind'] !== 'held-by-peer') return undefined;
const phase = record['phase'];
if (typeof phase !== 'string' || !isSessionOwnershipPhase(phase)) {
return undefined;
}
const address = record['address'];
const retryAfterMs = record['retry_after_ms'];
return {
kind: 'held-by-peer',
phase,
address: typeof address === 'string' && address.length > 0 ? address : undefined,
retry_after_ms:
typeof retryAfterMs === 'number' && Number.isFinite(retryAfterMs) && retryAfterMs >= 0
? retryAfterMs
: undefined,
};
}

/** Extract the ownership details from any thrown value: a DaemonApiError with
* code 40922 carrying a well-formed details payload. Anything else → undefined. */
export function getSessionOwnershipDetails(err: unknown): SessionOwnershipDetails | undefined {
if (!isDaemonApiError(err)) return undefined;
if (err.code !== SESSION_HELD_BY_PEER_CODE) return undefined;
return narrowSessionOwnershipDetails(err.details);
}
3 changes: 2 additions & 1 deletion apps/kimi-web/src/api/daemon/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,8 @@ export interface WireInFlightTurn {
/** `GET /sessions/{sid}/snapshot` — atomic rebuild state at a watermark. */
export interface WireSessionSnapshot {
as_of_seq: number;
epoch: string;
/** Absent until the journal's first durable event: "no baseline" ≠ "baseline changed". */
epoch?: string;
session: WireSession;
messages: { items: WireMessage[]; has_more: boolean };
in_flight_turn: WireInFlightTurn | null;
Expand Down
Loading