-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(kimi-code): support automatic updates for native installations via staged swap #2994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 37 commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
552e119
feat(kimi-code): support automatic updates for native installations v…
liruifengv 965e94b
fix(kimi-code): fully clean staged artifacts on swap discard paths
liruifengv 6a21aed
fix(kimi-code): restore staged metadata on swap failure and sweep upd…
liruifengv 4d3fbaf
Merge remote-tracking branch 'origin/main' into feat/native-staged-au…
liruifengv e8284b6
fix(kimi-code): address codex review on lock contention and swap cras…
liruifengv 5ee963b
Merge remote-tracking branch 'origin/feat/native-staged-auto-update' …
liruifengv 993dafa
test(kimi-code): align swap test expectation with smoke-before-rename…
liruifengv bec250b
fix(kimi-code): stage the bare CDN binary instead of unzipping
liruifengv f04b4c0
fix(kimi-code): address second codex review round
liruifengv 844f558
feat(kimi-code): show download progress for native updates
liruifengv 3cb0cc0
fix(kimi-code): bound native update downloads with an idle timeout
liruifengv ef97ea3
fix(kimi-code): retry native updates blocked by an orphaned active re…
liruifengv 11a913b
fix: skip staged swap while another instance holds a fresh claim
liruifengv bc747e0
fix: keep the install lock while its holder process is alive
liruifengv e630b0b
fix: keep recovery artifacts on rollback failure and wait out same-ve…
liruifengv 07b52bd
fix: stamp the swap claim with a fresh mtime when claiming
liruifengv 4019f20
fix: stamp the claim before the rename so it is born fresh
liruifengv 40fbbcc
fix: chmod the staged download before publishing it at its final name
liruifengv d4dcccd
fix: publish the install lock atomically via hard link
liruifengv 21d32be
fix: serialize stale-lock takeover through a secondary lock
liruifengv 32f0e33
fix: verify lock ownership after publish and preserve freshly staged …
liruifengv 41b3c0a
fix: reject mismatched manifests, take over from dead holders, unique…
liruifengv c8dbe15
fix: restrict staging cleanup to updater-owned names and retry short …
liruifengv e5aa04b
fix: scope failure cleanup, recognize all semvers, reverify staged ch…
liruifengv 37bbf47
fix: validate versions before path derivation and honor the update op…
liruifengv 8d8f3b9
fix: restrict backup cleanup to updater-owned .bak names
liruifengv d199e98
fix: claim staged metadata before validating it and let manual upgrad…
liruifengv 849145e
fix: promote adopted stages to manual and preserve claim-referenced p…
liruifengv 53dfffb
fix: drop exe deletion from stale-claim cleanup
liruifengv bdc9567
fix: never delete the staged exe when discarding a claim
liruifengv f1af868
fix: only reap staging orphans old enough to be abandoned
liruifengv 763ebcf
fix: honor the persisted auto-update preference in the swap and drop …
liruifengv b934466
fix: gate on claimed metadata, stop shared-path deletes on failure, e…
liruifengv 8168d5e
fix: confirm the manual marker before reporting stage adoption
liruifengv 7d2d52f
fix(cli): verify the staged payload digest before adopting it as alre…
liruifengv bd90100
fix(cli): keep staged metadata until its replacement is ready
liruifengv 0de8d01
fix(cli): preserve the exe referenced by the current staged record du…
liruifengv e539748
chore(kimi-code): reword the native auto-update changeset
liruifengv c01433d
chore(kimi-code): trim the native auto-update changeset
liruifengv eb71053
fix(cli): support update locking on filesystems without hard links
liruifengv 9c4cf49
fix(cli): publish staged exes under unique names and recover orphaned…
liruifengv cf5e0c8
fix(cli): verify the staged payload digest in the lock-wait adoption …
liruifengv fbc61f6
fix(cli): serialize swap critical sections and preserve in-flight pub…
liruifengv cfc6717
fix(cli): keep backup cleanup inside the swap mutex
liruifengv f576043
Merge branch 'main' into feat/native-staged-auto-update
liruifengv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": minor | ||
| --- | ||
|
|
||
| Support automatic updates for native (single-binary) installations, including Windows: new versions download in the background, verify against the release checksum, and swap in on the next launch. Run `kimi upgrade` to update now, or let the background updater handle it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| /** | ||
| * Hidden `kimi __update_download <version>` sub-command: the self-spawned | ||
| * worker behind native staged updates. Preflight spawns it detached in the | ||
| * background (and the `upgrade` command in the foreground); it downloads, | ||
| * verifies and stages the binary next to the running exe. The swap into | ||
| * place happens on the next startup (see `cli/update/native-swap.ts`). | ||
| */ | ||
|
|
||
| import { log } from '@moonshot-ai/kimi-code-sdk'; | ||
|
|
||
| import { | ||
| readUpdateInstallLockVersion, | ||
| tryAcquireUpdateInstallLock, | ||
| type UpdateInstallLockHandle, | ||
| } from '#/cli/update/install-lock'; | ||
| import { | ||
| promoteStagedUpdateToManual, | ||
| readStagedNativeUpdate, | ||
| stageNativeUpdate, | ||
| } from '#/cli/update/native-stage'; | ||
| import { detectNativeInstall } from '#/cli/update/source'; | ||
|
|
||
| const LOCK_HELD_POLL_INTERVAL_MS = 2_000; | ||
|
|
||
| type StagedUpdateWait = | ||
| | { readonly status: 'staged' } | ||
| | { readonly status: 'takeover'; readonly lock: UpdateInstallLockHandle | null }; | ||
|
|
||
| /** | ||
| * Another worker holds the install lock for the SAME version. Returning right | ||
| * away would report a success that has not happened yet — the in-flight | ||
| * download may still fail — so wait for it: 'staged' once its staged update is | ||
| * verified on disk; 'takeover' once the lock becomes acquirable, with the lock | ||
| * already held for the caller. The lock goes stale the moment its holder dies | ||
| * (see install-lock), so a killed downloader cannot strand a foreground | ||
| * `kimi upgrade` in this loop. | ||
| * | ||
| * A manual (explicit-upgrade) waiter adopts only after CONFIRMING the manual | ||
| * marker landed on the stage — a concurrent startup swap may be claiming and | ||
| * restoring the metadata right now, and reporting adoption for a promotion | ||
| * that never persisted would strand the update under the env opt-out. | ||
| */ | ||
| async function waitForStagedUpdate( | ||
| version: string, | ||
| exePath: string, | ||
| manual: boolean, | ||
| ): Promise<StagedUpdateWait> { | ||
| for (;;) { | ||
| const staged = await readStagedNativeUpdate(exePath); | ||
| if (staged !== null && staged.version === version) { | ||
| if (!manual || (await promoteStagedUpdateToManual(exePath, staged))) { | ||
| return { status: 'staged' }; | ||
| } | ||
| // The stage is being claimed/restored by a concurrent swap — the next | ||
| // poll either promotes the restored stage or takes over once it is | ||
| // gone. | ||
| } else { | ||
| // Poll the acquisition itself: while the holder lives its lock stays | ||
| // fresh and this returns null without side effects; when the holder | ||
| // finishes (or dies) without staging, the takeover happens right here. | ||
| const lock = await tryAcquireUpdateInstallLock({ version }); | ||
| if (lock !== null) return { status: 'takeover', lock }; | ||
| } | ||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, LOCK_HELD_POLL_INTERVAL_MS); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export async function runUpdateDownloadCommand( | ||
| version: string, | ||
| manual: boolean = false, | ||
| ): Promise<number> { | ||
| if (!detectNativeInstall()) { | ||
| process.stderr.write('error: update download is only available in the native build\n'); | ||
| return 1; | ||
| } | ||
| const out = process.stdout; | ||
| let lock = await tryAcquireUpdateInstallLock({ version }); | ||
|
liruifengv marked this conversation as resolved.
liruifengv marked this conversation as resolved.
|
||
| if (lock === null) { | ||
| const holderVersion = await readUpdateInstallLockVersion(); | ||
| if (holderVersion === version) { | ||
| // Another worker is already downloading this exact version: wait for it | ||
| // and adopt its verified result instead of exiting on a maybe. | ||
| out.write( | ||
| `A download of Kimi Code ${version} is already in progress; waiting for it to finish…\n`, | ||
| ); | ||
| const wait = await waitForStagedUpdate(version, process.execPath, manual); | ||
| if (wait.status === 'staged') { | ||
| out.write(`Kimi Code ${version} is downloaded; it applies on the next start.\n`); | ||
| return 0; | ||
| } | ||
| // The holder finished without staging (failed or died): take over. The | ||
| // lock may already be held by another winner of the takeover race — | ||
| // the null check below reports that as held. | ||
| lock = wait.lock; | ||
| } else if (holderVersion === undefined) { | ||
| // The lock was released between the two reads — retry the acquire once. | ||
| lock = await tryAcquireUpdateInstallLock({ version }); | ||
| } | ||
| if (lock === null) { | ||
| process.stderr.write( | ||
| `error: another update (${holderVersion ?? 'unknown version'}) is already downloading\n`, | ||
| ); | ||
| return 1; | ||
| } | ||
| } | ||
| const label = `Downloading Kimi Code ${version} (${process.platform}-${process.arch})…`; | ||
| const onProgress = createDownloadProgress(out, label); | ||
| try { | ||
| const result = await stageNativeUpdate({ | ||
| version, | ||
| exePath: process.execPath, | ||
| onProgress, | ||
| manual, | ||
| }); | ||
| if (out.isTTY) out.write('\n'); | ||
| if (result.status === 'already-staged') { | ||
| out.write(`Kimi Code ${version} is already downloaded; it applies on the next start.\n`); | ||
| } | ||
| return 0; | ||
| } catch (error) { | ||
| if (out.isTTY) out.write('\n'); | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| process.stderr.write(`error: failed to download update ${version}: ${message}\n`); | ||
| log.warn('native update download failed', { version, error: message }); | ||
| return 1; | ||
| } finally { | ||
| await lock.release().catch(() => {}); | ||
| } | ||
| } | ||
|
|
||
| const PROGRESS_FRAME_INTERVAL_MS = 100; | ||
| const PROGRESS_LINE_INTERVAL_BYTES = 32 * 1024 * 1024; | ||
|
|
||
| function formatDownloadProgress(label: string, downloaded: number, total: number | null): string { | ||
| const mb = Math.floor(downloaded / (1024 * 1024)); | ||
| if (total === null || total <= 0) return `${label} ${mb} MB`; | ||
| const totalMb = Math.max(1, Math.round(total / (1024 * 1024))); | ||
| const percent = Math.min(100, Math.floor((downloaded / total) * 100)); | ||
| return `${label} ${percent}% (${mb}/${totalMb} MB)`; | ||
| } | ||
|
|
||
| /** | ||
| * Download progress renderer for the (foreground) downloader: a single | ||
| * in-place line on a TTY (`\r` + clear-line, throttled to 10 fps, final frame | ||
| * always rendered), or one line per 32 MB when piped to a file. The caller | ||
| * owns the trailing newline. | ||
| */ | ||
| export function createDownloadProgress( | ||
| out: NodeJS.WriteStream, | ||
| label: string, | ||
| ): (downloadedBytes: number, totalBytes: number | null) => void { | ||
| const isTTY = out.isTTY; | ||
| let lastFrameAt = 0; | ||
| let lastLineAt = 0; | ||
| if (!isTTY) out.write(`${label}\n`); | ||
| return (downloaded, total) => { | ||
| const done = total !== null && downloaded >= total; | ||
| if (isTTY) { | ||
| const now = Date.now(); | ||
| if (!done && now - lastFrameAt < PROGRESS_FRAME_INTERVAL_MS) return; | ||
| lastFrameAt = now; | ||
| out.write(`\r\u001B[K${formatDownloadProgress(label, downloaded, total)}`); | ||
| return; | ||
| } | ||
| if (!done && downloaded - lastLineAt < PROGRESS_LINE_INTERVAL_BYTES) return; | ||
| lastLineAt = downloaded; | ||
| out.write(`${formatDownloadProgress(label, downloaded, total)}\n`); | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.