Skip to content

Commit 8913440

Browse files
authored
feat: show warning when resuming across working directories (#118)
1 parent 588145d commit 8913440

13 files changed

Lines changed: 186 additions & 21 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@moonshot-ai/agent-core": minor
3+
"@moonshot-ai/kimi-code-sdk": minor
4+
"@moonshot-ai/kimi-code": minor
5+
---
6+
7+
Support querying sessions by sessionId or workDir in listSessions, and show a helpful cd command when resuming a session from a different working directory.

‎apps/kimi-code/src/cli/run-prompt.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
track,
66
withTelemetryContext,
77
} from '@moonshot-ai/kimi-telemetry';
8+
import chalk from 'chalk';
89
import {
910
KimiHarness,
1011
log,
@@ -158,6 +159,22 @@ async function resolvePromptSession(
158159
setRestorePermission: (restorePermission: () => Promise<void>) => void,
159160
): Promise<ResolvedPromptSession> {
160161
if (opts.session !== undefined) {
162+
const sessions = await harness.listSessions({ sessionId: opts.session, workDir });
163+
const target = sessions[0];
164+
if (target === undefined) {
165+
throw new Error(`Session "${opts.session}" not found.`);
166+
}
167+
if (target.workDir !== workDir) {
168+
stderr.write(
169+
`${chalk.yellow(
170+
`Session "${opts.session}" was created under a different directory.\n` +
171+
` cd "${target.workDir}" && kimi -r ${opts.session}`,
172+
)}\n\n`,
173+
);
174+
throw new Error(
175+
`Session "${opts.session}" was created under a different directory.`,
176+
);
177+
}
161178
const session = await harness.resumeSession({ id: opts.session });
162179
const status = await session.getStatus();
163180
const restorePermission = await forcePromptPermission(

‎apps/kimi-code/src/tui/kimi-tui.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,11 +448,26 @@ export class KimiTUI {
448448
}
449449

450450
if (startup.sessionFlag !== undefined) {
451-
const sessions = await this.harness.listSessions({ workDir });
452-
const target = sessions.find((candidate) => candidate.id === startup.sessionFlag);
451+
const sessions = await this.harness.listSessions({
452+
sessionId: startup.sessionFlag,
453+
workDir,
454+
});
455+
const target = sessions[0];
453456
if (target === undefined) {
454457
throw new Error(`Session "${startup.sessionFlag}" not found.`);
455458
}
459+
if (target.workDir !== workDir) {
460+
this.state.ui.stop();
461+
process.stderr.write(
462+
`${chalk.yellow(
463+
`Session "${startup.sessionFlag}" was created under a different directory.\n` +
464+
` cd "${target.workDir}" && kimi -r ${startup.sessionFlag}`,
465+
)}\n\n`,
466+
);
467+
throw new Error(
468+
`Session "${startup.sessionFlag}" was created under a different directory.`,
469+
);
470+
}
456471
session = await this.harness.resumeSession({ id: startup.sessionFlag });
457472
shouldReplayHistory = true;
458473
} else {

‎apps/kimi-code/test/cli/run-prompt.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const mocks = vi.hoisted(() => {
5656
),
5757
harnessCreateSession: vi.fn(async () => session),
5858
harnessResumeSession: vi.fn(async () => session),
59-
harnessListSessions: vi.fn(async () => [{ id: 'ses_previous' }]),
59+
harnessListSessions: vi.fn(async () => [{ id: 'ses_previous', workDir: process.cwd() }]),
6060
harnessClose: vi.fn(),
6161
harnessTrack: vi.fn(),
6262
harnessGetCachedAccessToken: vi.fn(),

‎apps/kimi-code/test/tui/kimi-tui-startup.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ describe("KimiTUI startup", () => {
705705

706706
it("starts TUI without replaying when an explicit resume needs OAuth login", async () => {
707707
const harness = makeHarness(makeSession(), {
708-
listSessions: vi.fn(async () => [{ id: "ses-target" }]),
708+
listSessions: vi.fn(async () => [{ id: "ses-target", workDir: "/tmp/proj-a" }]),
709709
resumeSession: vi.fn(async () => {
710710
throw loginRequiredError();
711711
}),

‎packages/agent-core/src/rpc/core-api.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export interface ExportSessionResult {
105105
}
106106

107107
export interface ListSessionsPayload {
108-
readonly workDir: string;
108+
readonly workDir?: string;
109+
readonly sessionId?: string;
109110
}
110111

111112
export interface CoreInfo {

‎packages/agent-core/src/rpc/core-impl.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,8 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
317317
return this.resumeSession({ sessionId: id });
318318
}
319319

320-
async listSessions(input: ListSessionsPayload): Promise<readonly SessionSummary[]> {
321-
const options = input;
322-
return this.sessionStore.list({
323-
...options,
324-
workDir: requiredWorkDir('listSessions', options.workDir),
325-
});
320+
async listSessions(input: ListSessionsPayload = {}): Promise<readonly SessionSummary[]> {
321+
return this.sessionStore.list(input);
326322
}
327323

328324
async renameSession({ sessionId, ...payload }: RenameSessionRequest): Promise<void> {

‎packages/agent-core/src/session/store/session-store.ts‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,27 @@ export class SessionStore {
135135
await writeFile(statePath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8');
136136
}
137137

138-
async list(options: ListSessionsPayload): Promise<readonly SessionSummary[]> {
139-
const workDir = normalizeWorkDir(options.workDir);
138+
async list(options: ListSessionsPayload = {}): Promise<readonly SessionSummary[]> {
139+
const workDir =
140+
options.workDir === undefined ? undefined : normalizeRequiredWorkDir(options.workDir);
141+
const sessionId = normalizeOptionalSessionId(options.sessionId);
142+
143+
if (workDir !== undefined) {
144+
if (sessionId !== undefined) {
145+
const local = await this.summaryFromWorkDirSession(sessionId, workDir);
146+
if (local !== undefined) return [local];
147+
return this.listSessionId(sessionId);
148+
}
149+
return this.listWorkDir(workDir);
150+
}
151+
152+
if (sessionId !== undefined) {
153+
return this.listSessionId(sessionId);
154+
}
155+
return this.listAll();
156+
}
157+
158+
private async listWorkDir(workDir: string): Promise<readonly SessionSummary[]> {
140159
const bucketDir = join(this.sessionsDir, encodeWorkDirKey(workDir));
141160
let entries;
142161
try {
@@ -157,6 +176,38 @@ export class SessionStore {
157176
return sessions;
158177
}
159178

179+
private async listSessionId(sessionId: string): Promise<readonly SessionSummary[]> {
180+
try {
181+
return [await this.get(sessionId)];
182+
} catch (error) {
183+
if (error instanceof KimiError && error.code === ErrorCodes.SESSION_NOT_FOUND) {
184+
return [];
185+
}
186+
throw error;
187+
}
188+
}
189+
190+
private async listAll(): Promise<readonly SessionSummary[]> {
191+
const index = await readSessionIndex(this.homeDir, this.sessionsDir);
192+
const sessions: SessionSummary[] = [];
193+
for (const entry of index.values()) {
194+
if (!(await isDirectory(entry.sessionDir))) continue;
195+
sessions.push(await this.summaryFromDir(entry.sessionId, entry.sessionDir, entry.workDir));
196+
}
197+
sessions.sort(compareSessionSummary);
198+
return sessions;
199+
}
200+
201+
private async summaryFromWorkDirSession(
202+
sessionId: string,
203+
workDir: string,
204+
): Promise<SessionSummary | undefined> {
205+
if (!isSafeSessionId(sessionId)) return undefined;
206+
const sessionDir = this.sessionDirFor({ id: sessionId, workDir });
207+
if (!(await isDirectory(sessionDir))) return undefined;
208+
return this.summaryFromDir(sessionId, sessionDir, workDir);
209+
}
210+
160211
async assertDirectory(id: string): Promise<string> {
161212
return (await this.findExistingSessionEntry(id)).sessionDir;
162213
}
@@ -287,6 +338,17 @@ async function readOptionalState(sessionDir: string): Promise<SessionSummaryStat
287338
}
288339
}
289340

341+
function normalizeRequiredWorkDir(workDir: string): string {
342+
if (workDir.trim() === '') {
343+
throw new KimiError(ErrorCodes.REQUEST_WORK_DIR_REQUIRED, 'listSessions requires workDir');
344+
}
345+
return normalizeWorkDir(workDir);
346+
}
347+
348+
function normalizeOptionalSessionId(sessionId: string | undefined): string | undefined {
349+
return sessionId === undefined ? undefined : sessionId.trim();
350+
}
351+
290352
function normalizeForkTitle(title: string | undefined, fallback: unknown): string {
291353
if (title !== undefined) {
292354
const normalized = title.trim();

‎packages/node-sdk/src/kimi-harness.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class KimiHarness {
183183
return result;
184184
}
185185

186-
async listSessions(options: ListSessionsOptions): Promise<readonly SessionSummary[]> {
186+
async listSessions(options: ListSessionsOptions = {}): Promise<readonly SessionSummary[]> {
187187
return this.rpc.listSessions(options);
188188
}
189189

‎packages/node-sdk/src/rpc.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export class SDKRpcClient {
167167
return rpc.closeSession({ sessionId: input.sessionId });
168168
}
169169

170-
async listSessions(input: ListSessionsOptions): Promise<readonly SessionSummary[]> {
170+
async listSessions(input: ListSessionsOptions = {}): Promise<readonly SessionSummary[]> {
171171
const rpc = await this.getRpc();
172172
return rpc.listSessions(input);
173173
}

0 commit comments

Comments
 (0)