kimi-plugin-cc 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +1096 -0
- package/LICENSE +201 -0
- package/README.md +772 -0
- package/agents/kimi-ask.md +82 -0
- package/agents/kimi-challenge.md +82 -0
- package/agents/kimi-pursue.md +91 -0
- package/agents/kimi-rescue.md +85 -0
- package/agents/kimi-review.md +82 -0
- package/agents/kimi-swarm-write.md +92 -0
- package/agents/kimi-swarm.md +83 -0
- package/commands/README.md +16 -0
- package/commands/ask.md +32 -0
- package/commands/cancel.md +17 -0
- package/commands/challenge.md +29 -0
- package/commands/pursue.md +37 -0
- package/commands/replay.md +13 -0
- package/commands/rescue.md +32 -0
- package/commands/result.md +13 -0
- package/commands/review.md +29 -0
- package/commands/setup.md +23 -0
- package/commands/status.md +13 -0
- package/commands/swarm.md +38 -0
- package/dist/background-spawn.js +224 -0
- package/dist/cli-cancellation.js +43 -0
- package/dist/cli-client.js +845 -0
- package/dist/commands/ask.js +327 -0
- package/dist/commands/cancel.js +178 -0
- package/dist/commands/cli-helpers.js +112 -0
- package/dist/commands/models.js +140 -0
- package/dist/commands/not-implemented.js +4 -0
- package/dist/commands/pursue.js +311 -0
- package/dist/commands/registry.js +87 -0
- package/dist/commands/repair-sessions.js +159 -0
- package/dist/commands/replay.js +123 -0
- package/dist/commands/rescue.js +333 -0
- package/dist/commands/result.js +58 -0
- package/dist/commands/review-gate.js +393 -0
- package/dist/commands/review.js +218 -0
- package/dist/commands/setup.js +1132 -0
- package/dist/commands/status.js +27 -0
- package/dist/commands/swarm.js +576 -0
- package/dist/commands/worker.js +31 -0
- package/dist/companion.js +114 -0
- package/dist/config.js +22 -0
- package/dist/errors.js +40 -0
- package/dist/git.js +215 -0
- package/dist/hooks/approval-hook.js +112 -0
- package/dist/hooks/approval-policy.js +254 -0
- package/dist/hooks/config-safety.js +637 -0
- package/dist/hooks/install-paths.js +504 -0
- package/dist/hooks/install.js +281 -0
- package/dist/hooks/managed-block.js +745 -0
- package/dist/hooks/review-gate-stop.js +19 -0
- package/dist/job-store.js +559 -0
- package/dist/jobs.js +202 -0
- package/dist/kimi-command.js +72 -0
- package/dist/kimi-engine.js +624 -0
- package/dist/kimi-errors.js +220 -0
- package/dist/kimi-home.js +9 -0
- package/dist/kimi-timeouts.js +68 -0
- package/dist/kimi-version-probe.js +1410 -0
- package/dist/logging.js +22 -0
- package/dist/native-v2-preflight.js +417 -0
- package/dist/package.json +1 -0
- package/dist/parsing.js +553 -0
- package/dist/paths.js +27 -0
- package/dist/plugin-data.js +98 -0
- package/dist/process-tree.js +448 -0
- package/dist/render.js +159 -0
- package/dist/rescue-approval.js +951 -0
- package/dist/schemas/review-gate-output.js +43 -0
- package/dist/session-title.js +293 -0
- package/dist/stream-json.js +498 -0
- package/dist/types.js +1 -0
- package/dist/vendor/shell-quote/LICENSE +24 -0
- package/dist/vendor/shell-quote/index.js +1 -0
- package/dist/vendor/shell-quote/parse.js +229 -0
- package/dist/vendor/smol-toml/LICENSE +24 -0
- package/dist/vendor/smol-toml/date.js +127 -0
- package/dist/vendor/smol-toml/error.js +63 -0
- package/dist/vendor/smol-toml/extract.js +100 -0
- package/dist/vendor/smol-toml/parse.js +148 -0
- package/dist/vendor/smol-toml/primitive.js +179 -0
- package/dist/vendor/smol-toml/struct.js +184 -0
- package/dist/vendor/smol-toml/util.js +113 -0
- package/dist/version.js +7 -0
- package/docs/migration.md +215 -0
- package/docs/models.md +82 -0
- package/docs/registry-releases.md +70 -0
- package/docs/safety.md +318 -0
- package/docs/session-visibility.md +32 -0
- package/hooks/README.md +9 -0
- package/hooks/hooks.json +16 -0
- package/package.json +67 -0
- package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
- package/plugins/kimi-codex/dist/background-spawn.js +224 -0
- package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-codex/dist/cli-client.js +845 -0
- package/plugins/kimi-codex/dist/commands/ask.js +327 -0
- package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
- package/plugins/kimi-codex/dist/commands/models.js +140 -0
- package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
- package/plugins/kimi-codex/dist/commands/registry.js +87 -0
- package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
- package/plugins/kimi-codex/dist/commands/replay.js +123 -0
- package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
- package/plugins/kimi-codex/dist/commands/result.js +58 -0
- package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
- package/plugins/kimi-codex/dist/commands/review.js +218 -0
- package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
- package/plugins/kimi-codex/dist/commands/status.js +27 -0
- package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
- package/plugins/kimi-codex/dist/commands/worker.js +31 -0
- package/plugins/kimi-codex/dist/companion.js +114 -0
- package/plugins/kimi-codex/dist/config.js +22 -0
- package/plugins/kimi-codex/dist/errors.js +40 -0
- package/plugins/kimi-codex/dist/git.js +215 -0
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
- package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
- package/plugins/kimi-codex/dist/hooks/install.js +281 -0
- package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
- package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-codex/dist/job-store.js +559 -0
- package/plugins/kimi-codex/dist/jobs.js +202 -0
- package/plugins/kimi-codex/dist/kimi-command.js +72 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
- package/plugins/kimi-codex/dist/kimi-home.js +9 -0
- package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
- package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
- package/plugins/kimi-codex/dist/logging.js +22 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-codex/dist/package.json +1 -0
- package/plugins/kimi-codex/dist/parsing.js +553 -0
- package/plugins/kimi-codex/dist/paths.js +27 -0
- package/plugins/kimi-codex/dist/plugin-data.js +98 -0
- package/plugins/kimi-codex/dist/process-tree.js +448 -0
- package/plugins/kimi-codex/dist/render.js +159 -0
- package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
- package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-codex/dist/session-title.js +293 -0
- package/plugins/kimi-codex/dist/stream-json.js +498 -0
- package/plugins/kimi-codex/dist/types.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
- package/plugins/kimi-codex/dist/version.js +7 -0
- package/plugins/kimi-codex/scripts/companion.sh +37 -0
- package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
- package/scripts/companion.sh +37 -0
- package/scripts/review-gate-hook.sh +32 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
4
|
+
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
5
|
+
import { assertPersistedResumeLineage, executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, reconcileHistoricalJobProvenance, } from "../kimi-engine.js";
|
|
6
|
+
import { getManagedCommandConfig } from "./registry.js";
|
|
7
|
+
import { RuntimeError } from "../errors.js";
|
|
8
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
9
|
+
import { digestPrompt, markJobCancelled, markJobFailed, sweepStaleJobs } from "../jobs.js";
|
|
10
|
+
import { JobStore } from "../job-store.js";
|
|
11
|
+
import { KIMI_ASK_PROMPT_TIMEOUT_MS } from "../kimi-timeouts.js";
|
|
12
|
+
import { writeInvocationLogHeader } from "../logging.js";
|
|
13
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
14
|
+
import { parseAskArgs } from "../parsing.js";
|
|
15
|
+
import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
|
|
16
|
+
import { classifyManagedCommandFailure } from "../kimi-errors.js";
|
|
17
|
+
import { startBackgroundJob } from "../background-spawn.js";
|
|
18
|
+
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
19
|
+
import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
|
|
20
|
+
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
21
|
+
// v1.0 cutover note (PR 2):
|
|
22
|
+
//
|
|
23
|
+
// The v0.4 wire client/initialize/prompt sequence is replaced with a
|
|
24
|
+
// single `runCliPrompt` against `kimi -p --output-format stream-json`.
|
|
25
|
+
// The PreToolUse hook reads `KIMI_PLUGIN_CC_CMD=ask` and applies the
|
|
26
|
+
// same read-only tool policy as review/challenge/review_gate.
|
|
27
|
+
//
|
|
28
|
+
// Session id handling differs from v0.4:
|
|
29
|
+
//
|
|
30
|
+
// - v0.4 the client assigned the kimi session id upfront (passed via
|
|
31
|
+
// `--session <uuid>` on the wire command line).
|
|
32
|
+
// - v1.0 kimi-code assigns the session id and announces it via stderr
|
|
33
|
+
// ("To resume this session: kimi -r <uuid>"). We capture that id
|
|
34
|
+
// after the call and update the JobStore row.
|
|
35
|
+
// - For `--resume`, we pass the prior session id via `-r`; kimi-code
|
|
36
|
+
// keeps it across resumes.
|
|
37
|
+
const ASK_SUMMARY_MAX = 120;
|
|
38
|
+
const ASK_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
|
|
39
|
+
const ASK_PROMPT_PREFIX = [
|
|
40
|
+
"Answer the user's question directly in free-form prose.",
|
|
41
|
+
"Stay read-only.",
|
|
42
|
+
"Do not emit JSON unless the user explicitly asks for it.",
|
|
43
|
+
"",
|
|
44
|
+
"User question:",
|
|
45
|
+
"",
|
|
46
|
+
].join("\n");
|
|
47
|
+
export async function runAsk(argv, context) {
|
|
48
|
+
const parsed = parseAskArgs(argv);
|
|
49
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
50
|
+
await requireAskHookInstalled(context);
|
|
51
|
+
}
|
|
52
|
+
const paths = resolvePluginPaths(context.env);
|
|
53
|
+
await ensurePluginPaths(paths);
|
|
54
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
55
|
+
const askConfig = getManagedCommandConfig("ask");
|
|
56
|
+
const store = new JobStore(paths);
|
|
57
|
+
try {
|
|
58
|
+
await sweepStaleJobs(store, paths);
|
|
59
|
+
const jobId = randomUUID();
|
|
60
|
+
const sessionResolution = await resolveAskSession(store, repoIdentity.repoId, parsed.fresh, parsed.resume, parsed.resumeTarget);
|
|
61
|
+
const askPrompt = buildAskPrompt(parsed.prompt, sessionResolution.reusedSession);
|
|
62
|
+
const executionPlan = await prepareKimiExecutionPlan({
|
|
63
|
+
operationKind: "ask",
|
|
64
|
+
cwd: context.cwd,
|
|
65
|
+
env: context.env,
|
|
66
|
+
resumedFromJobId: sessionResolution.resumedFromJobId,
|
|
67
|
+
resumeSessionId: sessionResolution.kimiSessionId ?? undefined,
|
|
68
|
+
resumeSource: sessionResolution.sourceJob,
|
|
69
|
+
});
|
|
70
|
+
const logPath = path.join(paths.logsDir, `ask-${jobId}.jsonl`);
|
|
71
|
+
const job = store.createJob({
|
|
72
|
+
job_id: jobId,
|
|
73
|
+
repo_id: repoIdentity.repoId,
|
|
74
|
+
command_type: "ask",
|
|
75
|
+
cwd: context.cwd,
|
|
76
|
+
model: parsed.model ?? null,
|
|
77
|
+
thinking: parsed.thinking ?? null,
|
|
78
|
+
background: parsed.background,
|
|
79
|
+
pid: parsed.background ? null : process.pid,
|
|
80
|
+
kimi_pid: null,
|
|
81
|
+
status: "running",
|
|
82
|
+
kimi_session_id: sessionResolution.kimiSessionId,
|
|
83
|
+
...persistedExecutionPlanFields(executionPlan),
|
|
84
|
+
agent_profile: ASK_AGENT_PROFILE_PLACEHOLDER,
|
|
85
|
+
prompt_digest: digestPrompt(askPrompt),
|
|
86
|
+
summary: shorten(parsed.prompt ?? askPrompt, ASK_SUMMARY_MAX),
|
|
87
|
+
phase: parsed.background ? "queued" : "starting",
|
|
88
|
+
final_output_path: null,
|
|
89
|
+
stream_log_path: logPath,
|
|
90
|
+
error: null,
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
await writeInvocationLogHeader(logPath, {
|
|
94
|
+
commandType: "ask",
|
|
95
|
+
kimiSessionId: sessionResolution.kimiSessionId ?? "(pending)",
|
|
96
|
+
cwd: context.cwd,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const classified = new RuntimeError("ASK_LOG_HEADER_FAILED", `Failed to write ask invocation log header: ${error.message ?? String(error)}`, "ask.log-header", error instanceof Error ? { cause: error } : undefined);
|
|
101
|
+
await markJobFailed(store, paths, job, classified, askConfig.cancellation.failedSummary, { phase: "failed" });
|
|
102
|
+
throw classified;
|
|
103
|
+
}
|
|
104
|
+
if (parsed.background) {
|
|
105
|
+
return startBackgroundJob(job, askPrompt, context, paths, {
|
|
106
|
+
workerKind: "ask",
|
|
107
|
+
wait: parsed.wait,
|
|
108
|
+
promptEnvVar: "KIMI_PLUGIN_CC_ASK_PROMPT_B64",
|
|
109
|
+
reusedSessionEnvVar: "KIMI_PLUGIN_CC_ASK_REUSED_SESSION",
|
|
110
|
+
reusedSession: sessionResolution.reusedSession,
|
|
111
|
+
failedSummary: askConfig.cancellation.failedSummary,
|
|
112
|
+
missingResultErrorCode: "ASK_RESULT_MISSING",
|
|
113
|
+
spawnFailedErrorCode: "ASK_WORKER_SPAWN_FAILED",
|
|
114
|
+
earlyExitErrorCode: "ASK_WORKER_EXITED_EARLY",
|
|
115
|
+
nodeBinInvalidErrorCode: "ASK_NODE_BIN_INVALID",
|
|
116
|
+
waitStage: "ask.wait",
|
|
117
|
+
spawnStage: "ask.worker.spawn",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const completed = await executeAskJob(job.job_id, askPrompt, context);
|
|
121
|
+
if (!completed.final_output_path) {
|
|
122
|
+
throw new RuntimeError("ASK_RESULT_MISSING", "Ask finished without a rendered result.", "ask.result");
|
|
123
|
+
}
|
|
124
|
+
return (await readArtifact(completed.final_output_path)).trimEnd();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
store.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export async function executeAskJob(jobId, prompt, context, options) {
|
|
131
|
+
const paths = resolvePluginPaths(context.env);
|
|
132
|
+
await ensurePluginPaths(paths);
|
|
133
|
+
const store = new JobStore(paths);
|
|
134
|
+
const job = store.getJob(jobId);
|
|
135
|
+
if (!job) {
|
|
136
|
+
store.close();
|
|
137
|
+
throw new RuntimeError("JOB_NOT_FOUND", `Ask job ${jobId} was not found.`, "ask.worker");
|
|
138
|
+
}
|
|
139
|
+
const askConfig = getManagedCommandConfig("ask");
|
|
140
|
+
const cancel = askConfig.cancellation;
|
|
141
|
+
let handlers;
|
|
142
|
+
try {
|
|
143
|
+
// Re-verify in the worker so a background job cannot outlive or bypass
|
|
144
|
+
// enforcement drift between the user invocation and the actual Kimi spawn.
|
|
145
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
146
|
+
await requireAskHookInstalled(context);
|
|
147
|
+
}
|
|
148
|
+
handlers = createCliCancellationHandlers();
|
|
149
|
+
const executionPlan = executionPlanFromPersisted(job);
|
|
150
|
+
// Re-validate resume lineage from the store before spawning: a detached
|
|
151
|
+
// worker (or a forged queued row) reloads the persisted plan and would
|
|
152
|
+
// otherwise hand kimi_session_id to the spawn without the dispatch-time
|
|
153
|
+
// lineage check. Fresh sessions (no resumedFromJobId) are exempt.
|
|
154
|
+
if (job.kimi_session_id !== null) {
|
|
155
|
+
await assertPersistedResumeLineage(store, executionPlan, job.kimi_session_id, "ask");
|
|
156
|
+
}
|
|
157
|
+
if (options?.workerPid) {
|
|
158
|
+
store.updateRunningJob(job.job_id, { pid: options.workerPid, phase: "worker-running" });
|
|
159
|
+
}
|
|
160
|
+
const result = await runCliPromptWithBudget({
|
|
161
|
+
cwd: job.cwd,
|
|
162
|
+
env: context.env,
|
|
163
|
+
command: executionPlan.command,
|
|
164
|
+
prefixArgs: [...executionPlan.prefixArgs],
|
|
165
|
+
executionPlan,
|
|
166
|
+
prompt,
|
|
167
|
+
commandLabel: "ask",
|
|
168
|
+
model: job.model ?? undefined,
|
|
169
|
+
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
170
|
+
logPath: job.stream_log_path,
|
|
171
|
+
signal: handlers.signal,
|
|
172
|
+
}, KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
|
|
173
|
+
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
174
|
+
if (handlers.cancelling) {
|
|
175
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "ask.runtime");
|
|
176
|
+
}
|
|
177
|
+
assertCliResultSuccess(result, "ask.runtime");
|
|
178
|
+
// Persist whichever session id kimi actually announced. For fresh
|
|
179
|
+
// sessions this is the only chance to record it. For resumed
|
|
180
|
+
// sessions it should match the input, but capture anyway in case
|
|
181
|
+
// kimi-code mints a new id on each turn.
|
|
182
|
+
if (result.sessionId !== undefined &&
|
|
183
|
+
result.sessionId.length > 0 &&
|
|
184
|
+
result.sessionId !== job.kimi_session_id) {
|
|
185
|
+
// length>0 guard: empty-string capture would poison the row. The
|
|
186
|
+
// warning helper's null-ish check would also fire but only after
|
|
187
|
+
// the bad write. (Kimi alpha.4 challenge finding #3.)
|
|
188
|
+
store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
|
|
189
|
+
}
|
|
190
|
+
await syncKimiSessionTitle({
|
|
191
|
+
promptText: prompt,
|
|
192
|
+
env: context.env,
|
|
193
|
+
cwd: job.cwd,
|
|
194
|
+
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
195
|
+
title: buildKimiSessionTitle("ask", prompt.startsWith(ASK_PROMPT_PREFIX) ? prompt.slice(ASK_PROMPT_PREFIX.length) : prompt),
|
|
196
|
+
stderr: context.stderr,
|
|
197
|
+
});
|
|
198
|
+
// For ask, missing session id breaks both -r (latest-resume) and
|
|
199
|
+
// explicit --resume <id>. Warn loudly so the user notices before
|
|
200
|
+
// they try to continue the conversation.
|
|
201
|
+
if (job.kimi_session_id === null) {
|
|
202
|
+
warnIfSessionIdMissing(result, "ask", job.job_id, context.stderr);
|
|
203
|
+
}
|
|
204
|
+
const finalText = reassembleProseFromRecords(result.records);
|
|
205
|
+
const rendered = renderManagedJobOutput(job, finalText);
|
|
206
|
+
const artifactPath = await writeArtifact(paths, job, rendered.rendered);
|
|
207
|
+
if (handlers.cancelling) {
|
|
208
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterArtifact, "ask.runtime");
|
|
209
|
+
}
|
|
210
|
+
return (store.markCompleted(job.job_id, {
|
|
211
|
+
summary: rendered.summary,
|
|
212
|
+
phase: "done",
|
|
213
|
+
final_output_path: artifactPath,
|
|
214
|
+
error: null,
|
|
215
|
+
}) ?? job);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (handlers?.cancelling) {
|
|
219
|
+
const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "ask.runtime", error instanceof Error ? { cause: error } : undefined);
|
|
220
|
+
return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
|
|
221
|
+
}
|
|
222
|
+
// Hook preflight errors are already fully classified. Running them through
|
|
223
|
+
// the generic availability classifier can mistake words in the verifier's
|
|
224
|
+
// reason (for example "timed out") for a Kimi runtime timeout and erase the
|
|
225
|
+
// load-bearing hook-check code/stage from the persisted background job.
|
|
226
|
+
if (error instanceof RuntimeError && error.stage === "ask.hook-check") {
|
|
227
|
+
await markJobFailed(store, paths, job, error, cancel.failedSummary, { phase: "failed" });
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
const classified = classifyManagedCommandFailure(error, "ask", job.job_id);
|
|
231
|
+
await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
232
|
+
throw classified;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
handlers?.dispose();
|
|
236
|
+
store.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function requireAskHookInstalled(context) {
|
|
240
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK === "1") {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const installStatus = await verifyHookInstalled(context.env);
|
|
244
|
+
if (installStatus.installed) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
maybeWarnHookMissing(installStatus, "ask", context.stderr);
|
|
248
|
+
throw new RuntimeError("ASK_HOOK_NOT_INSTALLED", [
|
|
249
|
+
"ask refuses to run without the canonical kimi-plugin-cc PreToolUse hook.",
|
|
250
|
+
`Hook check failed: ${installStatus.reason ?? "unknown"}.`,
|
|
251
|
+
"Run /kimi:setup or $kimi-setup to install or repair this host's managed block.",
|
|
252
|
+
"Set KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1 only if you intentionally accept un-enforced execution.",
|
|
253
|
+
hookRefusalRetryProtocol(context.env),
|
|
254
|
+
].join(" "), "ask.hook-check", { details: hookRefusalDetails(installStatus) });
|
|
255
|
+
}
|
|
256
|
+
async function resolveAskSession(store, repoId, fresh, resume, resumeTarget) {
|
|
257
|
+
if (fresh) {
|
|
258
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
259
|
+
}
|
|
260
|
+
if (resumeTarget) {
|
|
261
|
+
const byJob = store.getJob(resumeTarget);
|
|
262
|
+
const scoped = byJob?.repo_id === repoId && byJob.command_type === "ask" ? byJob : null;
|
|
263
|
+
const exact = scoped ?? store.findAskJobBySession(repoId, resumeTarget);
|
|
264
|
+
if (!exact) {
|
|
265
|
+
throw new RuntimeError("ASK_RESUME_NOT_FOUND", `No ask job or session matched ${resumeTarget}.`, "ask.resume");
|
|
266
|
+
}
|
|
267
|
+
// Order matters: surface ASK_ALREADY_RUNNING before complaining that
|
|
268
|
+
// the session id is missing. In v1.0 a running ask row carries
|
|
269
|
+
// kimi_session_id=NULL (kimi-code assigns the id post-call), so the
|
|
270
|
+
// legacy "no session id → not found" check would mask the more
|
|
271
|
+
// accurate "already running" error.
|
|
272
|
+
const source = await reconcileHistoricalJobProvenance(store, exact);
|
|
273
|
+
ensureAskSessionIsNotRunning(source);
|
|
274
|
+
if (!source.kimi_session_id) {
|
|
275
|
+
throw new RuntimeError("ASK_RESUME_NOT_FOUND", `No ask job or session matched ${resumeTarget}.`, "ask.resume");
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
kimiSessionId: source.kimi_session_id,
|
|
279
|
+
reusedSession: true,
|
|
280
|
+
resumedFromJobId: source.job_id,
|
|
281
|
+
sourceJob: source,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (resume) {
|
|
285
|
+
const latest = store.findLatestJob({ repoId, commandType: "ask" });
|
|
286
|
+
if (!latest) {
|
|
287
|
+
throw new RuntimeError("ASK_RESUME_NOT_FOUND", "No prior ask session exists for this repository.", "ask.resume");
|
|
288
|
+
}
|
|
289
|
+
const source = await reconcileHistoricalJobProvenance(store, latest);
|
|
290
|
+
ensureAskSessionIsNotRunning(source);
|
|
291
|
+
if (!source.kimi_session_id) {
|
|
292
|
+
throw new RuntimeError("ASK_RESUME_NOT_FOUND", "No prior ask session exists for this repository.", "ask.resume");
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
kimiSessionId: source.kimi_session_id,
|
|
296
|
+
reusedSession: true,
|
|
297
|
+
resumedFromJobId: source.job_id,
|
|
298
|
+
sourceJob: source,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
302
|
+
}
|
|
303
|
+
function ensureAskSessionIsNotRunning(job) {
|
|
304
|
+
if (job.status === "running") {
|
|
305
|
+
throw new RuntimeError("ASK_ALREADY_RUNNING", `Ask session ${job.kimi_session_id ?? "<unknown>"} is already active under job ${job.job_id}.`, "ask.resume");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function buildAskPrompt(question, reusedSession) {
|
|
309
|
+
if (question?.trim()) {
|
|
310
|
+
return ASK_PROMPT_PREFIX + question.trim();
|
|
311
|
+
}
|
|
312
|
+
if (reusedSession) {
|
|
313
|
+
return [
|
|
314
|
+
"Continue the previous ask conversation in free-form prose.",
|
|
315
|
+
"Stay read-only.",
|
|
316
|
+
"Do not emit JSON unless the user explicitly asks for it.",
|
|
317
|
+
].join("\n");
|
|
318
|
+
}
|
|
319
|
+
throw new RuntimeError("INVALID_ARGS", "ask requires a question after the flags.", "ask.parse");
|
|
320
|
+
}
|
|
321
|
+
function shorten(text, max) {
|
|
322
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
323
|
+
if (normalized.length <= max) {
|
|
324
|
+
return normalized;
|
|
325
|
+
}
|
|
326
|
+
return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
327
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { RuntimeError } from "../errors.js";
|
|
3
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
4
|
+
import { markJobCancelled, sweepStaleJobs } from "../jobs.js";
|
|
5
|
+
import { JobStore, withJobStore } from "../job-store.js";
|
|
6
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
7
|
+
import { parseJobLookupArgs } from "../parsing.js";
|
|
8
|
+
import { getManagedCommandConfig } from "./registry.js";
|
|
9
|
+
export async function runCancel(argv, context) {
|
|
10
|
+
const parsed = parseJobLookupArgs(argv);
|
|
11
|
+
const paths = resolvePluginPaths(context.env);
|
|
12
|
+
await ensurePluginPaths(paths);
|
|
13
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
14
|
+
const store = new JobStore(paths);
|
|
15
|
+
try {
|
|
16
|
+
await sweepStaleJobs(store, paths);
|
|
17
|
+
const job = parsed.jobId
|
|
18
|
+
? store.getJob(parsed.jobId)
|
|
19
|
+
: store.findLatestJob({
|
|
20
|
+
repoId: repoIdentity.repoId,
|
|
21
|
+
commandType: parsed.type,
|
|
22
|
+
runningOnly: true,
|
|
23
|
+
});
|
|
24
|
+
if (!job) {
|
|
25
|
+
throw new RuntimeError("JOB_NOT_FOUND", "No running job was found to cancel.", "cancel.lookup");
|
|
26
|
+
}
|
|
27
|
+
if (job.status !== "running") {
|
|
28
|
+
return `${JSON.stringify({
|
|
29
|
+
job_id: job.job_id,
|
|
30
|
+
status: job.status,
|
|
31
|
+
message: "cancel is a no-op for terminal jobs.",
|
|
32
|
+
}, null, 2)}\n`;
|
|
33
|
+
}
|
|
34
|
+
if (job.pid === null && job.kimi_pid === null) {
|
|
35
|
+
throw new RuntimeError("CANCEL_NOT_SUPPORTED", `Job ${job.job_id} does not have a recorded process id to cancel.`, "cancel.runtime");
|
|
36
|
+
}
|
|
37
|
+
// review_gate runs in a Stop hook; SIGTERMing kimi_pid causes the wire
|
|
38
|
+
// process to exit with WIRE_PROCESS_EXITED, which the hook's catch path
|
|
39
|
+
// would persist as `failed`. Pre-mark the row as `cancelled` here so the
|
|
40
|
+
// user's intent is recorded canonically — the subsequent wire exit is a
|
|
41
|
+
// no-op against an already-terminal row (markFailed has a WHERE
|
|
42
|
+
// status='running' guard).
|
|
43
|
+
//
|
|
44
|
+
// Because the row is now terminal, waitForCancellation returns
|
|
45
|
+
// immediately on the status check and never escalates to SIGKILL. Run
|
|
46
|
+
// an unconditional SIGTERM→1s→SIGKILL sequence on the recorded pids
|
|
47
|
+
// here and return early so the lower-level wait/escalation path does
|
|
48
|
+
// not skip cleanup. (v0.2.4)
|
|
49
|
+
if (job.command_type === "review_gate") {
|
|
50
|
+
// Pre-mark before signaling so the row persists as `cancelled` rather
|
|
51
|
+
// than the `failed` the hook's catch path would write on WIRE_PROCESS_EXITED.
|
|
52
|
+
//
|
|
53
|
+
// If the pre-mark itself fails (disk I/O, SQLite contention), we must
|
|
54
|
+
// STILL signal the Kimi child — otherwise the user's cancel command
|
|
55
|
+
// produces an alive-but-marked-cancelled stuck state. Capture the
|
|
56
|
+
// pre-mark failure, run the SIGTERM→SIGKILL escalation unconditionally,
|
|
57
|
+
// and rethrow at the end so the user sees the underlying error.
|
|
58
|
+
let preMarkError;
|
|
59
|
+
try {
|
|
60
|
+
await withJobStore(paths, async (preMarkStore) => {
|
|
61
|
+
const reviewGateCancel = getManagedCommandConfig("review_gate").cancellation;
|
|
62
|
+
await markJobCancelled(preMarkStore, paths, job, reviewGateCancel.cancelledSummary, new RuntimeError(reviewGateCancel.errorCodes.cancelled, reviewGateCancel.cancelMessages.default, "cancel.runtime"));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
preMarkError = error;
|
|
67
|
+
}
|
|
68
|
+
signalJobProcesses(job, "SIGTERM");
|
|
69
|
+
await sleep(1_000);
|
|
70
|
+
if (hasLiveRecordedProcess(job)) {
|
|
71
|
+
signalJobProcesses(job, "SIGKILL");
|
|
72
|
+
}
|
|
73
|
+
if (preMarkError) {
|
|
74
|
+
throw preMarkError;
|
|
75
|
+
}
|
|
76
|
+
return `${JSON.stringify({
|
|
77
|
+
job_id: job.job_id,
|
|
78
|
+
status: "cancelled",
|
|
79
|
+
message: "review_gate cancelled; SIGTERM→SIGKILL escalation completed.",
|
|
80
|
+
}, null, 2)}\n`;
|
|
81
|
+
}
|
|
82
|
+
signalJobProcesses(job, "SIGTERM");
|
|
83
|
+
const cancelled = await waitForCancellation(paths, job.job_id);
|
|
84
|
+
if (cancelled) {
|
|
85
|
+
return `${JSON.stringify({
|
|
86
|
+
job_id: cancelled.job_id,
|
|
87
|
+
status: cancelled.status,
|
|
88
|
+
message: "Cancellation completed.",
|
|
89
|
+
}, null, 2)}\n`;
|
|
90
|
+
}
|
|
91
|
+
signalJobProcesses(job, "SIGKILL");
|
|
92
|
+
await withJobStore(paths, async (forcedStore) => {
|
|
93
|
+
const current = forcedStore.getJob(job.job_id);
|
|
94
|
+
if (current?.status === "running") {
|
|
95
|
+
await markJobCancelled(forcedStore, paths, current, "Job was force-cancelled after recorded process termination.", undefined, current.command_type === "ask" || current.command_type === "rescue" ? { phase: "cancelled" } : undefined);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return `${JSON.stringify({
|
|
99
|
+
job_id: job.job_id,
|
|
100
|
+
status: "cancelled",
|
|
101
|
+
message: "Cancellation escalated to worker termination.",
|
|
102
|
+
}, null, 2)}\n`;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
store.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function signalJobProcesses(job, signal) {
|
|
109
|
+
const pids = new Set();
|
|
110
|
+
if (job.pid !== null)
|
|
111
|
+
pids.add(job.pid);
|
|
112
|
+
if (job.kimi_pid !== null)
|
|
113
|
+
pids.add(job.kimi_pid);
|
|
114
|
+
for (const pid of pids) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pid, signal);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const code = error.code;
|
|
120
|
+
// ESRCH: process already gone. EPERM: pid was likely reused by an
|
|
121
|
+
// unrelated process we don't own — silently skip rather than surface a
|
|
122
|
+
// confusing "operation not permitted" to the user.
|
|
123
|
+
if (code !== "ESRCH" && code !== "EPERM") {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function sleep(ms) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
setTimeout(resolve, ms);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function waitForCancellation(paths, jobId) {
|
|
135
|
+
const deadline = Date.now() + 4_000;
|
|
136
|
+
while (Date.now() < deadline) {
|
|
137
|
+
const store = new JobStore(paths);
|
|
138
|
+
try {
|
|
139
|
+
const current = store.getJob(jobId);
|
|
140
|
+
if (current && current.status !== "running") {
|
|
141
|
+
return current;
|
|
142
|
+
}
|
|
143
|
+
if (current && !hasLiveRecordedProcess(current)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
store.close();
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
setTimeout(resolve, 200);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function hasLiveRecordedProcess(job) {
|
|
157
|
+
const pids = new Set();
|
|
158
|
+
if (job.pid !== null)
|
|
159
|
+
pids.add(job.pid);
|
|
160
|
+
if (job.kimi_pid !== null)
|
|
161
|
+
pids.add(job.kimi_pid);
|
|
162
|
+
for (const pid of pids) {
|
|
163
|
+
try {
|
|
164
|
+
process.kill(pid, 0);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const code = error.code;
|
|
169
|
+
// ESRCH: definitively dead. EPERM: pid likely reused by an
|
|
170
|
+
// unrelated process — treat as dead for our purposes (consistent
|
|
171
|
+
// with isPidAlive in jobs.ts and signalJobProcesses above).
|
|
172
|
+
if (code !== "ESRCH" && code !== "EPERM") {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reassemble prose final text from cli-client records.
|
|
4
|
+
*
|
|
5
|
+
* The v0.4 wire client returned a single `finalText` field as the
|
|
6
|
+
* canonical assistant prose. Stream-json instead emits a sequence of
|
|
7
|
+
* records, three of which can carry assistant prose:
|
|
8
|
+
*
|
|
9
|
+
* - `{role:"assistant", content:"..."}` — text token
|
|
10
|
+
* - `{role:"assistant", tool_calls:[...]}` — no text
|
|
11
|
+
* - `{role:"tool", tool_call_id:"...", content}` — tool output, ignore
|
|
12
|
+
*
|
|
13
|
+
* We concatenate every assistant `content` field in arrival order. The
|
|
14
|
+
* order is preserved by stream-json's NDJSON contract — kimi-code emits
|
|
15
|
+
* one record per line, parser preserves emission order, runtime
|
|
16
|
+
* pushes to `records[]` in the same order.
|
|
17
|
+
*
|
|
18
|
+
* Two correctness notes:
|
|
19
|
+
*
|
|
20
|
+
* 1. Empty assistant records (assistant tool_calls without prose)
|
|
21
|
+
* contribute zero characters but still appear in `records`. We
|
|
22
|
+
* skip them without inserting a separator, otherwise back-to-back
|
|
23
|
+
* tool calls would inject blank lines.
|
|
24
|
+
*
|
|
25
|
+
* 2. We do NOT trim individual content fragments. kimi-code emits
|
|
26
|
+
* tokens that may include leading/trailing whitespace as part of
|
|
27
|
+
* the model's intended formatting. Only the final concatenated
|
|
28
|
+
* string is whitespace-managed by the caller's render layer.
|
|
29
|
+
*/
|
|
30
|
+
export function reassembleProseFromRecords(records) {
|
|
31
|
+
let out = "";
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
if (record.role !== "assistant")
|
|
34
|
+
continue;
|
|
35
|
+
if (typeof record.content !== "string")
|
|
36
|
+
continue;
|
|
37
|
+
out += record.content;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Loud-fail warning when kimi finished a job but never announced a
|
|
43
|
+
* session id on stderr. We persist `kimi_session_id` as NULL in that
|
|
44
|
+
* case (the row is unresumable), and the user is silently surprised
|
|
45
|
+
* later when `-r` or `/kimi:replay` returns nothing.
|
|
46
|
+
*
|
|
47
|
+
* The right surface is human-readable stderr per the LLM-caller
|
|
48
|
+
* discipline invariant — load-bearing context still goes via the SQLite
|
|
49
|
+
* row (`kimi_session_id IS NULL`), but the user sees this warning
|
|
50
|
+
* inline immediately after the prompt completes so they know resume is
|
|
51
|
+
* unavailable for this job.
|
|
52
|
+
*
|
|
53
|
+
* Called at the end of every command that captures sessionId (review,
|
|
54
|
+
* challenge, ask, rescue). Review-gate intentionally skips: it's a Stop
|
|
55
|
+
* hook with no resume semantics and we don't want a runtime warning to
|
|
56
|
+
* leak into Claude Code's transcript on every assistant turn.
|
|
57
|
+
*/
|
|
58
|
+
export function warnIfSessionIdMissing(result, commandLabel, jobId, stderr) {
|
|
59
|
+
if (result.sessionId !== undefined && result.sessionId.length > 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
stderr.write(`[${commandLabel}] kimi did not announce a session id for job ${jobId}. ` +
|
|
63
|
+
`Resume and replay will not work for this job. ` +
|
|
64
|
+
`If this happens repeatedly, check kimi-code's stderr format (the " To resume this session" line) and report upstream.\n`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Translate a cli-client `CliClientResult` into the kind of error
|
|
68
|
+
* surface ask/review/rescue commands have always thrown when the
|
|
69
|
+
* underlying transport reported a failure.
|
|
70
|
+
*
|
|
71
|
+
* Order matters: a non-zero exit code may carry no records (process
|
|
72
|
+
* died early) or may carry partial records (model started but kimi
|
|
73
|
+
* crashed). Either way, we surface the exit-code failure first because
|
|
74
|
+
* trying to render a half-stream as `finalText` produces confusing
|
|
75
|
+
* downstream errors.
|
|
76
|
+
*
|
|
77
|
+
* `stage` is used as both the RuntimeError stage and a prefix-free
|
|
78
|
+
* label for code namespacing — callers should pass values like
|
|
79
|
+
* "ask.runtime" / "review.runtime" / "review_gate.runtime".
|
|
80
|
+
*/
|
|
81
|
+
export function assertCliResultSuccess(result, stage) {
|
|
82
|
+
if (result.aborted) {
|
|
83
|
+
// Aborted-by-our-signal is a cancellation, not a failure. The
|
|
84
|
+
// caller's outer handler checks `handlers.cancelling` and wraps
|
|
85
|
+
// into the canonical *_CANCELLED RuntimeError; we just need to
|
|
86
|
+
// bail without claiming the records are a complete response.
|
|
87
|
+
throw new RuntimeError("CLI_ABORTED", "kimi subprocess was cancelled before completing the response", stage, {
|
|
88
|
+
details: {
|
|
89
|
+
exit_code: result.exitCode,
|
|
90
|
+
signal: result.signal,
|
|
91
|
+
record_count: result.records.length,
|
|
92
|
+
stderr_tail_len: result.stderrTail.length,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (result.exitCode !== 0) {
|
|
97
|
+
const tail = result.stderrTail.trim();
|
|
98
|
+
throw new RuntimeError("CLI_NONZERO_EXIT", [
|
|
99
|
+
`kimi subprocess exited with code ${result.exitCode}`,
|
|
100
|
+
result.signal ? ` (signal ${result.signal})` : "",
|
|
101
|
+
tail.length > 0 ? `:\n${tail}` : ".",
|
|
102
|
+
].join(""), stage, {
|
|
103
|
+
details: {
|
|
104
|
+
exit_code: result.exitCode,
|
|
105
|
+
signal: result.signal,
|
|
106
|
+
record_count: result.records.length,
|
|
107
|
+
malformed_count: result.malformed.length,
|
|
108
|
+
stderr_tail_len: result.stderrTail.length,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|