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/acp-auth-gate-process-env-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix `kimi -p` refusing to start when the provider key comes only from the environment: the auth readiness gate now falls back to `process.env` through the provider's declared `apiKeyEnv`, with explicit `config.toml` credentials (inline `apiKey`, `[providers.<id>.env]`, oauth) still taking precedence.
5 changes: 5 additions & 0 deletions .changeset/acp-internal-turn-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kimi-code': patch
---

Forward internally-triggered turns (cron, background-task notifications) as ACP session/update notifications even when no client prompt is in flight.
5 changes: 5 additions & 0 deletions .changeset/acp-mode-from-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kimi-code': patch
---

Report the engine's actual permission mode (and plan state) as the ACP current mode instead of a hardcoded `default`.
6 changes: 6 additions & 0 deletions .changeset/bash-subcommand-permission-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moonshot-ai/agent-core-v2': patch
'@moonshot-ai/agent-core': patch
---

Evaluate Bash permission rules per sub-command (tree-sitter-bash decomposition) so compound commands cannot over-grant allows or bypass denies.
5 changes: 5 additions & 0 deletions .changeset/block-turns-during-compaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/agent-core-v2': patch
---

Block turns while auto compaction is in flight so a commit-time history-safety race can no longer cancel the compaction and kill the user's turn.
6 changes: 6 additions & 0 deletions .changeset/context-overflow-401-classification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moonshot-ai/agent-core-v2': patch
'@moonshot-ai/agent-core': patch
---

Classify context-limit 401 responses ("supports only N context") as context overflow rather than auth, so no pointless token refresh is forced and ACP clients see the provider message instead of "Authentication required".
5 changes: 5 additions & 0 deletions .changeset/external-hooks-config-reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/agent-core-v2': patch
---

Rebuild the external-hooks index when [[hooks]] arrive through a config change so late hooks fire and the heartbeat arms.
6 changes: 6 additions & 0 deletions .changeset/glob-slash-crossing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moonshot-ai/agent-core-v2': patch
'@moonshot-ai/agent-core': patch
---

Match permission-rule glob subjects as opaque text (so `*` crosses `/` for commands, URLs, and search subjects) while path subjects keep path semantics.
5 changes: 5 additions & 0 deletions .changeset/non-tty-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kimi-code': patch
---

`kimi upgrade` in a non-TTY environment now runs the automatic install and reports failure with a non-zero exit instead of silently exiting 0.
5 changes: 5 additions & 0 deletions .changeset/prompt-plan-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kap-server': patch
---

Apply plan_mode from the prompt submission body so a new web session's first message actually enters plan mode.
5 changes: 5 additions & 0 deletions .changeset/select-tools-always-registered.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/agent-core-v2': patch
---

Always register the select_tools disclosure tool so the announced MCP selector is actually callable under an allowlist-bound profile.
6 changes: 6 additions & 0 deletions .changeset/session-agent-profile-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moonshot-ai/kimi-code-sdk': patch
'@moonshot-ai/kimi-code': patch
---

Bind the requested --agent/--agent-file profile on interactive session create and thread agentFiles through the harness so the profile's tools apply.
5 changes: 5 additions & 0 deletions .changeset/stream-stall-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/agent-core-v2': patch
---

Add a streaming idle/stall timeout (KIMI_CODE_STREAM_STALL_TIMEOUT_MS, default 300s) to the model-request path so a stalled provider stream aborts like a cancel and persists the partial content.
5 changes: 5 additions & 0 deletions .changeset/tui-wire-staleness-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

TUI: stop serving a stale in-memory transcript after an external client (ACP attach, mobile) appends turns to the session's wire journal. The send guard now compares the journal's newest user turn against the last turn the TUI rendered and refuses input — with a reload hint — when an unseen external turn would fork the session into two divergent histories. Re-selecting the current session from `/sessions` and `/reload` now re-read the journal from disk and re-hydrate the transcript instead of short-circuiting on the stale view.
5 changes: 5 additions & 0 deletions .changeset/wire-flush-before-dispose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/agent-core-v2': patch
---

Flush the replayable wire before disposing an agent scope on session close so the tail assistant records are never dropped.
5 changes: 5 additions & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export async function runShell(
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
skillDirs: opts.skillsDirs,
// `--agent-file` registers the file for this launch; the harness is built
// before any session exists, and the v2 engine reads explicit agent files
// from its own bootstrap arguments, so the registration has to happen here.
// Selecting the profile it defines stays per-session (`agentProfile`).
agentFiles: opts.agentFiles,
telemetry: telemetryClient,
onOAuthRefresh: (outcome) => {
if (outcome.success) {
Expand Down
50 changes: 48 additions & 2 deletions apps/kimi-code/src/cli/sub/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
NPM_PACKAGE_NAME,
type InstallSource,
type UpdateCache,
type UpdateTarget,
} from '#/cli/update/types';

interface WritableLike {
Expand Down Expand Up @@ -86,7 +87,36 @@ export async function handleUpgrade(

const source = await deps.detectInstallSource().catch(() => 'unsupported' as const);
const installCommand = installCommandFor(source, target.version, deps.platform);
if (!canAutoInstall(source, deps.platform) || !deps.isInteractive) {
if (!deps.isInteractive) {
if (!canAutoInstall(source, deps.platform)) {
// Non-interactive session with no automatic install path: report the
// no-op with a non-zero exit code so automation can detect that the
// upgrade did not happen, and put the manual instructions on stderr.
trackUpgradeEvent(deps.track, 'upgrade_command_manual_command', {
current_version: currentVersion,
target_version: target.version,
source,
});
logUpgradeWarn(deps.logger, 'manual upgrade unavailable in non-interactive session', {
currentVersion,
targetVersion: target.version,
source,
});
deps.stderr.write(renderManualUpdateMessage(currentVersion, target, source, installCommand));
return 1;
}
// Non-interactive but auto-install is supported: run the update like the
// automatic update path instead of printing instructions and exiting 0.
return runForegroundInstall(
deps,
currentVersion,
target,
source,
'upgrade_command_auto_install_started',
);
}

if (!canAutoInstall(source, deps.platform)) {
trackUpgradeEvent(deps.track, 'upgrade_command_manual_command', {
current_version: currentVersion,
target_version: target.version,
Expand Down Expand Up @@ -131,8 +161,24 @@ export async function handleUpgrade(
return 0;
}

return runForegroundInstall(
deps,
currentVersion,
target,
source,
'upgrade_command_install_selected',
);
}

async function runForegroundInstall(
deps: UpgradeDeps,
currentVersion: string,
target: UpdateTarget,
source: InstallSource,
startedEvent: string,
): Promise<number> {
try {
trackUpgradeEvent(deps.track, 'upgrade_command_install_selected', {
trackUpgradeEvent(deps.track, startedEvent, {
current_version: currentVersion,
target_version: target.version,
source,
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/tui/controllers/session-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export interface SessionEventHost {
handleShellOutput(event: { commandId: string; update: { kind: string; text?: string } }): void;
handleShellStarted(event: { commandId: string; taskId: string }): void;
sendNormalUserInput(text: string): void;
/** Re-read the active session's wire journal into the staleness baseline. */
refreshWireTipFromDisk(): void;
updateTerminalTitle(): void;
sendQueuedMessage(session: Session, item: QueuedMessage): void;
shiftQueuedMessage(): QueuedMessage | undefined;
Expand Down Expand Up @@ -406,6 +408,9 @@ export class SessionEventHandler {
}
this.pluginMcpToolsUsedInTurn.clear();
this.scheduleQueuedGoalPromotion();
// The turn's journal records are now on disk; advance the staleness
// baseline so the next input is not mistaken for an external append.
this.host.refreshWireTipFromDisk();
}

private handleStepBegin(event: TurnStepStartedEvent): void {
Expand Down
116 changes: 111 additions & 5 deletions apps/kimi-code/src/tui/kimi-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ import {
groupTurns,
turnsToTrim,
} from './utils/transcript-window';
import {
readWireTurnBoundaryTime,
wireTailAheadOfTranscript,
} from './utils/wire-staleness';

export type { TUIState } from './tui-state';
export { createTUIState } from './tui-state';
Expand Down Expand Up @@ -365,6 +369,16 @@ export class KimiTUI {
private currentLoadingTip: { kind: LoadingTipKind; tip: string | undefined } | undefined =
undefined;
private lastHistoryContent: string | undefined;
/**
* The newest user-turn timestamp this TUI has observed on the active
* session's wire journal (seeded at session switch, advanced after each turn
* ends). The send guard compares the live journal against this to detect
* turns appended by external clients. `undefined` until a session exposes a
* readable journal (fresh sessions start undefined and stay fail-open).
*/
private wireTipTime: number | undefined;
/** In-flight {@link refreshWireTipFromDisk} read, awaited by the send guard. */
private wireTipRefresh: Promise<void> | null = null;
// Live `!` shell output entries, keyed by commandId so concurrent commands
// each update their own card and stale events are dropped. Mutated in place
// as `shell.output` events arrive; removed when the command completes.
Expand Down Expand Up @@ -1318,6 +1332,56 @@ export class KimiTUI {
this.updateQueueDisplay();
}

/**
* Re-read the active session's newest wire user-turn into {@link wireTipTime}.
* Failures keep the last observed tip so the guard stays fail-open.
*/
refreshWireTipFromDisk(): void {
const sessionDir = this.session?.summary?.sessionDir;
if (sessionDir === undefined) return;
const task = readWireTurnBoundaryTime(sessionDir, MAIN_AGENT_ID)
.then((time) => {
this.wireTipTime = time;
})
.catch(() => {
// Keep the last observed tip; the guard fails open when unreadable.
});
this.wireTipRefresh = task;
void task.finally(() => {
if (this.wireTipRefresh === task) this.wireTipRefresh = null;
});
}

/**
* Block a prompt when an external client has appended a turn to the session's
* wire journal after this TUI last rendered one. Sending anyway would fork
* the session into two divergent histories, so the user is told to reload
* instead. Only meaningful while the session is idle — in-flight input is
* queued by the caller and must not be gated on the live journal.
*/
private async assertWireFresh(session: Session): Promise<boolean> {
if (this.state.appState.streamingPhase !== 'idle') return false;
// A tip refresh kicked off by the last turn may still be in flight; await
// it so the comparison never sees a stale baseline and false-positives.
if (this.wireTipRefresh !== null) {
await this.wireTipRefresh.catch(() => undefined);
}
const sessionDir = session.summary?.sessionDir;
if (sessionDir === undefined) return false;
const wireTailTime = await readWireTurnBoundaryTime(sessionDir, MAIN_AGENT_ID).catch(
() => undefined,
);
if (!wireTailAheadOfTranscript({ transcriptTipTime: this.wireTipTime, wireTailTime })) {
return false;
}
this.showStatus(
'Session was modified outside this terminal; your transcript is out of date.\n' +
`Reload with /reload, or resume fresh with: kimi -S ${quoteShellArg(session.id)}`,
'warning',
);
return true;
}

async sendNormalUserInput(text: string, preExtracted?: ExtractionResult): Promise<void> {
if (this.btwPanelController.sendUserInput(text)) return;
if (this.state.appState.model.trim().length === 0) {
Expand Down Expand Up @@ -1354,6 +1418,18 @@ export class KimiTUI {
session = await this.ensureSession();
if (session === undefined) return;
}
// An external client (ACP/mobile) may have appended turns to the session's
// wire journal while this TUI was idle. Sending now would continue from a
// stale context and silently fork the session, so refuse until the user
// reloads the session. Only sessions that expose an on-disk journal are
// checked: the guard is a fail-open no-op otherwise, and gating on
// `sessionDir` keeps the send path synchronous for journal-less sessions
// (the check must not interpose an await between submit and send).
if (session.summary?.sessionDir !== undefined && (await this.assertWireFresh(session))) {
this.updateQueueDisplay();
this.state.ui.requestRender();
return;
}
if (extraction.hasMedia) {
this.sendMessage(session, text, {
hasMedia: true,
Expand Down Expand Up @@ -1925,6 +2001,9 @@ export class KimiTUI {
this.harness.setTelemetryContext({ sessionId: session.id });
this.registerSessionHandlers(session);
this.syncAdditionalDirs(session);
// Seed the wire staleness baseline for the newly active session (fresh
// sessions yield `undefined` and stay fail-open until their first turn).
this.refreshWireTipFromDisk();
}

async syncRuntimeState(session: Session = this.requireSession()): Promise<void> {
Expand Down Expand Up @@ -2174,10 +2253,6 @@ export class KimiTUI {
// checks below see settled state — the pending prompt would otherwise
// replace the resumed session when creation completes.
await this.waitForLazyCreation();
if (targetSessionId === this.state.appState.sessionId) {
this.showStatus('Already on this session.');
return true;
}
if (this.state.appState.streamingPhase !== 'idle') {
this.showError('Cannot switch sessions while streaming — press Esc or Ctrl-C first.');
return false;
Expand All @@ -2186,6 +2261,23 @@ export class KimiTUI {
this.showError('Cannot switch sessions while history is replaying.');
return false;
}
if (targetSessionId === this.state.appState.sessionId) {
// Re-selecting the current session: an external client (ACP/mobile) may
// have appended turns to the wire journal since this TUI attached, and
// the in-memory transcript is stale. Reload from disk — the SDK closes
// the live scope and re-attaches, restoring the appended journal — and
// re-hydrate the transcript instead of short-circuiting.
const session = this.requireSession();
try {
await session.reloadSession({ forcePluginSessionStartReminder: true });
} catch (error) {
const msg = formatErrorMessage(error);
this.showError(`Failed to reload session ${targetSessionId}: ${msg}`);
return false;
}
await this.reloadCurrentSessionView(session, `Reloaded session (${session.id}).`);
return true;
}

let session: Session;
try {
Expand Down Expand Up @@ -2253,7 +2345,21 @@ export class KimiTUI {
} catch {
/* keep the reloaded session usable even if dynamic skills fail */
}
this.sessionEventHandler.startSubscription();
// A reload re-read the wire journal from disk (closing and re-attaching
// the live scope), so the fresh resume snapshot may contain turns appended
// by external clients (ACP/mobile) that the old in-memory view never saw.
// Clear the stale transcript and re-hydrate it; also re-seed the staleness
// baseline so the next input is not mistaken for an external append.
this.clearTranscriptAndRedraw();
try {
await this.sessionReplay.hydrateFromReplay(session);
} catch (error) {
const msg = formatErrorMessage(error);
this.showError(`Failed to replay session history: ${msg}`);
} finally {
this.sessionEventHandler.startSubscription();
}
this.refreshWireTipFromDisk();
const resumeState = session.getResumeState();
if (resumeState?.warning !== undefined) {
this.showStatus(`Warning: ${resumeState.warning}`, 'warning');
Expand Down
Loading
Loading