mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
chore(telemetry): normalize telemetry property keys to snake_case (#1196)
- Rename camelCase telemetry keys to snake_case on compaction_finished, compaction_failed, micro_compaction_finished, and the tool error event (tokens_before, tokens_after, compacted_count, retry_count, thinking_level, error_type, input_tokens/output_tokens, and the micro compaction config/effect keys). - Emit a fixed client-attribution key set (client_id/name/version/ui_mode, null when absent) from both session_started producers (core-impl and kimi-harness) so they share a stable schema. - Drop the duplicate current/latest keys on update_prompted and the redundant ui_mode on server_started. - Additive fields: login.method=oauth and question_answered.answered. Telemetry-only change; no changeset.
This commit is contained in:
parent
3e98e709b3
commit
10ffb7d9f9
15 changed files with 110 additions and 85 deletions
|
|
@ -18,7 +18,7 @@ import { startServer, type RunningServer } from '@moonshot-ai/server';
|
|||
import chalk from 'chalk';
|
||||
import { Option, type Command } from 'commander';
|
||||
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS, WEB_UI_MODE } from '#/constant/app';
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';
|
||||
import { getNativeWebAssetsDir } from '#/native/web-assets';
|
||||
import { darkColors } from '#/tui/theme/colors';
|
||||
import { openUrl as defaultOpenUrl } from '#/utils/open-url';
|
||||
|
|
@ -346,7 +346,7 @@ async function runServerInProcess(
|
|||
},
|
||||
});
|
||||
|
||||
track('server_started', { ui_mode: WEB_UI_MODE, daemon: mode.daemon });
|
||||
track('server_started', { daemon: mode.daemon });
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
void shutdown('SIGINT');
|
||||
|
|
|
|||
|
|
@ -430,8 +430,6 @@ function trackUpdatePrompted(
|
|||
rolloutTelemetry: RolloutTelemetry,
|
||||
): void {
|
||||
trackUpdateEvent(track, 'update_prompted', {
|
||||
current: currentVersion,
|
||||
latest: target.version,
|
||||
current_version: currentVersion,
|
||||
target_version: target.version,
|
||||
source,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ async function handleKimiCodeOAuthLogin(host: SlashCommandHost): Promise<void> {
|
|||
}
|
||||
host.track('login', {
|
||||
provider: DEFAULT_OAUTH_PROVIDER_NAME,
|
||||
method: 'oauth',
|
||||
already_logged_in: alreadyLoggedIn,
|
||||
});
|
||||
if (alreadyLoggedIn) {
|
||||
|
|
|
|||
|
|
@ -828,8 +828,8 @@ describe('runUpdatePreflight', () => {
|
|||
const track = vi.fn();
|
||||
await runUpdatePreflight('0.4.0', { ...options, track });
|
||||
expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({
|
||||
current: '0.4.0',
|
||||
latest: '0.5.0',
|
||||
current_version: '0.4.0',
|
||||
target_version: '0.5.0',
|
||||
decision: 'prompt-install',
|
||||
source: 'npm-global',
|
||||
}));
|
||||
|
|
@ -915,7 +915,7 @@ describe('runUpdatePreflight', () => {
|
|||
expect.objectContaining({ target: { version: '0.5.0' } }),
|
||||
);
|
||||
expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({
|
||||
latest: '0.5.0',
|
||||
target_version: '0.5.0',
|
||||
rollout_bucket: expect.any(Number),
|
||||
rollout_delay_seconds: 0,
|
||||
rollout_from_manifest: true,
|
||||
|
|
@ -945,7 +945,7 @@ describe('runUpdatePreflight', () => {
|
|||
expect.objectContaining({ target: { version: '0.7.0' } }),
|
||||
);
|
||||
expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({
|
||||
latest: '0.7.0',
|
||||
target_version: '0.7.0',
|
||||
rollout_bucket: expect.any(Number),
|
||||
rollout_delay_seconds: 43_200,
|
||||
rollout_from_manifest: true,
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,7 @@ describe('KimiTUI startup', () => {
|
|||
});
|
||||
expect(harness.track).toHaveBeenCalledWith('login', {
|
||||
provider: 'managed:kimi-code',
|
||||
method: 'oauth',
|
||||
already_logged_in: false,
|
||||
});
|
||||
});
|
||||
|
|
@ -1302,6 +1303,7 @@ describe('KimiTUI startup', () => {
|
|||
);
|
||||
expect(harness.track).toHaveBeenCalledWith('login', {
|
||||
provider: 'managed:kimi-code',
|
||||
method: 'oauth',
|
||||
already_logged_in: true,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1314,7 +1314,7 @@ export class AcpSession {
|
|||
// event so dashboards stay coherent.
|
||||
this.emitTelemetry('question_dismissed');
|
||||
} else {
|
||||
this.emitTelemetry('question_answered');
|
||||
this.emitTelemetry('question_answered', { answered: Object.keys(answer).length });
|
||||
}
|
||||
return answer;
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ describe('AcpSession.handleQuestion', () => {
|
|||
expect(req.toolCall.content).toEqual([
|
||||
{ type: 'content', content: { type: 'text', text: '哪个口味?' } },
|
||||
]);
|
||||
expect(trackCalls).toEqual([{ event: 'question_answered', properties: undefined }]);
|
||||
expect(trackCalls).toEqual([{ event: 'question_answered', properties: { answered: 1 } }]);
|
||||
});
|
||||
|
||||
it('skip: q0_skip resolves to null with question_dismissed telemetry', async () => {
|
||||
|
|
@ -207,7 +207,7 @@ describe('AcpSession.handleQuestion', () => {
|
|||
// Telemetry: degraded(multi_question) first, then answered.
|
||||
expect(trackCalls).toEqual([
|
||||
{ event: 'question_degraded', properties: { reason: 'multi_question', dropped: 2 } },
|
||||
{ event: 'question_answered', properties: undefined },
|
||||
{ event: 'question_answered', properties: { answered: 1 } },
|
||||
]);
|
||||
// log.warn fired with the dropped count.
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
|
|
@ -236,7 +236,7 @@ describe('AcpSession.handleQuestion', () => {
|
|||
expect(raw.permissionRequests).toHaveLength(1);
|
||||
expect(trackCalls).toEqual([
|
||||
{ event: 'question_degraded', properties: { reason: 'multi_select' } },
|
||||
{ event: 'question_answered', properties: undefined },
|
||||
{ event: 'question_answered', properties: { answered: 1 } },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '#/errors';
|
||||
import {
|
||||
APIEmptyResponseError,
|
||||
inputTotal,
|
||||
isRetryableGenerateError,
|
||||
type GenerateResult,
|
||||
type Message,
|
||||
|
|
@ -413,29 +414,35 @@ export class FullCompaction {
|
|||
tokensAfter,
|
||||
};
|
||||
|
||||
// Telemetry keys are snake_case, but the `context.apply_compaction`
|
||||
// record written below keeps its persisted camelCase field names
|
||||
// (consumed by external projectors). The two channels intentionally
|
||||
// diverge — don't rename the record side to match.
|
||||
this.agent.telemetry.track('compaction_finished', {
|
||||
tokensBefore: result.tokensBefore,
|
||||
tokensAfter: result.tokensAfter,
|
||||
source: data.source,
|
||||
tokens_before: result.tokensBefore,
|
||||
tokens_after: result.tokensAfter,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
compactedCount: result.compactedCount,
|
||||
retryCount,
|
||||
compacted_count: result.compactedCount,
|
||||
retry_count: retryCount,
|
||||
round,
|
||||
thinkingLevel: this.agent.config.thinkingLevel,
|
||||
...usage,
|
||||
...data,
|
||||
thinking_level: this.agent.config.thinkingLevel,
|
||||
...(usage === null
|
||||
? {}
|
||||
: { input_tokens: inputTotal(usage), output_tokens: usage.output }),
|
||||
});
|
||||
this.agent.context.applyCompaction(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) return;
|
||||
this.agent.telemetry.track('compaction_failed', {
|
||||
...data,
|
||||
tokensBefore,
|
||||
source: data.source,
|
||||
tokens_before: tokensBefore,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
round,
|
||||
retryCount,
|
||||
thinkingLevel: this.agent.config.thinkingLevel,
|
||||
errorType: error instanceof Error ? error.name : 'Unknown',
|
||||
retry_count: retryCount,
|
||||
thinking_level: this.agent.config.thinkingLevel,
|
||||
error_type: error instanceof Error ? error.name : 'Unknown',
|
||||
});
|
||||
if (isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED) throw error;
|
||||
throw new KimiError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error });
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class MicroCompaction {
|
|||
const previousEffect = this.measureEffect(history, previousCutoff);
|
||||
const rawContextTokens = estimateTokensForMessages(history);
|
||||
// Whole-context length before/after this cutoff change, mirroring the
|
||||
// `tokensBefore`/`tokensAfter` fields on `compaction_finished` so the
|
||||
// `tokens_before`/`tokens_after` fields on `compaction_finished` so the
|
||||
// two compaction paths can be compared on the same axis.
|
||||
const tokensBefore =
|
||||
rawContextTokens -
|
||||
|
|
@ -82,15 +82,21 @@ export class MicroCompaction {
|
|||
effect.truncatedToolResultTokensBefore +
|
||||
effect.truncatedToolResultTokensAfter;
|
||||
this.agent.telemetry.track('micro_compaction_finished', {
|
||||
...config,
|
||||
...effect,
|
||||
tokensBefore,
|
||||
tokensAfter,
|
||||
keep_recent_messages: config.keepRecentMessages,
|
||||
min_content_tokens: config.minContentTokens,
|
||||
cache_missed_threshold_ms: config.cacheMissedThresholdMs,
|
||||
truncated_marker: config.truncatedMarker,
|
||||
min_context_usage_ratio: config.minContextUsageRatio,
|
||||
truncated_tool_result_count: effect.truncatedToolResultCount,
|
||||
truncated_tool_result_tokens_before: effect.truncatedToolResultTokensBefore,
|
||||
truncated_tool_result_tokens_after: effect.truncatedToolResultTokensAfter,
|
||||
tokens_before: tokensBefore,
|
||||
tokens_after: tokensAfter,
|
||||
previous_cutoff: previousCutoff,
|
||||
cutoff: nextCutoff,
|
||||
message_count: history.length,
|
||||
cache_age_ms: cacheAgeMs,
|
||||
thinkingLevel: this.agent.config.thinkingLevel,
|
||||
thinking_level: this.agent.config.thinkingLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,7 +566,7 @@ export class ToolManager {
|
|||
...base,
|
||||
outcome: 'error',
|
||||
duration_ms: Date.now() - startedAt,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error_type: error instanceof Error ? error.name : 'Unknown',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1141,19 +1141,16 @@ function telemetryErrorReason(error: unknown): string {
|
|||
|
||||
function clientTelemetryProperties(client: ClientTelemetryInfo | undefined): TelemetryProperties {
|
||||
if (client === undefined) return {};
|
||||
const properties: Record<string, string> = {};
|
||||
addNonEmpty(properties, 'client_id', client.id);
|
||||
addNonEmpty(properties, 'client_name', client.name);
|
||||
addNonEmpty(properties, 'client_version', client.version);
|
||||
addNonEmpty(properties, 'ui_mode', client.uiMode);
|
||||
return properties;
|
||||
}
|
||||
|
||||
function addNonEmpty(target: Record<string, string>, key: string, value: string | undefined): void {
|
||||
const trimmed = value?.trim();
|
||||
if (trimmed !== undefined && trimmed.length > 0) {
|
||||
target[key] = trimmed;
|
||||
}
|
||||
// Emit a fixed key set (null when the client did not provide a field) so
|
||||
// `session_started` has a stable schema across clients, matching the harness
|
||||
// producer in `kimi-harness.ts`. Other session events also inherit these as
|
||||
// context properties, so they share the same stable client-attribution shape.
|
||||
return {
|
||||
client_id: client.id ?? null,
|
||||
client_name: client.name ?? null,
|
||||
client_version: client.version ?? null,
|
||||
ui_mode: client.uiMode ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function resumeSessionResult(
|
||||
|
|
|
|||
|
|
@ -235,17 +235,14 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_finished',
|
||||
properties: expect.objectContaining({
|
||||
source: 'manual',
|
||||
instruction: 'Keep the important test facts.',
|
||||
tokensBefore: 39,
|
||||
tokensAfter: 5,
|
||||
tokens_before: 39,
|
||||
tokens_after: 5,
|
||||
duration_ms: expect.any(Number),
|
||||
compactedCount: 6,
|
||||
retryCount: 0,
|
||||
thinkingLevel: 'off',
|
||||
inputOther: 520,
|
||||
output: 8,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
compacted_count: 6,
|
||||
retry_count: 0,
|
||||
thinking_level: 'off',
|
||||
input_tokens: 520,
|
||||
output_tokens: 8,
|
||||
}),
|
||||
});
|
||||
await ctx.expectResumeMatches();
|
||||
|
|
@ -514,8 +511,8 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_finished',
|
||||
properties: expect.objectContaining({
|
||||
source: 'manual',
|
||||
tokensBefore: 25,
|
||||
retryCount: 1,
|
||||
tokens_before: 25,
|
||||
retry_count: 1,
|
||||
}),
|
||||
});
|
||||
await ctx.expectResumeMatches();
|
||||
|
|
@ -650,8 +647,8 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_failed',
|
||||
properties: expect.objectContaining({
|
||||
source: 'manual',
|
||||
retryCount: 4,
|
||||
errorType: 'APIEmptyResponseError',
|
||||
retry_count: 4,
|
||||
error_type: 'APIEmptyResponseError',
|
||||
}),
|
||||
});
|
||||
// No summary was ever applied; the original history is left intact.
|
||||
|
|
@ -764,16 +761,16 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_failed',
|
||||
properties: expect.objectContaining({
|
||||
source: 'manual',
|
||||
tokensBefore: 25,
|
||||
tokens_before: 25,
|
||||
duration_ms: expect.any(Number),
|
||||
round: 1,
|
||||
retryCount: 0,
|
||||
errorType: 'Error',
|
||||
retry_count: 0,
|
||||
error_type: 'Error',
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
records.find((record) => record.event === 'compaction_failed')?.properties,
|
||||
).not.toHaveProperty('tokensAfter');
|
||||
).not.toHaveProperty('tokens_after');
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
|
|
@ -878,10 +875,10 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_failed',
|
||||
properties: expect.objectContaining({
|
||||
source: 'manual',
|
||||
tokensBefore: 25,
|
||||
tokens_before: 25,
|
||||
duration_ms: expect.any(Number),
|
||||
retryCount: 4,
|
||||
errorType: 'APIConnectionError',
|
||||
retry_count: 4,
|
||||
error_type: 'APIConnectionError',
|
||||
}),
|
||||
});
|
||||
await ctx.expectResumeMatches();
|
||||
|
|
@ -1212,10 +1209,10 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_finished',
|
||||
properties: expect.objectContaining({
|
||||
source: 'auto',
|
||||
tokensBefore: 46,
|
||||
tokensAfter: 28,
|
||||
compactedCount: 4,
|
||||
retryCount: 0,
|
||||
tokens_before: 46,
|
||||
tokens_after: 28,
|
||||
compacted_count: 4,
|
||||
retry_count: 0,
|
||||
}),
|
||||
});
|
||||
await ctx.expectResumeMatches();
|
||||
|
|
@ -1740,7 +1737,7 @@ describe('FullCompaction', () => {
|
|||
event: 'compaction_finished',
|
||||
properties: expect.objectContaining({
|
||||
source: 'auto',
|
||||
thinkingLevel: 'high',
|
||||
thinking_level: 'high',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -471,24 +471,27 @@ describe('MicroCompaction', () => {
|
|||
|
||||
const event = singleTelemetryEvent(records, 'micro_compaction_finished');
|
||||
expect(event.properties).toMatchObject({
|
||||
...microCompaction,
|
||||
truncatedMarker: DEFAULT_MARKER,
|
||||
keep_recent_messages: microCompaction.keepRecentMessages,
|
||||
min_content_tokens: microCompaction.minContentTokens,
|
||||
cache_missed_threshold_ms: microCompaction.cacheMissedThresholdMs,
|
||||
truncated_marker: DEFAULT_MARKER,
|
||||
min_context_usage_ratio: microCompaction.minContextUsageRatio,
|
||||
previous_cutoff: 0,
|
||||
cutoff: 7,
|
||||
message_count: 9,
|
||||
cache_age_ms: 61 * MINUTE,
|
||||
truncatedToolResultCount: 2,
|
||||
truncatedToolResultTokensBefore: expect.any(Number),
|
||||
truncatedToolResultTokensAfter: expect.any(Number),
|
||||
tokensBefore: expect.any(Number),
|
||||
tokensAfter: expect.any(Number),
|
||||
thinkingLevel: 'off',
|
||||
truncated_tool_result_count: 2,
|
||||
truncated_tool_result_tokens_before: expect.any(Number),
|
||||
truncated_tool_result_tokens_after: expect.any(Number),
|
||||
tokens_before: expect.any(Number),
|
||||
tokens_after: expect.any(Number),
|
||||
thinking_level: 'off',
|
||||
});
|
||||
expect(numberProperty(event, 'truncatedToolResultTokensBefore')).toBeGreaterThan(
|
||||
numberProperty(event, 'truncatedToolResultTokensAfter'),
|
||||
expect(numberProperty(event, 'truncated_tool_result_tokens_before')).toBeGreaterThan(
|
||||
numberProperty(event, 'truncated_tool_result_tokens_after'),
|
||||
);
|
||||
expect(numberProperty(event, 'tokensBefore')).toBeGreaterThan(
|
||||
numberProperty(event, 'tokensAfter'),
|
||||
expect(numberProperty(event, 'tokens_before')).toBeGreaterThan(
|
||||
numberProperty(event, 'tokens_after'),
|
||||
);
|
||||
|
||||
expect(ctx.agent.context.messages).toHaveLength(9);
|
||||
|
|
@ -533,9 +536,9 @@ describe('MicroCompaction', () => {
|
|||
expect(secondEvent.properties).toMatchObject({
|
||||
previous_cutoff: 4,
|
||||
cutoff: 7,
|
||||
truncatedToolResultCount: 2,
|
||||
tokensBefore: expectedContextTokensBefore,
|
||||
tokensAfter: estimateTokensForMessages(ctx.agent.context.messages),
|
||||
truncated_tool_result_count: 2,
|
||||
tokens_before: expectedContextTokensBefore,
|
||||
tokens_after: estimateTokensForMessages(ctx.agent.context.messages),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -265,6 +265,11 @@ export class KimiHarness {
|
|||
...sessionScoped,
|
||||
// Canonical fields are owned by the harness and must win over any
|
||||
// caller-supplied sessionStartedProperties that happen to share a key.
|
||||
// `client_id` is always null here: a single-process host has no
|
||||
// per-connection client id (that concept only exists for daemon clients,
|
||||
// see core-impl.ts). Kept as an explicit key so both producers share the
|
||||
// same session_started schema.
|
||||
client_id: null,
|
||||
client_name: this.identity?.userAgentProduct ?? null,
|
||||
client_version: this.identity?.version ?? null,
|
||||
ui_mode: this.uiMode,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -132,6 +133,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -169,6 +171,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'print',
|
||||
|
|
@ -201,6 +204,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -236,6 +240,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -255,6 +260,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -295,6 +301,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -333,6 +340,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: forked.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: 'kimi-code-cli',
|
||||
client_version: '0.0.0-test',
|
||||
ui_mode: 'shell',
|
||||
|
|
@ -368,6 +376,7 @@ describe('KimiHarness.createSession transport link', () => {
|
|||
event: 'session_started',
|
||||
sessionId: session.id,
|
||||
properties: {
|
||||
client_id: null,
|
||||
client_name: null,
|
||||
client_version: null,
|
||||
ui_mode: 'shell',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue