Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/stream-json-resume-hint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Emit session resume hint as a structured meta message in stream-json output format.
54 changes: 41 additions & 13 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import {
initializeTelemetry,
setCrashPhase,
setTelemetryContext,
shutdownTelemetry,
Expand All @@ -17,9 +15,10 @@ import {
type TelemetryClient,
} from '@moonshot-ai/kimi-code-sdk';

import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_USER_AGENT_PRODUCT } from '#/constant/app';
import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';

import type { CLIOptions, PromptOutputFormat } from './options';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
import { createKimiCodeHostIdentity } from './version';

interface PromptOutput {
Expand Down Expand Up @@ -54,12 +53,14 @@ export async function runPrompt(
const stderr = io.stderr ?? process.stderr;
const promptProcess = io.process ?? process;
const workDir = process.cwd();
const telemetryBootstrap = createCliTelemetryBootstrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delay prompt telemetry bootstrap until startup can initialize

Creating telemetryBootstrap at function entry mints device_id before any of the prompt startup checks run, but first_launch is only emitted later inside initializeCliTelemetry(). If prompt startup fails early (for example, no configured model in resolvePromptSession() or another pre-session error), the command exits without initializing telemetry, so the first-launch event is permanently missed on subsequent successful runs.

Useful? React with 👍 / 👎.

const telemetryClient: TelemetryClient = {
track,
withContext: withTelemetryContext,
setContext: setTelemetryContext,
};
const harness = new KimiHarness({
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
uiMode: PROMPT_UI_MODE,
skillDirs: opts.skillsDirs,
Expand Down Expand Up @@ -112,17 +113,13 @@ export async function runPrompt(
);
restorePromptSessionPermission = restorePermission;

const deviceId = createKimiDeviceId(harness.homeDir);
initializeTelemetry({
homeDir: harness.homeDir,
deviceId,
enabled: config.telemetry !== false,
appName: CLI_USER_AGENT_PRODUCT,
initializeCliTelemetry({
harness,
bootstrap: telemetryBootstrap,
config,
version,
uiMode: PROMPT_UI_MODE,
model: telemetryModel,
getAccessToken: async () =>
(await harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
setCrashPhase('runtime');

Expand All @@ -133,8 +130,9 @@ export async function runPrompt(
afk: true,
});

await runPromptTurn(session, opts.prompt!, opts.outputFormat ?? 'text', stdout, stderr);
stderr.write(`To resume this session: kimi -r ${session.id}\n`);
const outputFormat = opts.outputFormat ?? 'text';
await runPromptTurn(session, opts.prompt!, outputFormat, stdout, stderr);
writeResumeHint(session.id, outputFormat, stdout, stderr);

withTelemetryContext({ sessionId: session.id }).track('exit', {
duration_s: (Date.now() - startedAt) / 1000,
Expand Down Expand Up @@ -476,6 +474,36 @@ interface PromptJsonToolMessage {
content: string;
}

interface PromptJsonResumeMetaMessage {
role: 'meta';
type: 'session.resume_hint';
session_id: string;
command: string;
content: string;
}

function writeResumeHint(
sessionId: string,
outputFormat: PromptOutputFormat,
stdout: PromptOutput,
stderr: PromptOutput,
): void {
const command = `kimi -r ${sessionId}`;
const content = `To resume this session: ${command}`;
if (outputFormat === 'stream-json') {
const message: PromptJsonResumeMetaMessage = {
role: 'meta',
type: 'session.resume_hint',
session_id: sessionId,
command,
content,
};
stdout.write(`${JSON.stringify(message)}\n`);
return;
}
stderr.write(`${content}\n`);
}

class PromptJsonWriter implements PromptTurnWriter {
private assistantText = '';
private readonly toolCalls: PromptJsonToolCall[] = [];
Expand Down
28 changes: 8 additions & 20 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { execSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join } from 'node:path';

import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import {
initializeTelemetry,
setCrashPhase,
setTelemetryContext,
shutdownTelemetry,
Expand All @@ -13,7 +11,7 @@ import {
} from '@moonshot-ai/kimi-telemetry';
import { KimiHarness, log, type TelemetryClient } from '@moonshot-ai/kimi-code-sdk';

import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, CLI_USER_AGENT_PRODUCT } from '#/constant/app';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app';
import { detectPendingMigration } from '#/migration/index';
import type { TuiConfig } from '#/tui/config';
import { loadTuiConfig, TuiConfigParseError } from '#/tui/config';
Expand All @@ -22,6 +20,7 @@ import { KimiTUI } from '#/tui/index';
import { detectTerminalTheme } from '#/tui/theme/detect';

import type { CLIOptions } from './options';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
import { createKimiCodeHostIdentity } from './version';

export async function runShell(
Expand All @@ -46,12 +45,14 @@ export async function runShell(
const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme;

const workDir = process.cwd();
const telemetryBootstrap = createCliTelemetryBootstrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delay telemetry bootstrap until migrate-only early return is cleared

runShell now calls createCliTelemetryBootstrap() before checking the migrateOnly && migrationPlan === null fast-return path. In that path (run-shell.ts, later lines 82-85), the command exits before initializeCliTelemetry(), so a first-run invocation can mint device_id without ever sending first_launch; subsequent normal launches then no longer qualify as first launch. This regresses first-launch telemetry accuracy specifically for users who run migrate-only first.

Useful? React with 👍 / 👎.

const telemetryClient: TelemetryClient = {
track,
withContext: withTelemetryContext,
setContext: setTelemetryContext,
};
const harness = new KimiHarness({
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
telemetry: telemetryClient,
onOAuthRefresh: (outcome) => {
Expand Down Expand Up @@ -96,28 +97,15 @@ export async function runShell(
migrateOnly: runOptions.migrateOnly,
});

let firstLaunch = false;
const deviceId = createKimiDeviceId(harness.homeDir, {
onFirstLaunch: () => {
firstLaunch = true;
},
});
initializeTelemetry({
homeDir: harness.homeDir,
deviceId,
enabled: config.telemetry !== false,
appName: CLI_USER_AGENT_PRODUCT,
initializeCliTelemetry({
harness,
bootstrap: telemetryBootstrap,
config,
version,
uiMode: CLI_UI_MODE,
model: config.defaultModel,
getAccessToken: async () =>
(await harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
setCrashPhase('runtime');

if (firstLaunch) {
harness.track('first_launch');
}
const resumed = opts.continue || opts.session !== undefined;
const trackLifecycleForSession = (
sessionId: string,
Expand Down
26 changes: 14 additions & 12 deletions apps/kimi-code/src/cli/sub/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

import { createInterface } from 'node:readline/promises';

import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import {
initializeTelemetry,
setTelemetryContext,
shutdownTelemetry,
track,
Expand All @@ -24,7 +22,8 @@ import {
} from '@moonshot-ai/kimi-code-sdk';
import type { Command } from 'commander';

import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, CLI_USER_AGENT_PRODUCT } from '#/constant/app';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from '#/cli/telemetry';
import { createKimiCodeHostIdentity } from '#/cli/version';

interface WritableLike {
Expand Down Expand Up @@ -121,6 +120,7 @@ export function registerExportCommand(parent: Command, deps?: Partial<ExportDeps

function createDefaultExportDeps(overrides: Partial<ExportDeps> = {}): ExportDeps {
let harness: KimiHarness | undefined;
let telemetryBootstrap: ReturnType<typeof createCliTelemetryBootstrap> | undefined;
let telemetryInitialized = false;
let telemetryShutdown = false;
const identity = createKimiCodeHostIdentity();
Expand All @@ -129,29 +129,31 @@ function createDefaultExportDeps(overrides: Partial<ExportDeps> = {}): ExportDep
withContext: withTelemetryContext,
setContext: setTelemetryContext,
};
const getTelemetryBootstrap = (): ReturnType<typeof createCliTelemetryBootstrap> => {
telemetryBootstrap ??= createCliTelemetryBootstrap();
return telemetryBootstrap;
};
const getHarness = (): KimiHarness => {
const currentTelemetryBootstrap = getTelemetryBootstrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid creating first-launch marker during session lookup-only export flow

In createDefaultExportDeps, getHarness() now eagerly calls getTelemetryBootstrap(), and listSessions uses getHarness() even before export is confirmed. When users run kimi export without a session id and then cancel (or when no previous session exists), device_id can be created but initializeCliTelemetry() is never reached, so first_launch is permanently missed on the next real run. This is a telemetry correctness regression introduced by moving bootstrap creation into the lookup path.

Useful? React with 👍 / 👎.

harness ??= new KimiHarness({
homeDir: currentTelemetryBootstrap.homeDir,
identity,
telemetry: telemetryClient,
});
return harness;
};
const initializeDefaultTelemetry = async (): Promise<void> => {
if (telemetryInitialized) return;
const currentTelemetryBootstrap = getTelemetryBootstrap();
const currentHarness = getHarness();
await currentHarness.ensureConfigFile();
const config = await currentHarness.getConfig();
const deviceId = createKimiDeviceId(currentHarness.homeDir);
initializeTelemetry({
homeDir: currentHarness.homeDir,
deviceId,
enabled: config.telemetry !== false,
appName: CLI_USER_AGENT_PRODUCT,
initializeCliTelemetry({
harness: currentHarness,
bootstrap: currentTelemetryBootstrap,
config,
version: identity.version,
uiMode: CLI_UI_MODE,
model: config.defaultModel,
getAccessToken: async () =>
(await currentHarness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
telemetryInitialized = true;
};
Expand Down
48 changes: 48 additions & 0 deletions apps/kimi-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth';
import { initializeTelemetry } from '@moonshot-ai/kimi-telemetry';
import { resolveKimiHome, type KimiConfig, type KimiHarness } from '@moonshot-ai/kimi-code-sdk';

import { CLI_USER_AGENT_PRODUCT } from '#/constant/app';

export interface CliTelemetryBootstrap {
readonly homeDir: string;
readonly deviceId: string;
readonly firstLaunch: boolean;
}

export interface InitializeCliTelemetryOptions {
readonly harness: KimiHarness;
readonly bootstrap: CliTelemetryBootstrap;
readonly config: Pick<KimiConfig, 'defaultModel' | 'telemetry'>;
readonly version: string;
readonly uiMode: string;
readonly model?: string;
}

export function createCliTelemetryBootstrap(): CliTelemetryBootstrap {
let firstLaunch = false;
const homeDir = resolveKimiHome();
const deviceId = createKimiDeviceId(homeDir, {
onFirstLaunch: () => {
firstLaunch = true;
},
});
return { homeDir, deviceId, firstLaunch };
}

export function initializeCliTelemetry(options: InitializeCliTelemetryOptions): void {
initializeTelemetry({
homeDir: options.harness.homeDir,
deviceId: options.bootstrap.deviceId,
enabled: options.config.telemetry !== false,
appName: CLI_USER_AGENT_PRODUCT,
version: options.version,
uiMode: options.uiMode,
model: options.model ?? options.config.defaultModel,
getAccessToken: async () =>
(await options.harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
if (options.bootstrap.firstLaunch) {
options.harness.track('first_launch');
}
}
Loading
Loading