Merge branch 'kimi-code-v2' of https://github.com/MoonshotAI/kimi-code into kimi-code-v2

This commit is contained in:
_Kerman 2026-07-08 17:02:39 +08:00
commit 5eef0a6736
3 changed files with 176 additions and 9 deletions

View file

@ -83,8 +83,8 @@ export interface ISessionLifecycleService {
* agent from the persisted wire log. Returns the existing handle when the
* session is already live (a no-op in that case live agents are never
* re-restored). Returns `undefined` when the session is unknown to the index
* or its workspace is no longer registered (mirrors the cold-source
* limitation of `fork`).
* or neither the persisted session summary nor the workspace registry can
* provide a workdir (mirrors the cold-source limitation of `fork`).
*
* Lets the read edges (snapshot / messages) serve cold sessions created by
* a previous process or by v1 without requiring a prior `create` in this

View file

@ -8,7 +8,8 @@
* its `agentLifecycle` agents, and
* broadcasts through `event`. Materializes the session's initial metadata on
* creation by resolving `sessionMetadata`. Bound at App scope. Persisted
* sessions are the `sessionIndex` read model.
* sessions are discovered through the `sessionIndex` read model, and workspace
* roots are remembered through `workspaceRegistry`.
*/
import { randomUUID } from 'node:crypto';
@ -23,7 +24,6 @@ import {
} from '#/_base/di/scope';
import { Disposable } from '#/_base/di/lifecycle';
import { Emitter, type Event } from '#/_base/event';
import { encodeWorkDirKey } from '#/_base/utils/workdir-slug';
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import { ensureMainAgent, MAIN_AGENT_ID } from '#/session/agentLifecycle/mainAgent';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
@ -63,6 +63,10 @@ import {
ISessionLifecycleService,
} from './sessionLifecycle';
type MaterializeSessionOptions = CreateSessionOptions & {
readonly workspaceId?: string;
};
export class SessionLifecycleService extends Disposable implements ISessionLifecycleService {
declare readonly _serviceBrand: undefined;
private readonly sessions = new Map<string, ISessionScopeHandle>();
@ -102,8 +106,9 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
return handle;
}
private async materializeSession(opts: CreateSessionOptions): Promise<ISessionScopeHandle> {
const workspaceId = encodeWorkDirKey(opts.workDir);
private async materializeSession(opts: MaterializeSessionOptions): Promise<ISessionScopeHandle> {
const workspace = await this.workspaceRegistry.createOrTouch(opts.workDir);
const workspaceId = opts.workspaceId ?? workspace.id;
const sessionScope = this.bootstrap.sessionScope(workspaceId, opts.sessionId);
const sessionDir = this.bootstrap.sessionDir(workspaceId, opts.sessionId);
// Metadata lives at `<sessionDir>/state.json` (shared with v1's layout; the
@ -169,10 +174,18 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
const summary = await this.index.get(sessionId);
if (summary === undefined) return undefined;
const workspace = await this.workspaceRegistry.get(summary.workspaceId);
if (workspace === undefined) return undefined;
const workspace =
summary.cwd === undefined
? await this.workspaceRegistry.get(summary.workspaceId)
: undefined;
const workDir = summary.cwd ?? workspace?.root;
if (workDir === undefined) return undefined;
const handle = await this.materializeSession({ sessionId, workDir: workspace.root });
const handle = await this.materializeSession({
sessionId,
workDir,
workspaceId: summary.workspaceId,
});
const agents = handle.accessor.get(IAgentLifecycleService);
if (agents.getHandle(MAIN_AGENT_ID) === undefined) {
const main = await ensureMainAgent(handle);

View file

@ -26,6 +26,9 @@ import { ISessionIndex } from '#/app/sessionIndex/sessionIndex';
import { IAppendLogStore } from '#/persistence/interface/appendLogStore';
import { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore';
import { IWorkspaceRegistry, type Workspace } from '#/app/workspaceRegistry/workspaceRegistry';
import { encodeWorkDirKey } from '#/_base/utils/workdir-slug';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/mainAgent';
function bootstrapStub(): IBootstrapService {
return {
@ -111,6 +114,34 @@ function workspaceRegistryStub(): IWorkspaceRegistry {
};
}
function persistentWorkspaceRegistryStub(): IWorkspaceRegistry {
const workspaces = new Map<string, Workspace>();
return {
_serviceBrand: undefined,
list: () => Promise.resolve([...workspaces.values()]),
get: (id) => Promise.resolve(workspaces.get(id)),
createOrTouch: (root, name) => {
const id = encodeWorkDirKey(root);
const now = 1;
const existing = workspaces.get(id);
const workspace: Workspace =
existing !== undefined
? { ...existing, lastOpenedAt: now }
: {
id,
root,
name: name ?? 'proj',
createdAt: now,
lastOpenedAt: now,
};
workspaces.set(id, workspace);
return Promise.resolve(workspace);
},
update: () => Promise.resolve(undefined),
delete: () => Promise.resolve(),
};
}
function sessionIndexStub(): ISessionIndex {
return {
_serviceBrand: undefined,
@ -120,6 +151,27 @@ function sessionIndexStub(): ISessionIndex {
};
}
function sessionIndexWithSummary(
sessionId: string,
workDir: string,
workspaceId = encodeWorkDirKey(workDir),
): ISessionIndex {
const summary = {
id: sessionId,
workspaceId,
cwd: workDir,
createdAt: 1,
updatedAt: 1,
archived: false,
};
return {
_serviceBrand: undefined,
list: () => Promise.resolve({ items: [summary], total: 1, hasMore: false }),
get: (id) => Promise.resolve(id === sessionId ? summary : undefined),
countActive: () => Promise.resolve(1),
};
}
function appendLogStoreStub(): IAppendLogStore {
return {
_serviceBrand: undefined,
@ -167,6 +219,23 @@ function agentLifecycleStub(): IAgentLifecycleService {
};
}
function agentLifecycleWithMainStub(): IAgentLifecycleService {
const main = {
id: MAIN_AGENT_ID,
kind: LifecycleScope.Agent,
accessor: {
get: () => {
throw new Error('unexpected main agent service access');
},
},
dispose: () => {},
} as IAgentScopeHandle;
return {
...agentLifecycleStub(),
getHandle: (id) => (id === MAIN_AGENT_ID ? main : undefined),
};
}
function tick(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0));
}
@ -263,6 +332,91 @@ describe('SessionLifecycleService', () => {
expect(h.kind).toBe(LifecycleScope.Session);
});
it('registers the workspace during create so a cold resume can resolve the workdir', async () => {
const workDir = '/tmp/proj';
const workspaceRegistry = persistentWorkspaceRegistryStub();
const sessionIndex = sessionIndexWithSummary('s1', workDir);
const first = build([
stubPair(IWorkspaceRegistry, workspaceRegistry),
stubPair(ISessionIndex, sessionIndex),
]);
await first.create({ sessionId: 's1', workDir });
await expect(workspaceRegistry.get(encodeWorkDirKey(workDir))).resolves.toMatchObject({
root: workDir,
});
host?.dispose();
host = undefined;
const second = build([
stubPair(IWorkspaceRegistry, workspaceRegistry),
stubPair(ISessionIndex, sessionIndex),
stubPair(IAgentLifecycleService, agentLifecycleWithMainStub()),
]);
const resumed = await second.resume('s1');
expect(resumed?.id).toBe('s1');
expect(resumed?.accessor.get(ISessionContext).cwd).toBe(workDir);
});
it('resumes from the persisted cwd when the workspace registry entry is missing', async () => {
const workDir = '/tmp/proj';
const svc = build([
stubPair(IWorkspaceRegistry, persistentWorkspaceRegistryStub()),
stubPair(ISessionIndex, sessionIndexWithSummary('s1', workDir)),
stubPair(IAgentLifecycleService, agentLifecycleWithMainStub()),
]);
const resumed = await svc.resume('s1');
expect(resumed?.id).toBe('s1');
expect(resumed?.accessor.get(ISessionContext).workspaceId).toBe(encodeWorkDirKey(workDir));
});
it('resumes with the persisted cwd and indexed workspace id when the registry root is stale', async () => {
const workDir = '/tmp/proj';
const staleRoot = '/tmp/stale';
const indexedWorkspaceId = 'wd_indexed';
const workspaceRegistry: IWorkspaceRegistry = {
_serviceBrand: undefined,
list: () => Promise.resolve([]),
get: (id) =>
Promise.resolve(
id === indexedWorkspaceId
? {
id: indexedWorkspaceId,
root: staleRoot,
name: 'stale',
createdAt: 1,
lastOpenedAt: 1,
}
: undefined,
),
createOrTouch: (root, name) =>
Promise.resolve({
id: encodeWorkDirKey(root),
root,
name: name ?? 'proj',
createdAt: 1,
lastOpenedAt: 1,
}),
update: () => Promise.resolve(undefined),
delete: () => Promise.resolve(),
};
const svc = build([
stubPair(IWorkspaceRegistry, workspaceRegistry),
stubPair(ISessionIndex, sessionIndexWithSummary('s1', workDir, indexedWorkspaceId)),
stubPair(IAgentLifecycleService, agentLifecycleWithMainStub()),
]);
const resumed = await svc.resume('s1');
const ctx = resumed?.accessor.get(ISessionContext);
expect(ctx?.cwd).toBe(workDir);
expect(ctx?.workspaceId).toBe(indexedWorkspaceId);
expect(ctx?.sessionDir).toBe(`/tmp/sessions/${indexedWorkspaceId}/s1`);
});
it('archive flags metadata, removes agents, publishes the event, and disposes the session', async () => {
let archived: boolean | undefined;
const removed: string[] = [];