From e6bbc37fb46efdaae798c59612db5fb0a0b43e42 Mon Sep 17 00:00:00 2001 From: bbingz Date: Tue, 2 Jun 2026 12:07:50 +0800 Subject: [PATCH 1/2] feat(kimi): migrate to kimi-code v0.6.0 CLI (drops legacy python kimi-cli contract) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local `kimi` binary is now kimi-code v0.6.0 (~/.kimi-code), not the python kimi-cli. Its one-shot `-p` mode rejects --yolo/--auto/--plan and dropped --print/--input-format/--no-thinking/ --max-steps-per-turn, and the session id is now `session_` emitted structurally. Runtime (packages/polycli-runtime/src/kimi.js, full rewrite): - buildKimiInvocation: `-p --output-format stream-json` (+ -m, + -r / -C). No --yolo (rejected with -p), no --print/--input-format (removed upstream). - session id read STRUCTURALLY from the `{role:"meta",type:"session.resume_hint",session_id}` event (keeps the `session_` prefix; never scans prose for a bare UUID -> no fabrication). - deleted the dead resume machinery (resolveKimiResumeSession/readKimiLastSession/validateKimiResume Target/kimi.json/md5-sessions-dir) — resume is delegated to the CLI; resumeLast -> -C, resume -> -r. Also drops the resume-mismatch warning (a python-kimi quirk guard). - auth probe drops --max-steps-per-turn (now config.toml-level) and normalizes ETIMEDOUT so a probe timeout stays inconclusive. default-model + config path -> ~/.kimi-code/config.toml. Host + review: - sessions.mjs kimi purge derivation -> ~/.kimi-code/sessions/wd__// (verified on disk); storeRoot kimi -> .kimi-code/sessions. - kimi /review is now prompt-only (like minimax): -p mode has no flag-based read-only lever, so REVIEW_HARD_CONSTRAINTS.kimi() returns {} and REVIEW_FLAG_EXPECTATIONS.kimi drops the stale --no-thinking/--max-steps tokens. A drift CHECK now guards the load-bearing -p/--output-format invocation flags (resolves the review-drift "kimi uncovered" finding). - prompt-runtime ask path for kimi is unconstrained (no -p-compatible constraint flags exist). Tests: kimi.test.js rewritten for the new contract; integration/prompt-runtime/sessions/review-flags /consistency/exports tests updated. npm test 451/451; companion bundles byte-identical. Doc-debt (follow-up): the kimi-cli-runtime / kimi-prompting skill prose + provider-paths.md + polycli-v1-public-surface.md still describe the legacy python kimi-cli flags; advisory only, not the code contract. Tracked in memory project_deep_review_2026_06_02. --- .gitignore | 3 + packages/polycli-runtime/src/kimi.js | 291 +++--------------- packages/polycli-runtime/src/review-flags.js | 10 +- packages/polycli-runtime/test/exports.test.js | 1 - packages/polycli-runtime/test/kimi.test.js | 235 +++++--------- .../polycli-runtime/test/review-flags.test.js | 5 +- .../bin/polycli-companion.bundle.mjs | 288 +++-------------- .../scripts/polycli-companion.bundle.mjs | 288 +++-------------- .../scripts/polycli-companion.bundle.mjs | 288 +++-------------- .../scripts/polycli-companion.bundle.mjs | 288 +++-------------- .../polycli/scripts/lib/prompt-runtime.mjs | 10 +- plugins/polycli/scripts/lib/review.mjs | 5 +- plugins/polycli/scripts/lib/sessions.mjs | 19 +- .../scripts/polycli-companion.bundle.mjs | 288 +++-------------- .../scripts/tests/integration.test.mjs | 83 +---- .../scripts/tests/prompt-runtime.test.mjs | 6 +- .../tests/review-flags-consistency.test.mjs | 5 +- .../polycli/scripts/tests/sessions.test.mjs | 18 +- scripts/check-review-cli-drift.mjs | 7 + 19 files changed, 421 insertions(+), 1717 deletions(-) diff --git a/.gitignore b/.gitignore index 039a37e..16be833 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ dist/ # Claude Code worktree state (agent isolation runs) .claude/ + +# CodeGraph local index (not committed) +.codegraph/ diff --git a/packages/polycli-runtime/src/kimi.js b/packages/polycli-runtime/src/kimi.js index 1135780..eaa3c12 100644 --- a/packages/polycli-runtime/src/kimi.js +++ b/packages/polycli-runtime/src/kimi.js @@ -1,10 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; import { binaryAvailable, runCommand } from "@bbingz/polycli-utils/process"; -import { resolveSessionId } from "@bbingz/polycli-utils/session-id"; import { classifyProviderFailure, formatProviderExitError } from "./errors.js"; import { spawnStreamingCommand } from "./spawn.js"; @@ -12,168 +10,13 @@ import { spawnStreamingCommand } from "./spawn.js"; const KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; const DEFAULT_TIMEOUT_MS = 900_000; const AUTH_CHECK_TIMEOUT_MS = 30_000; -const PROMPT_STDIN_THRESHOLD_BYTES = 100_000; const KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -const KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export const TRANSIENT_PROBE_ERROR_PATTERNS = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i, ]; -const KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); - -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} - -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} - -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} - -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} - -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} - -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` — ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} - -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} - -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found", - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} - -export function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false, -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null, - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || (!resumeSessionId && !resumeLast)) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd, - }; - } - - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd, - }; - } - sessionId = last.sessionId; - } - - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode, - }), - reason: validation.reason, - cwdHash, - realCwd, - }; - } - - return { ok: true, sessionId, cwdHash, realCwd }; -} +// kimi-code v0.6.0 stores its default model in ~/.kimi-code/config.toml (the legacy python +// kimi-cli used ~/.kimi/config.toml; that install is migrated, marker `.migrated-to-kimi-code`). +const KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { @@ -189,31 +32,23 @@ export function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN, } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - - if (yolo) args.push("--yolo"); + // kimi-code one-shot mode: `-p --output-format stream-json`. NOTE: `-p` cannot be + // combined with `--yolo`, `--auto`, or `--plan` (the CLI rejects them) — `-p` is itself the + // non-interactive headless runner, so no approval flag is passed. Resume is delegated to the + // CLI: `-r ` (per the CLI's own session.resume_hint) or `-C` to continue the last session. + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - - return { - bin, - args, - input: useStdin ? promptText : undefined, - useStdin, - }; + return { bin, args }; } function parseKimiEventLine(line) { @@ -248,18 +83,29 @@ export function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + // The session id arrives STRUCTURALLY in a `{role:"meta", type:"session.resume_hint", + // session_id:"session_"}` event. Read it from there and keep the `session_` prefix + // intact — never scan the prose for a bare UUID (that would drop the prefix and could + // fabricate an id from a UUID the user asked about). + if (!sessionId + && event.role === "meta" + && typeof event.session_id === "string" + && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } export function getKimiAvailability(cwd) { @@ -287,11 +133,12 @@ function buildKimiAuthStatus(result) { } export function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { + // kimi-code dropped the per-invocation `--max-steps-per-turn` flag (it now lives in + // ~/.kimi-code/config.toml [loop_control]); the probe is a plain non-interactive ping. const result = promptRunner({ prompt: "ping", cwd, timeout: AUTH_CHECK_TIMEOUT_MS, - extraArgs: ["--max-steps-per-turn", "1"], }); return buildKimiAuthStatus(result); } @@ -304,32 +151,17 @@ export function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN, } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin, - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input, - }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error = result.error.message; + const error = result.error.code === "ETIMEDOUT" + ? `kimi timed out after ${Math.round(timeout / 1000)}s` + : result.error.message; return { ok: false, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -343,23 +175,18 @@ export function runKimiPrompt({ } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"], - }); - const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, errorCode: classifyProviderFailure(error, { provider: "kimi" }), - }, resume.sessionId); + status: result.status, + }; } export function runKimiPromptStreaming({ @@ -370,32 +197,18 @@ export function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => {}, bin = KIMI_BIN, spawnImpl, } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin, - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -406,37 +219,19 @@ export function runKimiPromptStreaming({ }, }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"], - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : (result.ok ? "kimi produced no visible text" : result.error); - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }), - }, resume.sessionId); + }; }); } - -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...(result.warnings || []), warning], - }; -} diff --git a/packages/polycli-runtime/src/review-flags.js b/packages/polycli-runtime/src/review-flags.js index 53d6d7b..9511c25 100644 --- a/packages/polycli-runtime/src/review-flags.js +++ b/packages/polycli-runtime/src/review-flags.js @@ -91,10 +91,12 @@ export const REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null, }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null, + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]), }), agy: Object.freeze({ expectFlags: Object.freeze([]), diff --git a/packages/polycli-runtime/test/exports.test.js b/packages/polycli-runtime/test/exports.test.js index 51bc7b7..e3145d6 100644 --- a/packages/polycli-runtime/test/exports.test.js +++ b/packages/polycli-runtime/test/exports.test.js @@ -68,7 +68,6 @@ test("runtime index exports expected surface", () => { "parseOpenCodeStreamText", "parsePiStreamText", "parseQwenStreamText", - "resolveKimiResumeSession", "runAgyPrompt", "runAgyPromptStreaming", "runClaudePrompt", diff --git a/packages/polycli-runtime/test/kimi.test.js b/packages/polycli-runtime/test/kimi.test.js index 2a17eff..0118452 100644 --- a/packages/polycli-runtime/test/kimi.test.js +++ b/packages/polycli-runtime/test/kimi.test.js @@ -11,7 +11,6 @@ import { buildKimiInvocation, extractKimiText, getKimiAuthStatus, - resolveKimiResumeSession, parseKimiStreamText, runKimiPrompt, runKimiPromptStreaming, @@ -29,179 +28,94 @@ function withFakeKimiBin(source, fn) { } } -test("buildKimiInvocation omits -p in stdin mode, defaults to yolo, and enables input-format text", () => { +const RESUME_HINT = (id) => + JSON.stringify({ role: "meta", type: "session.resume_hint", session_id: id, command: `kimi -r ${id}` }); + +test("buildKimiInvocation targets kimi-code one-shot -p + stream-json (no --yolo/--print/--input-format)", () => { const invocation = buildKimiInvocation({ - prompt: "x".repeat(100_000), - model: "kimi-k2", - resumeSessionId: "123e4567-e89b-12d3-a456-426614174000", + prompt: "review this", + model: "kimi-for-coding", + resumeSessionId: "session_123e4567-e89b-42d3-a456-426614174000", }); - assert.equal(invocation.useStdin, true); - assert.equal(invocation.input.length, 100_000); assert.deepEqual(invocation.args, [ - "--print", + "-p", + "review this", "--output-format", "stream-json", - "--input-format", - "text", - "--yolo", "-m", - "kimi-k2", + "kimi-for-coding", "-r", - "123e4567-e89b-12d3-a456-426614174000", + "session_123e4567-e89b-42d3-a456-426614174000", ]); }); -test("buildKimiInvocation omits --yolo when caller opts out", () => { - const invocation = buildKimiInvocation({ - prompt: "ping", - yolo: false, - }); - - assert.equal(invocation.args.includes("--yolo"), false); -}); - -test("resolveKimiResumeSession resolves and validates the last cwd session before spawn", () => { - const home = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-home-")); - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-cwd-")); - const sessionId = "123e4567-e89b-42d3-a456-426614174000"; - const oldHome = process.env.HOME; - const oldUserProfile = process.env.USERPROFILE; - - try { - process.env.HOME = home; - process.env.USERPROFILE = home; - fs.mkdirSync(path.join(home, ".kimi"), { recursive: true }); - const realCwd = fs.realpathSync(cwd); - fs.writeFileSync( - path.join(home, ".kimi", "kimi.json"), - `${JSON.stringify({ work_dirs: [{ path: realCwd, kaos: "local", last_session_id: sessionId }] })}\n` - ); - - const first = resolveKimiResumeSession({ cwd, resumeLast: true }); - assert.equal(first.ok, false); - assert.match(first.error, /not found/i); - - fs.mkdirSync(path.join(home, ".kimi", "sessions", first.cwdHash, sessionId), { recursive: true }); - fs.writeFileSync(path.join(home, ".kimi", "sessions", first.cwdHash, sessionId, "context.jsonl"), "{}\n"); - - const second = resolveKimiResumeSession({ cwd, resumeLast: true }); - assert.equal(second.ok, true); - assert.equal(second.sessionId, sessionId); - } finally { - if (oldHome == null) delete process.env.HOME; - else process.env.HOME = oldHome; - if (oldUserProfile == null) delete process.env.USERPROFILE; - else process.env.USERPROFILE = oldUserProfile; - fs.rmSync(home, { recursive: true, force: true }); - fs.rmSync(cwd, { recursive: true, force: true }); - } -}); - -test("runKimiPromptStreaming rejects invalid explicit resume ids before spawn", async () => { - const result = await runKimiPromptStreaming({ - prompt: "ping", - cwd: process.cwd(), - resumeSessionId: "not-a-uuid", - spawnImpl() { - throw new Error("spawn should not be called"); - }, - }); - - assert.equal(result.ok, false); - assert.match(result.error, /invalid sessionId format/i); -}); - -test("runKimiPromptStreaming warns when requested resume id differs from returned session", async () => { - const home = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-home-")); - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-cwd-")); - const requested = "123e4567-e89b-42d3-a456-426614174000"; - const returned = "223e4567-e89b-42d3-a456-426614174001"; - const oldHome = process.env.HOME; - const oldUserProfile = process.env.USERPROFILE; - const child = new EventEmitter(); - child.stdout = new EventEmitter(); - child.stderr = new EventEmitter(); - child.stdin = { write() {}, end() {}, on() {} }; - child.kill = () => {}; - - try { - process.env.HOME = home; - process.env.USERPROFILE = home; - const resolved = resolveKimiResumeSession({ cwd, resumeSessionId: requested }); - fs.mkdirSync(path.join(home, ".kimi", "sessions", resolved.cwdHash, requested), { recursive: true }); - fs.writeFileSync(path.join(home, ".kimi", "sessions", resolved.cwdHash, requested, "context.jsonl"), "{}\n"); - - const result = await runKimiPromptStreaming({ - prompt: "ping", - cwd, - resumeSessionId: requested, - spawnImpl() { - queueMicrotask(() => { - child.stderr.emit("data", `To resume: kimi -r ${returned}\n`); - child.stdout.emit("data", '{"role":"assistant","content":[{"type":"text","text":"hello"}]}\n'); - child.emit("close", 0, null); - }); - return child; - }, - }); - - assert.equal(result.ok, true); - assert.equal(result.resumeMismatched, true); - assert.deepEqual(result.warnings, [ - `Warning: requested --resume ${requested} did not match returned session ${returned}`, - ]); - } finally { - if (oldHome == null) delete process.env.HOME; - else process.env.HOME = oldHome; - if (oldUserProfile == null) delete process.env.USERPROFILE; - else process.env.USERPROFILE = oldUserProfile; - fs.rmSync(home, { recursive: true, force: true }); - fs.rmSync(cwd, { recursive: true, force: true }); - } +test("buildKimiInvocation uses -C for resume-last and omits resume flags for a fresh run", () => { + assert.deepEqual( + buildKimiInvocation({ prompt: "ping", resumeLast: true }).args, + ["-p", "ping", "--output-format", "stream-json", "-C"] + ); + assert.deepEqual( + buildKimiInvocation({ prompt: "ping" }).args, + ["-p", "ping", "--output-format", "stream-json"] + ); }); -test("parseKimiStreamText keeps assistant text and tool events separate", () => { +test("parseKimiStreamText keeps assistant text, tool events, and reads the structured session id", () => { const parsed = parseKimiStreamText( [ '{"role":"assistant","content":[{"type":"text","text":"hello"},{"type":"think","text":"hidden"}]}', '{"role":"tool","name":"bash","content":[{"type":"text","text":"ran"}]}', - '{"role":"assistant","content":[{"type":"text","text":" world"}],"model":"kimi-k2"}', + '{"role":"assistant","content":" world","model":"kimi-for-coding"}', + RESUME_HINT("session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"), ].join("\n") ); assert.equal(parsed.response, "hello world"); - assert.equal(parsed.model, "kimi-k2"); - assert.equal(parsed.events.length, 3); + assert.equal(parsed.model, "kimi-for-coding"); + assert.equal(parsed.events.length, 4); assert.equal(parsed.toolEvents.length, 1); - assert.equal(extractKimiText({ role: "assistant", content: [{ type: "text", text: "ok" }] }), "ok"); + // The full `session_` id is preserved (not the bare UUID a prose scan would yield). + assert.equal(parsed.sessionId, "session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"); }); -test("extractKimiText supports string assistant content", () => { +test("extractKimiText supports both string and array assistant content", () => { + assert.equal(extractKimiText({ role: "assistant", content: "final body" }), "final body"); assert.equal( - extractKimiText({ role: "assistant", content: "final review body" }), - "final review body" + extractKimiText({ role: "assistant", content: [{ type: "text", text: "ok" }] }), + "ok" ); }); -test("runKimiPrompt prefers stdout session ids before stderr fallback", () => { +test("runKimiPrompt reads the structured session_ id and never fabricates from prose", () => { withFakeKimiBin( `#!/usr/bin/env node -process.stdout.write("stdout session 123e4567-e89b-42d3-a456-426614174000\\n"); -process.stderr.write("stderr session 223e4567-e89b-42d3-a456-426614174000\\n"); -process.stdout.write(JSON.stringify({ role: "assistant", content: [{ type: "text", text: "hello world" }] }) + "\\n"); +process.stdout.write(JSON.stringify({ role: "assistant", content: "here is a uuid 123e4567-e89b-42d3-a456-426614174000" }) + "\\n"); +process.stdout.write(${JSON.stringify(RESUME_HINT("session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"))} + "\\n"); `, ({ root, bin }) => { - const result = runKimiPrompt({ - prompt: "ping", - cwd: root, - defaultModel: "kimi-fallback", - bin, - }); + const result = runKimiPrompt({ prompt: "give me a uuid", cwd: root, bin }); + + assert.equal(result.ok, true); + // The prose UUID is in the answer but is NEVER promoted to a sessionId; the structured + // session.resume_hint id (with its `session_` prefix) is used instead. + assert.match(result.response, /123e4567-e89b-42d3-a456-426614174000/); + assert.equal(result.sessionId, "session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"); + } + ); +}); + +test("runKimiPrompt leaves sessionId null when no resume_hint event is emitted (no fabrication)", () => { + withFakeKimiBin( + `#!/usr/bin/env node +process.stdout.write(JSON.stringify({ role: "assistant", content: "uuid 123e4567-e89b-42d3-a456-426614174000 in the answer" }) + "\\n"); +`, + ({ root, bin }) => { + const result = runKimiPrompt({ prompt: "give me a uuid", cwd: root, bin }); assert.equal(result.ok, true); - assert.equal(result.sessionId, "123e4567-e89b-42d3-a456-426614174000"); - assert.equal(result.model, "kimi-fallback"); + assert.match(result.response, /123e4567/); + assert.equal(result.sessionId, null); } ); }); @@ -213,11 +127,7 @@ process.stdout.write("secret token\\n"); process.exit(2); `, ({ root, bin }) => { - const result = runKimiPrompt({ - prompt: "ping", - cwd: root, - bin, - }); + const result = runKimiPrompt({ prompt: "ping", cwd: root, bin }); assert.equal(result.ok, false); assert.equal(result.error, "kimi exited with code 2"); @@ -225,6 +135,24 @@ process.exit(2); ); }); +test("runKimiPrompt normalizes a real spawn timeout so the auth probe stays inconclusive", () => { + withFakeKimiBin( + `#!/usr/bin/env node +setTimeout(() => {}, 5000); +`, + ({ root, bin }) => { + const result = runKimiPrompt({ prompt: "ping", cwd: root, bin, timeout: 200 }); + + assert.equal(result.ok, false); + assert.match(result.error, /kimi timed out after/i); + + const auth = getKimiAuthStatus(root, { promptRunner: () => result }); + assert.equal(auth.loggedIn, true); + assert.match(auth.detail, /inconclusive/i); + } + ); +}); + test("getKimiAuthStatus keeps loggedIn=true for transient probe failures", () => { const auth = getKimiAuthStatus(process.cwd(), { promptRunner() { @@ -284,20 +212,24 @@ test("runKimiPromptStreaming returns a structured failure on spawn error", async assert.equal(result.errorCode, "binary_missing"); }); -test("runKimiPromptStreaming treats resume footer exits as success when assistant text exists", async () => { +test("runKimiPromptStreaming captures the structured session id and emits events", async () => { const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.stdin = { write() {}, end() {}, on() {} }; child.kill = () => {}; + const events = []; const result = await runKimiPromptStreaming({ prompt: "ping", + onEvent(event) { + events.push(event); + }, spawnImpl() { queueMicrotask(() => { - child.stdout.emit("data", '{"role":"assistant","content":[{"type":"text","text":"hello"}]}\n'); - child.stderr.emit("data", "To resume: kimi -r 123e4567-e89b-42d3-a456-426614174000\n"); - child.emit("close", 1, null); + child.stdout.emit("data", '{"role":"assistant","content":"hello"}\n'); + child.stdout.emit("data", RESUME_HINT("session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac") + "\n"); + child.emit("close", 0, null); }); return child; }, @@ -305,8 +237,8 @@ test("runKimiPromptStreaming treats resume footer exits as success when assistan assert.equal(result.ok, true); assert.equal(result.response, "hello"); - assert.equal(result.error, null); - assert.equal(result.errorCode, null); + assert.equal(result.sessionId, "session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"); + assert.equal(events.length, 2); }); test("runKimiPromptStreaming returns an explicit error when no visible assistant text is emitted", async () => { @@ -320,7 +252,6 @@ test("runKimiPromptStreaming returns an explicit error when no visible assistant prompt: "ping", spawnImpl() { queueMicrotask(() => { - child.stderr.emit("data", "To resume: kimi -r 123\n"); child.stdout.emit("data", '{"role":"assistant","content":[{"type":"think","text":"hidden"}]}\n'); child.emit("close", 0, null); }); diff --git a/packages/polycli-runtime/test/review-flags.test.js b/packages/polycli-runtime/test/review-flags.test.js index 5a61fa3..120c268 100644 --- a/packages/polycli-runtime/test/review-flags.test.js +++ b/packages/polycli-runtime/test/review-flags.test.js @@ -24,7 +24,7 @@ test("every provider declares expectFlags + extraArgTokens as `--`-flag arrays ( // extraArgs carry --extensions/--allowed-mcp-server-names while its help/drift // flags are --approval-mode/--policy. assert.deepEqual(REVIEW_FLAG_EXPECTATIONS.gemini.extraArgTokens, ["--extensions", "--allowed-mcp-server-names"]); - assert.deepEqual(REVIEW_FLAG_EXPECTATIONS.kimi.extraArgTokens, ["--no-thinking", "--max-steps-per-turn"]); + assert.deepEqual(REVIEW_FLAG_EXPECTATIONS.kimi.extraArgTokens, []); }); test("claude/gemini/qwen carry the exact drift expect tokens", () => { @@ -50,7 +50,8 @@ test("read-only option keys mirror assertNoReviewConstraintOverride", () => { assert.equal(REVIEW_FLAG_EXPECTATIONS.opencode.readOnlyOptionKey, "skipPermissions"); assert.equal(REVIEW_FLAG_EXPECTATIONS.opencode.readOnlyValue, null); assert.equal(REVIEW_FLAG_EXPECTATIONS.cmd.readOnlyOptionKey, "yolo"); - assert.equal(REVIEW_FLAG_EXPECTATIONS.kimi.readOnlyOptionKey, "yolo"); + // kimi review is prompt-only under kimi-code (no flag-based read-only lever), like minimax. + assert.equal(REVIEW_FLAG_EXPECTATIONS.kimi.readOnlyOptionKey, undefined); assert.deepEqual(REVIEW_FLAG_EXPECTATIONS.copilot.readOnlyOptionKeys, [ "allowAllTools", "allowAllPaths", diff --git a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs index 2e4cd02..cba01fd 100755 --- a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs +++ b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs @@ -1456,156 +1456,14 @@ function runGeminiPromptStreaming({ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; var KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; var DEFAULT_TIMEOUT_MS4 = 9e5; var AUTH_CHECK_TIMEOUT_MS4 = 3e4; -var PROMPT_STDIN_THRESHOLD_BYTES = 1e5; var KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -var KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; var TRANSIENT_PROBE_ERROR_PATTERNS2 = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i ]; -var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` \u2014 ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found" - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} -function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || !resumeSessionId && !resumeLast) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd - }; - } - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd - }; - } - sessionId = last.sessionId; - } - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode - }), - reason: validation.reason, - cwdHash, - realCwd - }; - } - return { ok: true, sessionId, cwdHash, realCwd }; -} +var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { const text = fs.readFileSync(KIMI_CONFIG_PATH, "utf8"); @@ -1619,28 +1477,19 @@ function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - if (yolo) args.push("--yolo"); + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - return { - bin, - args, - input: useStdin ? promptText : void 0, - useStdin - }; + return { bin, args }; } function parseKimiEventLine(line) { const trimmed = String(line ?? "").trim(); @@ -1668,16 +1517,20 @@ function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } function getKimiAvailability(cwd) { return binaryAvailable(KIMI_BIN, ["-V"], { cwd }); @@ -1704,8 +1557,7 @@ function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { const result = promptRunner({ prompt: "ping", cwd, - timeout: AUTH_CHECK_TIMEOUT_MS4, - extraArgs: ["--max-steps-per-turn", "1"] + timeout: AUTH_CHECK_TIMEOUT_MS4 }); return buildKimiAuthStatus(result); } @@ -1717,30 +1569,13 @@ function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error2 = result.error.message; + const error2 = result.error.code === "ETIMEDOUT" ? `kimi timed out after ${Math.round(timeout / 1e3)}s` : result.error.message; return { ok: false, error: error2, errorCode: classifyProviderFailure(error2, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -1753,22 +1588,18 @@ function runKimiPrompt({ }; } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, - errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + errorCode: classifyProviderFailure(error, { provider: "kimi" }), + status: result.status + }; } function runKimiPromptStreaming({ prompt, @@ -1778,32 +1609,18 @@ function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => { }, bin = KIMI_BIN, spawnImpl } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -1817,37 +1634,20 @@ function runKimiPromptStreaming({ } }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : result.ok ? "kimi produced no visible text" : result.error; - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + }; }); } -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...result.warnings || [], warning] - }; -} // packages/polycli-runtime/src/qwen.js var QWEN_BIN = process.env.QWEN_CLI_BIN || "qwen"; @@ -4133,10 +3933,12 @@ var REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]) }), agy: Object.freeze({ expectFlags: Object.freeze([]), @@ -5007,13 +4809,13 @@ function buildRunExplanation(events, runId) { import fs5 from "node:fs"; import os3 from "node:os"; import path5 from "node:path"; -import { createHash as createHash2 } from "node:crypto"; +import { createHash } from "node:crypto"; function storeRoot(provider, homedir) { switch (provider) { case "claude": return path5.join(homedir, ".claude", "projects"); case "kimi": - return path5.join(homedir, ".kimi", "sessions"); + return path5.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -5040,10 +4842,15 @@ function deriveSessionArtifactCandidate({ provider, sessionId, workspaceRoot, ho }; } case "kimi": { - const cwdHash = createHash2("md5").update(workspaceRoot).digest("hex"); + let realCwd = workspaceRoot; + try { + realCwd = fs5.realpathSync(workspaceRoot); + } catch { + } + const slug = `wd_${path5.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path5.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path5.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir" }; } @@ -5494,13 +5301,6 @@ function buildPromptRuntimeOptions({ yolo: true }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]) - }; - } if (kind === "ask" && provider === "claude") { return { ...runtimeOptions, @@ -5689,7 +5489,7 @@ function assertReviewProviderSupported(provider) { } var REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + return {}; }, qwen() { return { diff --git a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs index 2e4cd02..cba01fd 100755 --- a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs @@ -1456,156 +1456,14 @@ function runGeminiPromptStreaming({ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; var KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; var DEFAULT_TIMEOUT_MS4 = 9e5; var AUTH_CHECK_TIMEOUT_MS4 = 3e4; -var PROMPT_STDIN_THRESHOLD_BYTES = 1e5; var KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -var KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; var TRANSIENT_PROBE_ERROR_PATTERNS2 = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i ]; -var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` \u2014 ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found" - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} -function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || !resumeSessionId && !resumeLast) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd - }; - } - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd - }; - } - sessionId = last.sessionId; - } - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode - }), - reason: validation.reason, - cwdHash, - realCwd - }; - } - return { ok: true, sessionId, cwdHash, realCwd }; -} +var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { const text = fs.readFileSync(KIMI_CONFIG_PATH, "utf8"); @@ -1619,28 +1477,19 @@ function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - if (yolo) args.push("--yolo"); + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - return { - bin, - args, - input: useStdin ? promptText : void 0, - useStdin - }; + return { bin, args }; } function parseKimiEventLine(line) { const trimmed = String(line ?? "").trim(); @@ -1668,16 +1517,20 @@ function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } function getKimiAvailability(cwd) { return binaryAvailable(KIMI_BIN, ["-V"], { cwd }); @@ -1704,8 +1557,7 @@ function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { const result = promptRunner({ prompt: "ping", cwd, - timeout: AUTH_CHECK_TIMEOUT_MS4, - extraArgs: ["--max-steps-per-turn", "1"] + timeout: AUTH_CHECK_TIMEOUT_MS4 }); return buildKimiAuthStatus(result); } @@ -1717,30 +1569,13 @@ function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error2 = result.error.message; + const error2 = result.error.code === "ETIMEDOUT" ? `kimi timed out after ${Math.round(timeout / 1e3)}s` : result.error.message; return { ok: false, error: error2, errorCode: classifyProviderFailure(error2, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -1753,22 +1588,18 @@ function runKimiPrompt({ }; } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, - errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + errorCode: classifyProviderFailure(error, { provider: "kimi" }), + status: result.status + }; } function runKimiPromptStreaming({ prompt, @@ -1778,32 +1609,18 @@ function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => { }, bin = KIMI_BIN, spawnImpl } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -1817,37 +1634,20 @@ function runKimiPromptStreaming({ } }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : result.ok ? "kimi produced no visible text" : result.error; - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + }; }); } -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...result.warnings || [], warning] - }; -} // packages/polycli-runtime/src/qwen.js var QWEN_BIN = process.env.QWEN_CLI_BIN || "qwen"; @@ -4133,10 +3933,12 @@ var REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]) }), agy: Object.freeze({ expectFlags: Object.freeze([]), @@ -5007,13 +4809,13 @@ function buildRunExplanation(events, runId) { import fs5 from "node:fs"; import os3 from "node:os"; import path5 from "node:path"; -import { createHash as createHash2 } from "node:crypto"; +import { createHash } from "node:crypto"; function storeRoot(provider, homedir) { switch (provider) { case "claude": return path5.join(homedir, ".claude", "projects"); case "kimi": - return path5.join(homedir, ".kimi", "sessions"); + return path5.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -5040,10 +4842,15 @@ function deriveSessionArtifactCandidate({ provider, sessionId, workspaceRoot, ho }; } case "kimi": { - const cwdHash = createHash2("md5").update(workspaceRoot).digest("hex"); + let realCwd = workspaceRoot; + try { + realCwd = fs5.realpathSync(workspaceRoot); + } catch { + } + const slug = `wd_${path5.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path5.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path5.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir" }; } @@ -5494,13 +5301,6 @@ function buildPromptRuntimeOptions({ yolo: true }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]) - }; - } if (kind === "ask" && provider === "claude") { return { ...runtimeOptions, @@ -5689,7 +5489,7 @@ function assertReviewProviderSupported(provider) { } var REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + return {}; }, qwen() { return { diff --git a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs index 2e4cd02..cba01fd 100755 --- a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs @@ -1456,156 +1456,14 @@ function runGeminiPromptStreaming({ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; var KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; var DEFAULT_TIMEOUT_MS4 = 9e5; var AUTH_CHECK_TIMEOUT_MS4 = 3e4; -var PROMPT_STDIN_THRESHOLD_BYTES = 1e5; var KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -var KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; var TRANSIENT_PROBE_ERROR_PATTERNS2 = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i ]; -var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` \u2014 ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found" - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} -function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || !resumeSessionId && !resumeLast) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd - }; - } - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd - }; - } - sessionId = last.sessionId; - } - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode - }), - reason: validation.reason, - cwdHash, - realCwd - }; - } - return { ok: true, sessionId, cwdHash, realCwd }; -} +var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { const text = fs.readFileSync(KIMI_CONFIG_PATH, "utf8"); @@ -1619,28 +1477,19 @@ function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - if (yolo) args.push("--yolo"); + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - return { - bin, - args, - input: useStdin ? promptText : void 0, - useStdin - }; + return { bin, args }; } function parseKimiEventLine(line) { const trimmed = String(line ?? "").trim(); @@ -1668,16 +1517,20 @@ function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } function getKimiAvailability(cwd) { return binaryAvailable(KIMI_BIN, ["-V"], { cwd }); @@ -1704,8 +1557,7 @@ function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { const result = promptRunner({ prompt: "ping", cwd, - timeout: AUTH_CHECK_TIMEOUT_MS4, - extraArgs: ["--max-steps-per-turn", "1"] + timeout: AUTH_CHECK_TIMEOUT_MS4 }); return buildKimiAuthStatus(result); } @@ -1717,30 +1569,13 @@ function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error2 = result.error.message; + const error2 = result.error.code === "ETIMEDOUT" ? `kimi timed out after ${Math.round(timeout / 1e3)}s` : result.error.message; return { ok: false, error: error2, errorCode: classifyProviderFailure(error2, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -1753,22 +1588,18 @@ function runKimiPrompt({ }; } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, - errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + errorCode: classifyProviderFailure(error, { provider: "kimi" }), + status: result.status + }; } function runKimiPromptStreaming({ prompt, @@ -1778,32 +1609,18 @@ function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => { }, bin = KIMI_BIN, spawnImpl } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -1817,37 +1634,20 @@ function runKimiPromptStreaming({ } }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : result.ok ? "kimi produced no visible text" : result.error; - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + }; }); } -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...result.warnings || [], warning] - }; -} // packages/polycli-runtime/src/qwen.js var QWEN_BIN = process.env.QWEN_CLI_BIN || "qwen"; @@ -4133,10 +3933,12 @@ var REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]) }), agy: Object.freeze({ expectFlags: Object.freeze([]), @@ -5007,13 +4809,13 @@ function buildRunExplanation(events, runId) { import fs5 from "node:fs"; import os3 from "node:os"; import path5 from "node:path"; -import { createHash as createHash2 } from "node:crypto"; +import { createHash } from "node:crypto"; function storeRoot(provider, homedir) { switch (provider) { case "claude": return path5.join(homedir, ".claude", "projects"); case "kimi": - return path5.join(homedir, ".kimi", "sessions"); + return path5.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -5040,10 +4842,15 @@ function deriveSessionArtifactCandidate({ provider, sessionId, workspaceRoot, ho }; } case "kimi": { - const cwdHash = createHash2("md5").update(workspaceRoot).digest("hex"); + let realCwd = workspaceRoot; + try { + realCwd = fs5.realpathSync(workspaceRoot); + } catch { + } + const slug = `wd_${path5.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path5.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path5.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir" }; } @@ -5494,13 +5301,6 @@ function buildPromptRuntimeOptions({ yolo: true }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]) - }; - } if (kind === "ask" && provider === "claude") { return { ...runtimeOptions, @@ -5689,7 +5489,7 @@ function assertReviewProviderSupported(provider) { } var REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + return {}; }, qwen() { return { diff --git a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs index 2e4cd02..cba01fd 100755 --- a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs @@ -1456,156 +1456,14 @@ function runGeminiPromptStreaming({ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; var KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; var DEFAULT_TIMEOUT_MS4 = 9e5; var AUTH_CHECK_TIMEOUT_MS4 = 3e4; -var PROMPT_STDIN_THRESHOLD_BYTES = 1e5; var KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -var KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; var TRANSIENT_PROBE_ERROR_PATTERNS2 = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i ]; -var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` \u2014 ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found" - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} -function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || !resumeSessionId && !resumeLast) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd - }; - } - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd - }; - } - sessionId = last.sessionId; - } - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode - }), - reason: validation.reason, - cwdHash, - realCwd - }; - } - return { ok: true, sessionId, cwdHash, realCwd }; -} +var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { const text = fs.readFileSync(KIMI_CONFIG_PATH, "utf8"); @@ -1619,28 +1477,19 @@ function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - if (yolo) args.push("--yolo"); + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - return { - bin, - args, - input: useStdin ? promptText : void 0, - useStdin - }; + return { bin, args }; } function parseKimiEventLine(line) { const trimmed = String(line ?? "").trim(); @@ -1668,16 +1517,20 @@ function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } function getKimiAvailability(cwd) { return binaryAvailable(KIMI_BIN, ["-V"], { cwd }); @@ -1704,8 +1557,7 @@ function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { const result = promptRunner({ prompt: "ping", cwd, - timeout: AUTH_CHECK_TIMEOUT_MS4, - extraArgs: ["--max-steps-per-turn", "1"] + timeout: AUTH_CHECK_TIMEOUT_MS4 }); return buildKimiAuthStatus(result); } @@ -1717,30 +1569,13 @@ function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error2 = result.error.message; + const error2 = result.error.code === "ETIMEDOUT" ? `kimi timed out after ${Math.round(timeout / 1e3)}s` : result.error.message; return { ok: false, error: error2, errorCode: classifyProviderFailure(error2, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -1753,22 +1588,18 @@ function runKimiPrompt({ }; } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, - errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + errorCode: classifyProviderFailure(error, { provider: "kimi" }), + status: result.status + }; } function runKimiPromptStreaming({ prompt, @@ -1778,32 +1609,18 @@ function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => { }, bin = KIMI_BIN, spawnImpl } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -1817,37 +1634,20 @@ function runKimiPromptStreaming({ } }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : result.ok ? "kimi produced no visible text" : result.error; - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + }; }); } -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...result.warnings || [], warning] - }; -} // packages/polycli-runtime/src/qwen.js var QWEN_BIN = process.env.QWEN_CLI_BIN || "qwen"; @@ -4133,10 +3933,12 @@ var REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]) }), agy: Object.freeze({ expectFlags: Object.freeze([]), @@ -5007,13 +4809,13 @@ function buildRunExplanation(events, runId) { import fs5 from "node:fs"; import os3 from "node:os"; import path5 from "node:path"; -import { createHash as createHash2 } from "node:crypto"; +import { createHash } from "node:crypto"; function storeRoot(provider, homedir) { switch (provider) { case "claude": return path5.join(homedir, ".claude", "projects"); case "kimi": - return path5.join(homedir, ".kimi", "sessions"); + return path5.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -5040,10 +4842,15 @@ function deriveSessionArtifactCandidate({ provider, sessionId, workspaceRoot, ho }; } case "kimi": { - const cwdHash = createHash2("md5").update(workspaceRoot).digest("hex"); + let realCwd = workspaceRoot; + try { + realCwd = fs5.realpathSync(workspaceRoot); + } catch { + } + const slug = `wd_${path5.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path5.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path5.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir" }; } @@ -5494,13 +5301,6 @@ function buildPromptRuntimeOptions({ yolo: true }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]) - }; - } if (kind === "ask" && provider === "claude") { return { ...runtimeOptions, @@ -5689,7 +5489,7 @@ function assertReviewProviderSupported(provider) { } var REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + return {}; }, qwen() { return { diff --git a/plugins/polycli/scripts/lib/prompt-runtime.mjs b/plugins/polycli/scripts/lib/prompt-runtime.mjs index 119a53f..a989d31 100644 --- a/plugins/polycli/scripts/lib/prompt-runtime.mjs +++ b/plugins/polycli/scripts/lib/prompt-runtime.mjs @@ -56,13 +56,9 @@ export function buildPromptRuntimeOptions({ }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]), - }; - } + // kimi-code v0.6.0 has no per-invocation ask constraints: `-p` one-shot mode rejects + // --plan/--auto and the old --no-thinking/--max-steps-per-turn flags were removed (those + // are now config.toml-level). kimi ask therefore uses the plain `-p` invocation. if (kind === "ask" && provider === "claude") { return { diff --git a/plugins/polycli/scripts/lib/review.mjs b/plugins/polycli/scripts/lib/review.mjs index d4f1df3..4d517ca 100644 --- a/plugins/polycli/scripts/lib/review.mjs +++ b/plugins/polycli/scripts/lib/review.mjs @@ -98,7 +98,10 @@ export function assertReviewProviderSupported(provider) { const REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + // kimi-code v0.6.0 dropped --no-thinking/--max-steps-per-turn and its -p one-shot mode rejects + // --plan/--auto, so there is no flag-based read-only lever. Review is prompt-only (the review + // prompt forbids tools/edits), matching the minimax tier. No extra flags are emitted. + return {}; }, qwen() { return { diff --git a/plugins/polycli/scripts/lib/sessions.mjs b/plugins/polycli/scripts/lib/sessions.mjs index 177edd5..8c28c46 100644 --- a/plugins/polycli/scripts/lib/sessions.mjs +++ b/plugins/polycli/scripts/lib/sessions.mjs @@ -19,7 +19,7 @@ function storeRoot(provider, homedir) { case "claude": return path.join(homedir, ".claude", "projects"); case "kimi": - return path.join(homedir, ".kimi", "sessions"); + return path.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -55,12 +55,21 @@ export function deriveSessionArtifactCandidate({ provider, sessionId, workspaceR }; } case "kimi": { - // Reuses the exact derivation from polycli-runtime/src/kimi.js:39-41,86: - // ~/.kimi/sessions/// — a per-session DIR. - const cwdHash = createHash("md5").update(workspaceRoot).digest("hex"); + // kimi-code v0.6.0 store (verified on disk): ~/.kimi-code/sessions/ + // wd__// — a per-session DIR. sessionId is the + // structured `session_` captured from the stream-json resume_hint event, which is + // also the exact dir name. Use the realpath of the cwd (the store keys by realpath). + let realCwd = workspaceRoot; + try { + realCwd = fs.realpathSync(workspaceRoot); + } catch { + // workspace dir gone — fall back to the given path; recordArtifactPath will reject it + // if it does not resolve under the store root. + } + const slug = `wd_${path.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir", }; } diff --git a/plugins/polycli/scripts/polycli-companion.bundle.mjs b/plugins/polycli/scripts/polycli-companion.bundle.mjs index 2e4cd02..cba01fd 100755 --- a/plugins/polycli/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli/scripts/polycli-companion.bundle.mjs @@ -1456,156 +1456,14 @@ function runGeminiPromptStreaming({ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { createHash } from "node:crypto"; var KIMI_BIN = process.env.KIMI_CLI_BIN || "kimi"; var DEFAULT_TIMEOUT_MS4 = 9e5; var AUTH_CHECK_TIMEOUT_MS4 = 3e4; -var PROMPT_STDIN_THRESHOLD_BYTES = 1e5; var KIMI_EXPLICIT_AUTH_ERROR_RE = /\b(unauthenticated|unauthorized|not authenticated|not authorized|login required|log in|sign in|invalid api key|missing api key|api key required|token expired|invalid token|credential(?:s)? (?:missing|invalid|expired)|permission denied|access denied|forbidden|401|403)\b/i; -var KIMI_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; var TRANSIENT_PROBE_ERROR_PATTERNS2 = [ /\b(timed out|timeout|429|rate limit|no capacity available|temporar(?:y|ily)|service unavailable|overloaded|try again|econnreset|econnrefused|enotfound|network|socket hang up)\b/i ]; -var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi", "config.toml"); -function isKimiResumeFooter(text) { - return /^To resume:\s*kimi\s+-r\s+/im.test(String(text ?? "").trim()); -} -function kimiJsonPath() { - return path.join(os.homedir(), ".kimi", "kimi.json"); -} -function kimiSessionsDir() { - return path.join(os.homedir(), ".kimi", "sessions"); -} -function resolveRealCwd(cwd) { - return fs.realpathSync(cwd || process.cwd()); -} -function md5CwdPath(realCwd) { - return createHash("md5").update(realCwd).digest("hex"); -} -function formatKimiResumeError(reason, { sessionId, cwd, errCode } = {}) { - const cwdBase = cwd ? path.basename(cwd) : "?"; - if (reason === "invalid-uuid") return "invalid sessionId format; expected UUID."; - if (reason === "no-prior-session") return `no prior kimi session for this directory (${cwdBase}). Use /polycli:ask --provider kimi to start one.`; - if (reason === "kimi-json-malformed") return "~/.kimi/kimi.json is malformed; cannot resolve last session."; - if (reason === "session-not-found") return `session ${sessionId} not found for this directory (${cwdBase}).`; - if (reason === "session-empty") return `session ${sessionId} has no stored messages; cannot resume.`; - if (reason === "fs-error") return `filesystem access failed${errCode ? ` \u2014 ${errCode}` : ""}. Check permissions on ~/.kimi/.`; - return `kimi resume validation failed: ${reason}`; -} -function readKimiLastSession(realCwd) { - let raw; - try { - raw = fs.readFileSync(kimiJsonPath(), "utf8"); - } catch (error) { - if (error.code === "ENOENT") return { ok: false, reason: "no-prior-session" }; - return { ok: false, reason: "fs-error", errCode: error.code }; - } - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { ok: false, reason: "kimi-json-malformed" }; - } - if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.work_dirs)) { - return { ok: false, reason: "kimi-json-malformed" }; - } - const entry = parsed.work_dirs.find((item) => item && item.path === realCwd && item.kaos === "local"); - if (!entry || typeof entry.last_session_id !== "string" || entry.last_session_id.length === 0) { - return { ok: false, reason: "no-prior-session" }; - } - return { ok: true, sessionId: entry.last_session_id }; -} -function validateKimiResumeTarget({ realCwd, cwdHash, sessionId }) { - if (typeof sessionId !== "string" || !KIMI_UUID_RE.test(sessionId)) { - return { ok: false, reason: "invalid-uuid" }; - } - const sessionDir = path.join(kimiSessionsDir(), cwdHash, sessionId); - const contextPath = path.join(sessionDir, "context.jsonl"); - try { - const dirStat = fs.statSync(sessionDir); - if (!dirStat.isDirectory()) { - return { ok: false, reason: "session-not-found" }; - } - const contextStat = fs.statSync(contextPath); - if (!contextStat.isFile() || contextStat.size === 0) { - return { ok: false, reason: "session-empty" }; - } - return { ok: true }; - } catch (error) { - if (error.code === "ENOENT") { - return { - ok: false, - reason: fs.existsSync(sessionDir) ? "session-empty" : "session-not-found" - }; - } - return { ok: false, reason: "fs-error", errCode: error.code, realCwd }; - } -} -function resolveKimiResumeSession({ - cwd, - resumeSessionId = null, - resumeLast = false, - fresh = false -} = {}) { - let realCwd; - try { - realCwd = resolveRealCwd(cwd); - } catch (error) { - return { - ok: false, - status: 1, - error: formatKimiResumeError("fs-error", { errCode: error.code }), - reason: "fs-error", - cwdHash: null, - realCwd: null - }; - } - const cwdHash = md5CwdPath(realCwd); - if (fresh || !resumeSessionId && !resumeLast) { - return { ok: true, sessionId: null, cwdHash, realCwd }; - } - if (resumeSessionId && resumeLast) { - return { - ok: false, - status: 2, - error: "Choose only one of --resume-last, --resume, or --fresh.", - reason: "mutually-exclusive-resume-flags", - cwdHash, - realCwd - }; - } - let sessionId = resumeSessionId; - if (resumeLast) { - const last = readKimiLastSession(realCwd); - if (!last.ok) { - return { - ok: false, - status: last.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(last.reason, { cwd: realCwd, errCode: last.errCode }), - reason: last.reason, - cwdHash, - realCwd - }; - } - sessionId = last.sessionId; - } - const validation = validateKimiResumeTarget({ realCwd, cwdHash, sessionId }); - if (!validation.ok) { - return { - ok: false, - status: validation.reason === "invalid-uuid" ? 2 : 1, - error: formatKimiResumeError(validation.reason, { - sessionId, - cwd: realCwd, - errCode: validation.errCode - }), - reason: validation.reason, - cwdHash, - realCwd - }; - } - return { ok: true, sessionId, cwdHash, realCwd }; -} +var KIMI_CONFIG_PATH = process.env.KIMI_CONFIG_PATH || path.join(os.homedir(), ".kimi-code", "config.toml"); function readKimiDefaultModel() { try { const text = fs.readFileSync(KIMI_CONFIG_PATH, "utf8"); @@ -1619,28 +1477,19 @@ function buildKimiInvocation({ prompt, model = null, resumeSessionId = null, - yolo = true, + resumeLast = false, extraArgs = [], bin = KIMI_BIN } = {}) { - const promptText = String(prompt ?? ""); - const useStdin = Buffer.byteLength(promptText, "utf8") >= PROMPT_STDIN_THRESHOLD_BYTES; - const args = ["--print", "--output-format", "stream-json"]; - if (useStdin) { - args.push("--input-format", "text"); - } else { - args.unshift("-p", promptText); - } - if (yolo) args.push("--yolo"); + const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); - if (resumeSessionId) args.push("-r", resumeSessionId); + if (resumeLast) { + args.push("-C"); + } else if (resumeSessionId) { + args.push("-r", resumeSessionId); + } if (extraArgs.length > 0) args.push(...extraArgs); - return { - bin, - args, - input: useStdin ? promptText : void 0, - useStdin - }; + return { bin, args }; } function parseKimiEventLine(line) { const trimmed = String(line ?? "").trim(); @@ -1668,16 +1517,20 @@ function parseKimiStreamText(text) { const toolEvents = []; let response = ""; let model = null; + let sessionId = null; for (const rawLine of String(text ?? "").split(/\r?\n/)) { const event = parseKimiEventLine(rawLine); if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); + if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + sessionId = event.session_id; + } if (!model && typeof event.model === "string") model = event.model; if (!model && typeof event.message?.model === "string") model = event.message.model; response += extractKimiText(event); } - return { events, toolEvents, response, model }; + return { events, toolEvents, response, model, sessionId }; } function getKimiAvailability(cwd) { return binaryAvailable(KIMI_BIN, ["-V"], { cwd }); @@ -1704,8 +1557,7 @@ function getKimiAuthStatus(cwd, { promptRunner = runKimiPrompt } = {}) { const result = promptRunner({ prompt: "ping", cwd, - timeout: AUTH_CHECK_TIMEOUT_MS4, - extraArgs: ["--max-steps-per-turn", "1"] + timeout: AUTH_CHECK_TIMEOUT_MS4 }); return buildKimiAuthStatus(result); } @@ -1717,30 +1569,13 @@ function runKimiPrompt({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, bin = KIMI_BIN } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return { ok: false, error: resume.error, status: resume.status }; - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); - const result = runCommand(invocation.bin, invocation.args, { - cwd, - timeout, - input: invocation.input - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); + const result = runCommand(invocation.bin, invocation.args, { cwd, timeout }); if (result.error) { - const error2 = result.error.message; + const error2 = result.error.code === "ETIMEDOUT" ? `kimi timed out after ${Math.round(timeout / 1e3)}s` : result.error.message; return { ok: false, error: error2, errorCode: classifyProviderFailure(error2, { provider: "kimi" }) }; } if (result.status !== 0) { @@ -1753,22 +1588,18 @@ function runKimiPrompt({ }; } const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const error = parsed.response.trim() ? null : "kimi produced no visible text"; - return withKimiResumeWarnings({ + return { ok: Boolean(parsed.response.trim()), response: parsed.response, events: parsed.events, toolEvents: parsed.toolEvents, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), error, - errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + errorCode: classifyProviderFailure(error, { provider: "kimi" }), + status: result.status + }; } function runKimiPromptStreaming({ prompt, @@ -1778,32 +1609,18 @@ function runKimiPromptStreaming({ extraArgs = [], resumeSessionId = null, resumeLast = false, - fresh = false, - yolo = true, defaultModel = null, onEvent = () => { }, bin = KIMI_BIN, spawnImpl } = {}) { - const resume = resolveKimiResumeSession({ cwd, resumeSessionId, resumeLast, fresh }); - if (!resume.ok) { - return Promise.resolve({ ok: false, error: resume.error, status: resume.status }); - } - const invocation = buildKimiInvocation({ - prompt, - model, - resumeSessionId: resume.sessionId, - yolo, - extraArgs, - bin - }); + const invocation = buildKimiInvocation({ prompt, model, resumeSessionId, resumeLast, extraArgs, bin }); return spawnStreamingCommand({ bin: invocation.bin, args: invocation.args, cwd, env: { ...process.env }, - input: invocation.input, timeout, spawnImpl, onStdoutLine(line) { @@ -1817,37 +1634,20 @@ function runKimiPromptStreaming({ } }).then((result) => { const parsed = parseKimiStreamText(result.stdout); - const session = resolveSessionId({ - stdout: result.stdout, - stderr: result.stderr, - priority: ["stdout", "stderr", "file"] - }); const hasVisibleText = Boolean(parsed.response.trim()); - const resumeFooterOnly = hasVisibleText && !result.ok && isKimiResumeFooter(result.error); - const ok = (result.ok || resumeFooterOnly) && hasVisibleText; + const ok = result.ok && hasVisibleText; const error = ok ? null : result.ok ? "kimi produced no visible text" : result.error; - return withKimiResumeWarnings({ + return { ...result, ...parsed, - sessionId: session.sessionId, + sessionId: parsed.sessionId, model: parsed.model ?? model ?? defaultModel ?? readKimiDefaultModel(), ok, error, errorCode: classifyProviderFailure(error, { provider: "kimi" }) - }, resume.sessionId); + }; }); } -function withKimiResumeWarnings(result, requestedSessionId) { - if (!requestedSessionId || !result.ok || !result.sessionId || result.sessionId === requestedSessionId) { - return result; - } - const warning = `Warning: requested --resume ${requestedSessionId} did not match returned session ${result.sessionId}`; - return { - ...result, - resumeMismatched: true, - warnings: [...result.warnings || [], warning] - }; -} // packages/polycli-runtime/src/qwen.js var QWEN_BIN = process.env.QWEN_CLI_BIN || "qwen"; @@ -4133,10 +3933,12 @@ var REVIEW_FLAG_EXPECTATIONS = Object.freeze({ readOnlyValue: null }), kimi: Object.freeze({ - expectFlags: Object.freeze([]), - extraArgTokens: Object.freeze(["--no-thinking", "--max-steps-per-turn"]), - readOnlyOptionKey: "yolo", - readOnlyValue: null + // kimi-code v0.6.0: the legacy --no-thinking/--max-steps-per-turn review levers were removed + // upstream and -p one-shot mode rejects --plan/--auto, so review is prompt-only (extraArgTokens + // empty, like minimax). expectFlags are the load-bearing INVOCATION flags the runtime depends on + // (-p/--prompt + --output-format), so the drift check warns if kimi-code renames or drops them. + expectFlags: Object.freeze(["--prompt", "--output-format"]), + extraArgTokens: Object.freeze([]) }), agy: Object.freeze({ expectFlags: Object.freeze([]), @@ -5007,13 +4809,13 @@ function buildRunExplanation(events, runId) { import fs5 from "node:fs"; import os3 from "node:os"; import path5 from "node:path"; -import { createHash as createHash2 } from "node:crypto"; +import { createHash } from "node:crypto"; function storeRoot(provider, homedir) { switch (provider) { case "claude": return path5.join(homedir, ".claude", "projects"); case "kimi": - return path5.join(homedir, ".kimi", "sessions"); + return path5.join(homedir, ".kimi-code", "sessions"); default: return null; } @@ -5040,10 +4842,15 @@ function deriveSessionArtifactCandidate({ provider, sessionId, workspaceRoot, ho }; } case "kimi": { - const cwdHash = createHash2("md5").update(workspaceRoot).digest("hex"); + let realCwd = workspaceRoot; + try { + realCwd = fs5.realpathSync(workspaceRoot); + } catch { + } + const slug = `wd_${path5.basename(realCwd)}_${createHash("sha256").update(realCwd).digest("hex").slice(0, 12)}`; return { provider: "kimi", - path: path5.join(homedir, ".kimi", "sessions", cwdHash, sessionId), + path: path5.join(homedir, ".kimi-code", "sessions", slug, sessionId), kind: "dir" }; } @@ -5494,13 +5301,6 @@ function buildPromptRuntimeOptions({ yolo: true }; } - if (kind === "ask" && provider === "kimi") { - return { - ...runtimeOptions, - yolo: false, - extraArgs: mergeExtraArgs(runtimeOptions, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]) - }; - } if (kind === "ask" && provider === "claude") { return { ...runtimeOptions, @@ -5689,7 +5489,7 @@ function assertReviewProviderSupported(provider) { } var REVIEW_HARD_CONSTRAINTS = { kimi() { - return { yolo: false, extraArgs: ["--no-thinking", "--max-steps-per-turn", "1"] }; + return {}; }, qwen() { return { diff --git a/plugins/polycli/scripts/tests/integration.test.mjs b/plugins/polycli/scripts/tests/integration.test.mjs index e44f341..0129631 100644 --- a/plugins/polycli/scripts/tests/integration.test.mjs +++ b/plugins/polycli/scripts/tests/integration.test.mjs @@ -4,7 +4,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawn, spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; import { fileURLToPath } from "node:url"; import { createClaudeFixtureReplay } from "./helpers/fixture-replay.mjs"; @@ -170,28 +169,25 @@ if (process.env.KIMI_ARGV_LOG) { } const promptIndex = args.indexOf("-p"); const prompt = promptIndex >= 0 ? (args[promptIndex + 1] || "") : "ping"; -const noThinking = args.includes("--no-thinking"); const delayMatch = prompt.match(/__delay=(\\d+)/); const delay = delayMatch ? Number.parseInt(delayMatch[1], 10) : Number.parseInt(process.env.KIMI_DELAY_MS || "0", 10); const tailDelayMatch = prompt.match(/__tail=(\\d+)/); const tailDelay = tailDelayMatch ? Number.parseInt(tailDelayMatch[1], 10) : 0; const replyMatch = prompt.match(/__reply=([^\\n]+)/); const reply = process.env.KIMI_FIXED_REPLY || (replyMatch ? replyMatch[1] : prompt); +const returnSession = process.env.KIMI_RETURN_SESSION || "session_33333333-3333-4333-8333-333333333333"; (async () => { logEvent("start"); - process.stderr.write("To resume: kimi -r 33333333-3333-4333-8333-333333333333\\n"); if (delay > 0) await sleep(delay); - if (process.env.KIMI_REQUIRE_NO_THINKING === "1" && !noThinking) { - process.stdout.write(JSON.stringify({ role: "assistant", content: [{ type: "think", think: "missing no-thinking constraint" }] }) + "\\n"); - return; - } if (process.env.KIMI_CONTENT_MODE === "string") { process.stdout.write(JSON.stringify({ role: "assistant", content: reply, model: "kimi-test" }) + "\\n"); - } else if (process.env.KIMI_EMIT_THINKING === "1" && !noThinking) { + } else if (process.env.KIMI_EMIT_THINKING === "1") { process.stdout.write(JSON.stringify({ role: "assistant", content: [{ type: "think", think: "thinking before final" }, { type: "text", text: reply }], model: "kimi-test" }) + "\\n"); } else { process.stdout.write(JSON.stringify({ role: "assistant", content: [{ type: "text", text: reply }], model: "kimi-test" }) + "\\n"); } + // kimi-code emits the session id structurally in a resume_hint meta event (session_). + process.stdout.write(JSON.stringify({ role: "meta", type: "session.resume_hint", session_id: returnSession, command: "kimi -r " + returnSession }) + "\\n"); if (tailDelay > 0) await sleep(tailDelay); logEvent("end"); })(); @@ -500,18 +496,6 @@ function readJsonLine(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8").trim()); } -function createKimiSessionFixture({ home, cwd, sessionId }) { - const realCwd = fs.realpathSync(cwd); - const cwdHash = createHash("md5").update(realCwd).digest("hex"); - fs.mkdirSync(path.join(home, ".kimi", "sessions", cwdHash, sessionId), { recursive: true }); - fs.writeFileSync(path.join(home, ".kimi", "sessions", cwdHash, sessionId, "context.jsonl"), "{}\n"); - fs.writeFileSync( - path.join(home, ".kimi", "kimi.json"), - `${JSON.stringify({ work_dirs: [{ path: realCwd, kaos: "local", last_session_id: sessionId }] })}\n` - ); - return { cwdHash, realCwd }; -} - async function waitForTerminalJob(jobId, context) { const deadline = Date.now() + 10_000; while (Date.now() < deadline) { @@ -1065,7 +1049,6 @@ test("integration: kimi ask parses --resume-last, --resume, and --fresh", async const fake = createFakeKimiBin(); const sessionId = "33333333-3333-4333-8333-333333333333"; try { - createKimiSessionFixture({ home, cwd, sessionId }); const env = cleanEnv({ HOME: home, USERPROFILE: home, @@ -1075,14 +1058,17 @@ test("integration: kimi ask parses --resume-last, --resume, and --fresh", async KIMI_FIXED_REPLY: "KIMI_FLAGS_OK", }); + // kimi-code resolves resume itself: --resume-last -> -C (continue last for this cwd). const resumeLast = await runCompanion( ["ask", "--provider", "kimi", "--resume-last", "--json", "__reply=IGNORED"], { cwd, env } ); assert.equal(resumeLast.code, 0, resumeLast.stderr); let logged = readJsonLine(argLog); - assert.deepEqual(logged.argv.slice(logged.argv.indexOf("-r"), logged.argv.indexOf("-r") + 2), ["-r", sessionId]); + assert.equal(logged.argv.includes("-C"), true); + assert.equal(logged.argv.includes("-r"), false); + // --resume -> -r passed straight through to the CLI. const explicitResume = await runCompanion( ["rescue", "--provider", "kimi", "--resume", sessionId, "--json", "__reply=IGNORED"], { cwd, env } @@ -1098,6 +1084,7 @@ test("integration: kimi ask parses --resume-last, --resume, and --fresh", async assert.equal(fresh.code, 0, fresh.stderr); logged = readJsonLine(argLog); assert.equal(logged.argv.includes("-r"), false); + assert.equal(logged.argv.includes("-C"), false); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true }); @@ -1146,43 +1133,6 @@ test("integration: unsupported flags emit one-line notes and continue", async () } }); -test("integration: kimi explicit resume mismatch emits a warning after spawn", async () => { - const pluginData = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-plugin-data-")); - const home = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-home-")); - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-kimi-cwd-")); - const fake = createFakeKimiBin(); - const requested = "123e4567-e89b-42d3-a456-426614174000"; - const returned = "33333333-3333-4333-8333-333333333333"; - try { - createKimiSessionFixture({ home, cwd, sessionId: requested }); - const result = await runCompanion( - ["ask", "--provider", "kimi", "--resume", requested, "--json", "__reply=PONG"], - { - cwd, - env: cleanEnv({ - HOME: home, - USERPROFILE: home, - CLAUDE_PLUGIN_DATA: pluginData, - KIMI_CLI_BIN: fake.bin, - }), - } - ); - - assert.equal(result.code, 0, result.stderr); - const payload = JSON.parse(result.stdout); - assert.equal(payload.resumeMismatched, true); - assert.match( - result.stderr, - new RegExp(`Warning: requested --resume ${requested} did not match returned session ${returned}`) - ); - } finally { - fake.cleanup(); - fs.rmSync(pluginData, { recursive: true, force: true }); - fs.rmSync(home, { recursive: true, force: true }); - fs.rmSync(cwd, { recursive: true, force: true }); - } -}); - test("integration: setup and ask succeed for gemini via bundled companion", async () => { const pluginData = fs.mkdtempSync(path.join(os.tmpdir(), "polycli-plugin-data-")); const fake = createFakeGeminiBin(); @@ -1283,7 +1233,6 @@ test("integration: ask constrains kimi to a visible non-thinking answer", async CLAUDE_PLUGIN_DATA: pluginData, KIMI_CLI_BIN: fake.bin, KIMI_ARGV_LOG: argLog, - KIMI_REQUIRE_NO_THINKING: "1", KIMI_FIXED_REPLY: "KIMI_ASK_OK", }); const ask = await runCompanion( @@ -1296,8 +1245,10 @@ test("integration: ask constrains kimi to a visible non-thinking answer", async const logged = readJsonLine(argLog); const argv = logged.argv.join(" "); - assert.match(argv, /--no-thinking/); - assert.match(argv, /--max-steps-per-turn 1/); + // kimi-code one-shot invocation; the removed --no-thinking/--max-steps-per-turn flags must NOT reappear. + assert.match(argv, /-p /); + assert.match(argv, /--output-format stream-json/); + assert.doesNotMatch(argv, /--no-thinking|--max-steps-per-turn/); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true }); @@ -1324,8 +1275,8 @@ test("integration: review constrains kimi to one non-thinking turn", async () => assert.equal(payload.response, "REVIEW_OK"); const logged = JSON.parse(fs.readFileSync(argLog, "utf8").trim()); - assert.match(logged.argv.join(" "), /--no-thinking/); - assert.match(logged.argv.join(" "), /--max-steps-per-turn 1/); + assert.match(logged.argv.join(" "), /--output-format stream-json/); + assert.doesNotMatch(logged.argv.join(" "), /--no-thinking|--max-steps-per-turn/); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true }); @@ -1364,8 +1315,8 @@ test("integration: review --background preserves kimi runtime options and stored assert.equal(payload.job.jobId, started.job.jobId); const logged = JSON.parse(fs.readFileSync(argLog, "utf8").trim()); - assert.match(logged.argv.join(" "), /--no-thinking/); - assert.match(logged.argv.join(" "), /--max-steps-per-turn 1/); + assert.match(logged.argv.join(" "), /--output-format stream-json/); + assert.doesNotMatch(logged.argv.join(" "), /--no-thinking|--max-steps-per-turn/); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true }); diff --git a/plugins/polycli/scripts/tests/prompt-runtime.test.mjs b/plugins/polycli/scripts/tests/prompt-runtime.test.mjs index a5fc291..d790e83 100644 --- a/plugins/polycli/scripts/tests/prompt-runtime.test.mjs +++ b/plugins/polycli/scripts/tests/prompt-runtime.test.mjs @@ -6,13 +6,15 @@ import { PROMPT_FINAL_ANSWER_APPEND_SYSTEM, } from "../lib/prompt-runtime.mjs"; -test("buildPromptRuntimeOptions constrains kimi ask to visible plan-mode text", () => { +test("buildPromptRuntimeOptions leaves kimi ask unconstrained under kimi-code (no -p-compatible flags)", () => { const options = buildPromptRuntimeOptions({ provider: "kimi", kind: "ask", }); - assert.deepEqual(options.extraArgs, ["--plan", "--no-thinking", "--max-steps-per-turn", "1"]); + // kimi-code's -p one-shot mode rejects --plan/--auto and dropped --no-thinking/--max-steps-per-turn, + // so there are no per-invocation ask constraints to add. + assert.deepEqual(options, {}); }); test("buildPromptRuntimeOptions leaves kimi rescue unconstrained", () => { diff --git a/plugins/polycli/scripts/tests/review-flags-consistency.test.mjs b/plugins/polycli/scripts/tests/review-flags-consistency.test.mjs index 78d0d75..4a34511 100644 --- a/plugins/polycli/scripts/tests/review-flags-consistency.test.mjs +++ b/plugins/polycli/scripts/tests/review-flags-consistency.test.mjs @@ -54,8 +54,9 @@ test("assertNoReviewConstraintOverride rejects a bad value on the declared readO `${provider} should reject ${key}=yolo` ); } - // false-only guards (opencode/cmd/kimi): a non-false value is rejected. - for (const provider of ["opencode", "cmd", "kimi"]) { + // false-only guards (opencode/cmd): a non-false value is rejected. (kimi review is prompt-only + // under kimi-code, like minimax — it has no readOnlyOptionKey to guard.) + for (const provider of ["opencode", "cmd"]) { const key = REVIEW_FLAG_EXPECTATIONS[provider].readOnlyOptionKey; assert.throws( () => buildReviewRuntimeOptions({ provider, runtimeOptions: { [key]: true } }), diff --git a/plugins/polycli/scripts/tests/sessions.test.mjs b/plugins/polycli/scripts/tests/sessions.test.mjs index 6849448..582df4b 100644 --- a/plugins/polycli/scripts/tests/sessions.test.mjs +++ b/plugins/polycli/scripts/tests/sessions.test.mjs @@ -15,10 +15,6 @@ const HOME = '/home/tester'; const CWD = '/Users/tester/-Code-/polycli'; const SID = '01555281-0f41-48d9-bd9c-775a19ed3cda'; -function md5(value) { - return createHash('md5').update(value).digest('hex'); -} - // ---- deriveSessionArtifactCandidate ---- test('deriveSessionArtifactCandidate (claude) → one exact /.jsonl path', () => { @@ -39,14 +35,16 @@ test('deriveSessionArtifactCandidate (claude) → one exact // dir, basename==sessionId', () => { +test('deriveSessionArtifactCandidate (kimi) → kimi-code wd__/ dir, basename==sessionId', () => { const candidate = deriveSessionArtifactCandidate({ provider: 'kimi', sessionId: SID, workspaceRoot: CWD, homedir: HOME, }); - const expected = path.join(HOME, '.kimi', 'sessions', md5(CWD), SID); + // CWD does not exist on disk in the test, so the derivation falls back to the given path. + const slug = `wd_${path.basename(CWD)}_${createHash('sha256').update(CWD).digest('hex').slice(0, 12)}`; + const expected = path.join(HOME, '.kimi-code', 'sessions', slug, SID); assert.equal(candidate.path, expected); assert.equal(path.basename(candidate.path), SID); assert.equal(candidate.kind, 'dir'); @@ -287,7 +285,13 @@ test('planPurge validates kimi dir basename equals sessionId', () => { const rec = { provider: 'kimi', sessionId: SID, - sessionArtifactPath: path.join(HOME, '.kimi', 'sessions', md5(CWD), SID), + sessionArtifactPath: path.join( + HOME, + '.kimi-code', + 'sessions', + `wd_${path.basename(CWD)}_${createHash('sha256').update(CWD).digest('hex').slice(0, 12)}`, + SID, + ), workspaceRoot: CWD, }; const plan = planPurge({ diff --git a/scripts/check-review-cli-drift.mjs b/scripts/check-review-cli-drift.mjs index 76a73e6..1caa9b3 100755 --- a/scripts/check-review-cli-drift.mjs +++ b/scripts/check-review-cli-drift.mjs @@ -130,6 +130,13 @@ const CHECKS = [ expect: REVIEW_FLAG_EXPECTATIONS.cmd.expectFlags, notes: "Review hard constraint uses --permission-mode plan.", }, + { + provider: "kimi", + bin: process.env.KIMI_CLI_BIN || "kimi", + helpArgs: ["--help"], + expect: REVIEW_FLAG_EXPECTATIONS.kimi.expectFlags, + notes: "kimi-code one-shot uses -p/--prompt + --output-format stream-json (review is prompt-only; -p mode rejects --plan/--auto). Drift here means the runtime invocation contract changed.", + }, { provider: "agy", bin: process.env.AGY_CLI_BIN || "agy", From 4c6579b0461b587f48d2725cb05281de9d3e1f5c Mon Sep 17 00:00:00 2001 From: bbingz Date: Tue, 2 Jun 2026 16:04:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(kimi):=20Codex=20review-gate=20fixes=20?= =?UTF-8?q?=E2=80=94=20correct=20resume=20flag=20+=20tighten=20session=20p?= =?UTF-8?q?arse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the pre-merge Codex review gate for PR #6. - resume flag (real bug): buildKimiInvocation emitted `-r ` for resume-by-id, but kimi-code v0.6.0 has NO `-r` flag — `kimi --help` shows resume-by-id is `-S, --session [id]` (continue-last `-C` is correct). `-r` would be rejected at runtime, and the path IS reachable (`polycli rescue --provider kimi --resume ` threads through to it, per integration test). Switch to `--session `; update unit + integration assertions and the comment. - session-id parse (hardening): parseKimiStreamText adopted session_id from ANY `role:"meta"` event; the documented source is specifically the `{role:"meta", type:"session.resume_hint", session_id:"session_"}` event. Require `type === "session.resume_hint"` so an unrelated meta event carrying a session_id is not promoted. Add a regression test. - fragile review test (latent failure exposed by committing the migration): two `review --base HEAD~1 --scope branch` integration tests asserted `doesNotMatch(argv.join(" "), /--no-thinking|--max-steps.../)`. The reviewed diff is embedded in the single `-p ` element and legitimately mentions those removed flags as deleted-code text, so the substring check false-positives on this very migration's diff. Check the flags as discrete argv tokens (argv.includes) instead — same intent, no false positive. Codex false-positives (verified, NOT changed): #1 `-p`+`--plan/--auto/--yolo` combination is latent-only — no caller injects those (review extraArgTokens empty; prompt-runtime kimi-ask is plain `-p`); #4 the `~/.kimi/` literal is an intentional migration-history comment, code uses `~/.kimi-code/`. npm test 452/452 (was 451; +1 session-parse regression). 5 bundles byte-identical. --- packages/polycli-runtime/src/kimi.js | 7 ++++-- packages/polycli-runtime/test/kimi.test.js | 18 ++++++++++++-- .../bin/polycli-companion.bundle.mjs | 4 ++-- .../scripts/polycli-companion.bundle.mjs | 4 ++-- .../scripts/polycli-companion.bundle.mjs | 4 ++-- .../scripts/polycli-companion.bundle.mjs | 4 ++-- .../scripts/polycli-companion.bundle.mjs | 4 ++-- .../scripts/tests/integration.test.mjs | 24 ++++++++++++++----- 8 files changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/polycli-runtime/src/kimi.js b/packages/polycli-runtime/src/kimi.js index eaa3c12..f75acfd 100644 --- a/packages/polycli-runtime/src/kimi.js +++ b/packages/polycli-runtime/src/kimi.js @@ -39,13 +39,15 @@ export function buildKimiInvocation({ // kimi-code one-shot mode: `-p --output-format stream-json`. NOTE: `-p` cannot be // combined with `--yolo`, `--auto`, or `--plan` (the CLI rejects them) — `-p` is itself the // non-interactive headless runner, so no approval flag is passed. Resume is delegated to the - // CLI: `-r ` (per the CLI's own session.resume_hint) or `-C` to continue the last session. + // CLI: `--session ` (kimi-code v0.6.0's `-S, --session [id]`, per its own + // session.resume_hint) or `-C` to continue the last session. NOTE: the legacy python + // kimi-cli used `-r`; kimi-code v0.6.0 has no `-r` flag and rejects it. const args = ["-p", String(prompt ?? ""), "--output-format", "stream-json"]; if (model) args.push("-m", model); if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -96,6 +98,7 @@ export function parseKimiStreamText(text) { // fabricate an id from a UUID the user asked about). if (!sessionId && event.role === "meta" + && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; diff --git a/packages/polycli-runtime/test/kimi.test.js b/packages/polycli-runtime/test/kimi.test.js index 0118452..e5bf7c2 100644 --- a/packages/polycli-runtime/test/kimi.test.js +++ b/packages/polycli-runtime/test/kimi.test.js @@ -29,7 +29,7 @@ function withFakeKimiBin(source, fn) { } const RESUME_HINT = (id) => - JSON.stringify({ role: "meta", type: "session.resume_hint", session_id: id, command: `kimi -r ${id}` }); + JSON.stringify({ role: "meta", type: "session.resume_hint", session_id: id, command: `kimi --session ${id}` }); test("buildKimiInvocation targets kimi-code one-shot -p + stream-json (no --yolo/--print/--input-format)", () => { const invocation = buildKimiInvocation({ @@ -45,7 +45,7 @@ test("buildKimiInvocation targets kimi-code one-shot -p + stream-json (no --yolo "stream-json", "-m", "kimi-for-coding", - "-r", + "--session", "session_123e4567-e89b-42d3-a456-426614174000", ]); }); @@ -79,6 +79,20 @@ test("parseKimiStreamText keeps assistant text, tool events, and reads the struc assert.equal(parsed.sessionId, "session_a3e525ea-0ad2-49b0-9feb-477ebd05a9ac"); }); +test("parseKimiStreamText only adopts session_id from a session.resume_hint meta event", () => { + // A meta event of a different type that happens to carry a session_id must NOT be promoted — + // sessionId comes solely from the documented session.resume_hint event. + const parsed = parseKimiStreamText( + [ + '{"role":"meta","type":"session.start","session_id":"session_should-not-be-used"}', + '{"role":"assistant","content":"hi"}', + ].join("\n") + ); + + assert.equal(parsed.response, "hi"); + assert.equal(parsed.sessionId, null); +}); + test("extractKimiText supports both string and array assistant content", () => { assert.equal(extractKimiText({ role: "assistant", content: "final body" }), "final body"); assert.equal( diff --git a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs index cba01fd..7a99a45 100755 --- a/packages/polycli-terminal/bin/polycli-companion.bundle.mjs +++ b/packages/polycli-terminal/bin/polycli-companion.bundle.mjs @@ -1486,7 +1486,7 @@ function buildKimiInvocation({ if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -1523,7 +1523,7 @@ function parseKimiStreamText(text) { if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); - if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + if (!sessionId && event.role === "meta" && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; } if (!model && typeof event.model === "string") model = event.model; diff --git a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs index cba01fd..7a99a45 100755 --- a/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-codex/scripts/polycli-companion.bundle.mjs @@ -1486,7 +1486,7 @@ function buildKimiInvocation({ if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -1523,7 +1523,7 @@ function parseKimiStreamText(text) { if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); - if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + if (!sessionId && event.role === "meta" && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; } if (!model && typeof event.model === "string") model = event.model; diff --git a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs index cba01fd..7a99a45 100755 --- a/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-copilot/scripts/polycli-companion.bundle.mjs @@ -1486,7 +1486,7 @@ function buildKimiInvocation({ if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -1523,7 +1523,7 @@ function parseKimiStreamText(text) { if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); - if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + if (!sessionId && event.role === "meta" && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; } if (!model && typeof event.model === "string") model = event.model; diff --git a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs index cba01fd..7a99a45 100755 --- a/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli-opencode/scripts/polycli-companion.bundle.mjs @@ -1486,7 +1486,7 @@ function buildKimiInvocation({ if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -1523,7 +1523,7 @@ function parseKimiStreamText(text) { if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); - if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + if (!sessionId && event.role === "meta" && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; } if (!model && typeof event.model === "string") model = event.model; diff --git a/plugins/polycli/scripts/polycli-companion.bundle.mjs b/plugins/polycli/scripts/polycli-companion.bundle.mjs index cba01fd..7a99a45 100755 --- a/plugins/polycli/scripts/polycli-companion.bundle.mjs +++ b/plugins/polycli/scripts/polycli-companion.bundle.mjs @@ -1486,7 +1486,7 @@ function buildKimiInvocation({ if (resumeLast) { args.push("-C"); } else if (resumeSessionId) { - args.push("-r", resumeSessionId); + args.push("--session", resumeSessionId); } if (extraArgs.length > 0) args.push(...extraArgs); return { bin, args }; @@ -1523,7 +1523,7 @@ function parseKimiStreamText(text) { if (!event) continue; events.push(event); if (event.role === "tool") toolEvents.push(event); - if (!sessionId && event.role === "meta" && typeof event.session_id === "string" && event.session_id.length > 0) { + if (!sessionId && event.role === "meta" && event.type === "session.resume_hint" && typeof event.session_id === "string" && event.session_id.length > 0) { sessionId = event.session_id; } if (!model && typeof event.model === "string") model = event.model; diff --git a/plugins/polycli/scripts/tests/integration.test.mjs b/plugins/polycli/scripts/tests/integration.test.mjs index 0129631..90b14fc 100644 --- a/plugins/polycli/scripts/tests/integration.test.mjs +++ b/plugins/polycli/scripts/tests/integration.test.mjs @@ -1066,16 +1066,16 @@ test("integration: kimi ask parses --resume-last, --resume, and --fresh", async assert.equal(resumeLast.code, 0, resumeLast.stderr); let logged = readJsonLine(argLog); assert.equal(logged.argv.includes("-C"), true); - assert.equal(logged.argv.includes("-r"), false); + assert.equal(logged.argv.includes("--session"), false); - // --resume -> -r passed straight through to the CLI. + // --resume -> --session passed straight through to the CLI. const explicitResume = await runCompanion( ["rescue", "--provider", "kimi", "--resume", sessionId, "--json", "__reply=IGNORED"], { cwd, env } ); assert.equal(explicitResume.code, 0, explicitResume.stderr); logged = readJsonLine(argLog); - assert.deepEqual(logged.argv.slice(logged.argv.indexOf("-r"), logged.argv.indexOf("-r") + 2), ["-r", sessionId]); + assert.deepEqual(logged.argv.slice(logged.argv.indexOf("--session"), logged.argv.indexOf("--session") + 2), ["--session", sessionId]); const fresh = await runCompanion( ["ask", "--provider", "kimi", "--fresh", "--json", "__reply=IGNORED"], @@ -1083,7 +1083,7 @@ test("integration: kimi ask parses --resume-last, --resume, and --fresh", async ); assert.equal(fresh.code, 0, fresh.stderr); logged = readJsonLine(argLog); - assert.equal(logged.argv.includes("-r"), false); + assert.equal(logged.argv.includes("--session"), false); assert.equal(logged.argv.includes("-C"), false); } finally { fake.cleanup(); @@ -1276,7 +1276,13 @@ test("integration: review constrains kimi to one non-thinking turn", async () => const logged = JSON.parse(fs.readFileSync(argLog, "utf8").trim()); assert.match(logged.argv.join(" "), /--output-format stream-json/); - assert.doesNotMatch(logged.argv.join(" "), /--no-thinking|--max-steps-per-turn/); + // Check the legacy flags as discrete argv tokens, NOT as substrings of the joined string: + // the reviewed diff is embedded inside the single `-p ` element and can legitimately + // mention `--no-thinking`/`--max-steps-per-turn` as removed-code text (e.g. this very migration). + assert.ok( + !logged.argv.includes("--no-thinking") && !logged.argv.includes("--max-steps-per-turn"), + `kimi review must not pass legacy python kimi-cli flags as arguments; argv: ${logged.argv.join(" ")}` + ); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true }); @@ -1316,7 +1322,13 @@ test("integration: review --background preserves kimi runtime options and stored const logged = JSON.parse(fs.readFileSync(argLog, "utf8").trim()); assert.match(logged.argv.join(" "), /--output-format stream-json/); - assert.doesNotMatch(logged.argv.join(" "), /--no-thinking|--max-steps-per-turn/); + // Check the legacy flags as discrete argv tokens, NOT as substrings of the joined string: + // the reviewed diff is embedded inside the single `-p ` element and can legitimately + // mention `--no-thinking`/`--max-steps-per-turn` as removed-code text (e.g. this very migration). + assert.ok( + !logged.argv.includes("--no-thinking") && !logged.argv.includes("--max-steps-per-turn"), + `kimi review must not pass legacy python kimi-cli flags as arguments; argv: ${logged.argv.join(" ")}` + ); } finally { fake.cleanup(); fs.rmSync(pluginData, { recursive: true, force: true });