kimi-plugin-cc 2.4.5 → 2.5.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +42 -26
- package/dist/dsh/companion.js +174 -0
- package/dist/dsh/index.js +156 -0
- package/dist/hooks/install-paths.js +2 -2
- package/dist/version.js +1 -1
- package/docs/models.md +5 -1
- package/docs/registry-releases.md +20 -17
- package/docs/safety.md +22 -9
- package/docs/upstream-2.4.1-certification.md +6 -0
- package/package.json +40 -6
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
- package/plugins/kimi-codex/dist/dsh/index.js +156 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/LICENSE +201 -0
- package/plugins/kimi-dsh/README.md +80 -0
- package/plugins/kimi-dsh/cordis.patch.yml +45 -0
- package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
- package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-dsh/dist/cli-client.js +876 -0
- package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
- package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
- package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
- package/plugins/kimi-dsh/dist/commands/models.js +140 -0
- package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
- package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
- package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
- package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
- package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
- package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
- package/plugins/kimi-dsh/dist/commands/result.js +66 -0
- package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
- package/plugins/kimi-dsh/dist/commands/review.js +220 -0
- package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
- package/plugins/kimi-dsh/dist/commands/status.js +27 -0
- package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
- package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
- package/plugins/kimi-dsh/dist/companion.js +97 -0
- package/plugins/kimi-dsh/dist/config.js +22 -0
- package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
- package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
- package/plugins/kimi-dsh/dist/errors.js +40 -0
- package/plugins/kimi-dsh/dist/git.js +224 -0
- package/plugins/kimi-dsh/dist/help.js +43 -0
- package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
- package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
- package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
- package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
- package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
- package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-dsh/dist/job-id.js +12 -0
- package/plugins/kimi-dsh/dist/job-store.js +659 -0
- package/plugins/kimi-dsh/dist/jobs.js +217 -0
- package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
- package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
- package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
- package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
- package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
- package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
- package/plugins/kimi-dsh/dist/logging.js +22 -0
- package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
- package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
- package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
- package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
- package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
- package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
- package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
- package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
- package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
- package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
- package/plugins/kimi-dsh/dist/model-command.js +32 -0
- package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-dsh/dist/package.json +1 -0
- package/plugins/kimi-dsh/dist/parsing.js +620 -0
- package/plugins/kimi-dsh/dist/paths.js +27 -0
- package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
- package/plugins/kimi-dsh/dist/process-tree.js +479 -0
- package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
- package/plugins/kimi-dsh/dist/render.js +177 -0
- package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
- package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
- package/plugins/kimi-dsh/dist/session-title.js +293 -0
- package/plugins/kimi-dsh/dist/stream-json.js +346 -0
- package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
- package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-dsh/dist/types.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
- package/plugins/kimi-dsh/dist/version.js +7 -0
- package/plugins/kimi-dsh/package.json +48 -0
- package/plugins/kimi-dsh/scripts/companion.sh +37 -0
- package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { companionHelp } from "../help.js";
|
|
2
|
+
import { allocateJobId } from "../job-id.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
|
+
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
6
|
+
import { assertPersistedResumeLineage, executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, reconcileHistoricalJobProvenance, } from "../kimi-engine.js";
|
|
7
|
+
import { getManagedCommandConfig } from "./registry.js";
|
|
8
|
+
import { RuntimeError } from "../errors.js";
|
|
9
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
10
|
+
import { digestPrompt, markJobCancelled, markJobFailed, sweepStaleJobs } from "../jobs.js";
|
|
11
|
+
import { JobStore } from "../job-store.js";
|
|
12
|
+
import { classifyManagedCommandFailure } from "../kimi-errors.js";
|
|
13
|
+
import { KIMI_RESCUE_PROMPT_TIMEOUT_MS } from "../kimi-timeouts.js";
|
|
14
|
+
import { writeInvocationLogHeader } from "../logging.js";
|
|
15
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
16
|
+
import { parseRescueArgs } from "../parsing.js";
|
|
17
|
+
import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
|
|
18
|
+
import { startBackgroundJob } from "../background-spawn.js";
|
|
19
|
+
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
20
|
+
import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
|
|
21
|
+
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
22
|
+
export { describeMissingResult } from "../background-spawn.js";
|
|
23
|
+
// "Continue working on the bug" verbs that auto-attach to the latest
|
|
24
|
+
// rescue session. Documented UX trap: a prompt matching this pattern
|
|
25
|
+
// will resume the most recent rescue session regardless of whether
|
|
26
|
+
// that session was about the same bug. Users should pass an explicit
|
|
27
|
+
// `--resume <job-id>` or `--fresh` when the topic switches.
|
|
28
|
+
const AUTO_RESUME_PATTERN = /\b(continue|resume|keep going|keep working|apply the top fix|dig deeper)\b/i;
|
|
29
|
+
const RESCUE_SUMMARY_MAX = 120;
|
|
30
|
+
const RESCUE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
|
|
31
|
+
const RESCUE_CONTINUE_TASK = "Continue the previous rescue task using the latest repository state.";
|
|
32
|
+
const RESCUE_PROMPT_PREFIX = [
|
|
33
|
+
"Follow repository instructions and inspect the current state before editing.",
|
|
34
|
+
"Implement the smallest complete change that satisfies the task instead of stopping at a plan.",
|
|
35
|
+
"Do not mutate git state.",
|
|
36
|
+
"Run proportionate relevant checks, and never claim unrun checks passed.",
|
|
37
|
+
"Do not narrate progress.",
|
|
38
|
+
"Finish with a self-contained report of changed files, verification commands and results, and unfinished work or blockers.",
|
|
39
|
+
"",
|
|
40
|
+
"Rescue task:",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
export async function runRescue(argv, context) {
|
|
44
|
+
const parsed = parseRescueArgs(argv);
|
|
45
|
+
if (parsed.help)
|
|
46
|
+
return companionHelp(["task", "rescue", ...argv]);
|
|
47
|
+
const paths = resolvePluginPaths(context.env);
|
|
48
|
+
await ensurePluginPaths(paths);
|
|
49
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
50
|
+
const rescueConfig = getManagedCommandConfig("rescue");
|
|
51
|
+
const store = new JobStore(paths);
|
|
52
|
+
try {
|
|
53
|
+
await sweepStaleJobs(store, paths);
|
|
54
|
+
const sessionResolution = await resolveRescueSession(store, repoIdentity.repoId, parsed.prompt, parsed.fresh, parsed.resume, parsed.resumeTarget);
|
|
55
|
+
const prompt = buildRescuePrompt(parsed.prompt, sessionResolution.reusedSession);
|
|
56
|
+
const executionPlan = await prepareKimiExecutionPlan({
|
|
57
|
+
operationKind: "rescue",
|
|
58
|
+
cwd: context.cwd,
|
|
59
|
+
env: context.env,
|
|
60
|
+
resumedFromJobId: sessionResolution.resumedFromJobId,
|
|
61
|
+
resumeSessionId: sessionResolution.kimiSessionId ?? undefined,
|
|
62
|
+
resumeSource: sessionResolution.sourceJob,
|
|
63
|
+
});
|
|
64
|
+
const jobId = allocateJobId(context);
|
|
65
|
+
const logPath = path.join(paths.logsDir, `rescue-${jobId}.jsonl`);
|
|
66
|
+
const job = store.createJob({
|
|
67
|
+
job_id: jobId,
|
|
68
|
+
repo_id: repoIdentity.repoId,
|
|
69
|
+
command_type: "rescue",
|
|
70
|
+
cwd: context.cwd,
|
|
71
|
+
model: parsed.model ?? null,
|
|
72
|
+
thinking: null,
|
|
73
|
+
thinking_effort: parsed.thinkingEffort ?? null,
|
|
74
|
+
background: parsed.background,
|
|
75
|
+
budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "rescue" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
|
|
76
|
+
budget_source: parsed.budgetMs !== undefined ? "explicit"
|
|
77
|
+
: sessionResolution.sourceJob?.operation_kind === "rescue" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
|
|
78
|
+
pid: parsed.background ? null : process.pid,
|
|
79
|
+
kimi_pid: null,
|
|
80
|
+
status: "running",
|
|
81
|
+
kimi_session_id: sessionResolution.kimiSessionId,
|
|
82
|
+
kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
|
|
83
|
+
...persistedExecutionPlanFields(executionPlan),
|
|
84
|
+
agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
|
|
85
|
+
prompt_digest: digestPrompt(prompt),
|
|
86
|
+
summary: shorten(stripRescuePromptPrefix(prompt), RESCUE_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: "rescue",
|
|
95
|
+
kimiSessionId: sessionResolution.kimiSessionId ?? "(pending)",
|
|
96
|
+
cwd: context.cwd,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const classified = new RuntimeError("RESCUE_LOG_HEADER_FAILED", `Failed to write rescue invocation log header: ${error.message ?? String(error)}`, "rescue.log-header", error instanceof Error ? { cause: error } : undefined);
|
|
101
|
+
await markJobFailed(store, paths, job, classified, rescueConfig.cancellation.failedSummary, { phase: "failed" });
|
|
102
|
+
throw classified;
|
|
103
|
+
}
|
|
104
|
+
warnInheritedBudget(job, context.stderr);
|
|
105
|
+
if (parsed.background) {
|
|
106
|
+
return startBackgroundJob(job, prompt, context, paths, {
|
|
107
|
+
workerKind: "rescue",
|
|
108
|
+
wait: parsed.wait,
|
|
109
|
+
promptEnvVar: "KIMI_PLUGIN_CC_RESCUE_PROMPT_B64",
|
|
110
|
+
reusedSessionEnvVar: "KIMI_PLUGIN_CC_RESCUE_REUSED_SESSION",
|
|
111
|
+
reusedSession: sessionResolution.reusedSession,
|
|
112
|
+
failedSummary: rescueConfig.cancellation.failedSummary,
|
|
113
|
+
missingResultErrorCode: "RESCUE_RESULT_MISSING",
|
|
114
|
+
spawnFailedErrorCode: "RESCUE_WORKER_SPAWN_FAILED",
|
|
115
|
+
earlyExitErrorCode: "RESCUE_WORKER_EXITED_EARLY",
|
|
116
|
+
nodeBinInvalidErrorCode: "RESCUE_NODE_BIN_INVALID",
|
|
117
|
+
waitStage: "rescue.wait",
|
|
118
|
+
spawnStage: "rescue.worker.spawn",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const completed = await executeRescueJob(job.job_id, prompt, context);
|
|
122
|
+
if (!completed.final_output_path) {
|
|
123
|
+
throw new RuntimeError("RESCUE_RESULT_MISSING", "Rescue finished without a rendered result.", "rescue.result");
|
|
124
|
+
}
|
|
125
|
+
return readArtifact(completed.final_output_path);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
store.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function executeRescueJob(jobId, prompt, context, options) {
|
|
132
|
+
const paths = resolvePluginPaths(context.env);
|
|
133
|
+
await ensurePluginPaths(paths);
|
|
134
|
+
const store = new JobStore(paths);
|
|
135
|
+
const job = store.getJob(jobId);
|
|
136
|
+
if (!job) {
|
|
137
|
+
store.close();
|
|
138
|
+
throw new RuntimeError("JOB_NOT_FOUND", `Rescue job ${jobId} was not found.`, "rescue.worker");
|
|
139
|
+
}
|
|
140
|
+
const rescueConfig = getManagedCommandConfig("rescue");
|
|
141
|
+
const cancel = rescueConfig.cancellation;
|
|
142
|
+
// Rescue writes directly to the workspace. Without the PreToolUse hook,
|
|
143
|
+
// kimi-code's `-p` mode auto-approves every Bash/Write/Edit, including
|
|
144
|
+
// destructive ones. Refuse before the worker can spawn Kimi.
|
|
145
|
+
//
|
|
146
|
+
// The hook check happens BEFORE `createCliCancellationHandlers()` so
|
|
147
|
+
// an early return doesn't leak SIGTERM/SIGINT listeners. Tests /
|
|
148
|
+
// setup probes / intentional bypass: KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1.
|
|
149
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
150
|
+
const installStatus = await verifyHookInstalled(context.env);
|
|
151
|
+
if (!installStatus.installed) {
|
|
152
|
+
maybeWarnHookMissing(installStatus, "rescue", context.stderr);
|
|
153
|
+
const classified = new RuntimeError("RESCUE_HOOK_NOT_INSTALLED", [
|
|
154
|
+
"rescue refuses to run without the kimi-plugin-cc PreToolUse hook.",
|
|
155
|
+
`Hook check failed: ${installStatus.reason ?? "unknown"}.`,
|
|
156
|
+
"Repair by running Claude Code /kimi:setup or Codex $kimi-setup, then retry.",
|
|
157
|
+
"KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1 is only for deliberate tests or diagnostics.",
|
|
158
|
+
hookRefusalRetryProtocol(context.env),
|
|
159
|
+
].join(" "), "rescue.hook-check", { details: hookRefusalDetails(installStatus) });
|
|
160
|
+
try {
|
|
161
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
store.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const handlers = createCliCancellationHandlers();
|
|
169
|
+
try {
|
|
170
|
+
const executionPlan = executionPlanFromPersisted(job);
|
|
171
|
+
// Re-validate resume lineage from the store before spawning: a detached
|
|
172
|
+
// worker (or a forged queued row) reloads the persisted plan and would
|
|
173
|
+
// otherwise hand kimi_session_id to the spawn without the dispatch-time
|
|
174
|
+
// lineage check. Fresh sessions (no resumedFromJobId) are exempt.
|
|
175
|
+
if (job.kimi_session_id !== null) {
|
|
176
|
+
await assertPersistedResumeLineage(store, executionPlan, job.kimi_session_id, "rescue");
|
|
177
|
+
}
|
|
178
|
+
if (options?.workerPid) {
|
|
179
|
+
store.updateRunningJob(job.job_id, { pid: options.workerPid, phase: "worker-running" });
|
|
180
|
+
}
|
|
181
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
182
|
+
const result = await runCliPromptWithBudget({
|
|
183
|
+
cwd: job.cwd,
|
|
184
|
+
env: context.env,
|
|
185
|
+
command: executionPlan.command,
|
|
186
|
+
prefixArgs: [...executionPlan.prefixArgs],
|
|
187
|
+
executionPlan,
|
|
188
|
+
prompt,
|
|
189
|
+
commandLabel: "rescue",
|
|
190
|
+
trustedWorkspaceRoot: job.cwd,
|
|
191
|
+
model: job.model ?? undefined,
|
|
192
|
+
thinkingEffort: job.thinking_effort ?? undefined,
|
|
193
|
+
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
194
|
+
logPath: job.stream_log_path,
|
|
195
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
196
|
+
signal: handlers.signal,
|
|
197
|
+
}, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
|
|
198
|
+
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
199
|
+
if (handlers.cancelling) {
|
|
200
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
|
|
201
|
+
}
|
|
202
|
+
assertCliResultSuccess(result, "rescue.runtime");
|
|
203
|
+
warnHookEscapes(result, "rescue", context.stderr);
|
|
204
|
+
if (result.sessionId !== undefined &&
|
|
205
|
+
result.sessionId.length > 0) {
|
|
206
|
+
// Never persist an empty ID because NULL represents a missing session ID.
|
|
207
|
+
store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
|
|
208
|
+
}
|
|
209
|
+
await syncKimiSessionTitle({
|
|
210
|
+
promptText: prompt,
|
|
211
|
+
env: context.env,
|
|
212
|
+
cwd: job.cwd,
|
|
213
|
+
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
214
|
+
title: buildKimiSessionTitle("rescue", stripRescuePromptPrefix(prompt)),
|
|
215
|
+
stderr: context.stderr,
|
|
216
|
+
});
|
|
217
|
+
if (job.kimi_session_id === null) {
|
|
218
|
+
warnIfSessionIdMissing(result, "rescue", job.job_id, context.stderr);
|
|
219
|
+
}
|
|
220
|
+
const finalText = reassembleProseFromRecords(result.records);
|
|
221
|
+
const rendered = renderManagedJobOutput(job, finalText);
|
|
222
|
+
let artifactPath;
|
|
223
|
+
try {
|
|
224
|
+
if (context.env.KIMI_PLUGIN_CC_TEST_FAIL_WRITE_ARTIFACT === "1") {
|
|
225
|
+
throw new Error("Simulated artifact write failure (test seam).");
|
|
226
|
+
}
|
|
227
|
+
artifactPath = await writeArtifact(paths, job, rendered.rendered);
|
|
228
|
+
}
|
|
229
|
+
catch (writeError) {
|
|
230
|
+
// LLM-caller discipline: writing the raw model output to stderr
|
|
231
|
+
// for a wrapper LLM-caller would corrupt its prose stream. Keep
|
|
232
|
+
// the raw text in the RuntimeError details — surfaces via
|
|
233
|
+
// /kimi:status / /kimi:result --json — and emit only a short
|
|
234
|
+
// human-facing line on stderr.
|
|
235
|
+
context.stderr.write(`[kimi-plugin-cc] rescue artifact write failed for job ${job.job_id}; raw output preserved in error details.\n`);
|
|
236
|
+
const classified = new RuntimeError("RESCUE_ARTIFACT_WRITE_FAILED", `Failed to write rescue artifact: ${writeError.message ?? String(writeError)}`, "rescue.artifact", {
|
|
237
|
+
...(writeError instanceof Error ? { cause: writeError } : {}),
|
|
238
|
+
details: { rawOutput: finalText },
|
|
239
|
+
});
|
|
240
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
241
|
+
}
|
|
242
|
+
if (handlers.cancelling) {
|
|
243
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterArtifact, "rescue.runtime");
|
|
244
|
+
}
|
|
245
|
+
return (store.markCompleted(job.job_id, {
|
|
246
|
+
summary: rendered.summary,
|
|
247
|
+
phase: "done",
|
|
248
|
+
final_output_path: artifactPath,
|
|
249
|
+
error: null,
|
|
250
|
+
}) ?? job);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
|
|
254
|
+
preserveStage: true,
|
|
255
|
+
background: job.background,
|
|
256
|
+
budgetMs: job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS,
|
|
257
|
+
});
|
|
258
|
+
const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
|
|
259
|
+
if (handlers.cancelling) {
|
|
260
|
+
const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
|
|
261
|
+
return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
|
|
262
|
+
}
|
|
263
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
handlers.dispose();
|
|
267
|
+
store.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function buildRescuePrompt(prompt, reusedSession) {
|
|
271
|
+
if (prompt?.trim()) {
|
|
272
|
+
return RESCUE_PROMPT_PREFIX + prompt.trim();
|
|
273
|
+
}
|
|
274
|
+
if (reusedSession) {
|
|
275
|
+
return RESCUE_PROMPT_PREFIX + RESCUE_CONTINUE_TASK;
|
|
276
|
+
}
|
|
277
|
+
throw new RuntimeError("INVALID_ARGS", "rescue requires a task description.", "rescue.parse");
|
|
278
|
+
}
|
|
279
|
+
function stripRescuePromptPrefix(prompt) {
|
|
280
|
+
return prompt.startsWith(RESCUE_PROMPT_PREFIX) ? prompt.slice(RESCUE_PROMPT_PREFIX.length) : prompt;
|
|
281
|
+
}
|
|
282
|
+
async function resolveRescueSession(store, repoId, prompt, fresh, resume, resumeTarget) {
|
|
283
|
+
if (fresh) {
|
|
284
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
285
|
+
}
|
|
286
|
+
if (resumeTarget) {
|
|
287
|
+
const byJob = store.getJob(resumeTarget);
|
|
288
|
+
const scoped = byJob?.repo_id === repoId && byJob.command_type === "rescue" ? byJob : null;
|
|
289
|
+
const exact = scoped ?? store.findRescueJobBySession(repoId, resumeTarget);
|
|
290
|
+
if (!exact) {
|
|
291
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", `No rescue job or session matched ${resumeTarget}.`, "rescue.resume");
|
|
292
|
+
}
|
|
293
|
+
const source = await reconcileHistoricalJobProvenance(store, exact);
|
|
294
|
+
ensureSessionIsNotRunning(source);
|
|
295
|
+
if (!source.kimi_session_id) {
|
|
296
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", `No rescue job or session matched ${resumeTarget}.`, "rescue.resume");
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
kimiSessionId: source.kimi_session_id,
|
|
300
|
+
reusedSession: true,
|
|
301
|
+
resumedFromJobId: source.job_id,
|
|
302
|
+
sourceJob: source,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (resume) {
|
|
306
|
+
const latest = store.findLatestJob({ repoId, commandType: "rescue" });
|
|
307
|
+
if (!latest) {
|
|
308
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", "No prior rescue session exists for this repository.", "rescue.resume");
|
|
309
|
+
}
|
|
310
|
+
const source = await reconcileHistoricalJobProvenance(store, latest);
|
|
311
|
+
ensureSessionIsNotRunning(source);
|
|
312
|
+
if (!source.kimi_session_id) {
|
|
313
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", "No prior rescue session exists for this repository.", "rescue.resume");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
kimiSessionId: source.kimi_session_id,
|
|
317
|
+
reusedSession: true,
|
|
318
|
+
resumedFromJobId: source.job_id,
|
|
319
|
+
sourceJob: source,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (prompt && AUTO_RESUME_PATTERN.test(prompt)) {
|
|
323
|
+
const latest = store.findLatestJob({ repoId, commandType: "rescue" });
|
|
324
|
+
if (latest?.kimi_session_id && latest.status !== "running") {
|
|
325
|
+
const source = await reconcileHistoricalJobProvenance(store, latest);
|
|
326
|
+
return {
|
|
327
|
+
kimiSessionId: source.kimi_session_id,
|
|
328
|
+
reusedSession: true,
|
|
329
|
+
resumedFromJobId: source.job_id,
|
|
330
|
+
sourceJob: source,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
335
|
+
}
|
|
336
|
+
function ensureSessionIsNotRunning(job) {
|
|
337
|
+
if (job.status === "running") {
|
|
338
|
+
throw new RuntimeError("RESCUE_ALREADY_RUNNING", `Rescue session ${job.kimi_session_id ?? "<unknown>"} is already active under job ${job.job_id}.`, "rescue.resume");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function shorten(text, max) {
|
|
342
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
343
|
+
if (normalized.length <= max) {
|
|
344
|
+
return normalized;
|
|
345
|
+
}
|
|
346
|
+
return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
347
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
3
|
+
import { sweepStaleJobs } from "../jobs.js";
|
|
4
|
+
import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
|
|
5
|
+
import { renderBudgetSource, withJobStore, parseHookEscapes } from "../job-store.js";
|
|
6
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
7
|
+
import { parseJobLookupArgs } from "../parsing.js";
|
|
8
|
+
import { readArtifact, renderTerminalJobArtifact } from "../render.js";
|
|
9
|
+
export async function runResult(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
|
+
return withJobStore(paths, async (store) => {
|
|
15
|
+
await sweepStaleJobs(store, paths);
|
|
16
|
+
const found = parsed.jobId
|
|
17
|
+
? store.getJob(parsed.jobId)
|
|
18
|
+
: store.findLatestJob({
|
|
19
|
+
repoId: repoIdentity.repoId,
|
|
20
|
+
commandType: parsed.type,
|
|
21
|
+
terminalOnly: true,
|
|
22
|
+
});
|
|
23
|
+
if (!found) {
|
|
24
|
+
throw new RuntimeError("JOB_NOT_FOUND", "No matching terminal job was found for result.", "result.lookup");
|
|
25
|
+
}
|
|
26
|
+
const job = await reconcileHistoricalJobProvenance(store, found);
|
|
27
|
+
if (job.status === "running") {
|
|
28
|
+
// `result --json` preserves the existing result contract: callers only
|
|
29
|
+
// receive terminal artifacts, never a partial/running envelope.
|
|
30
|
+
throw new RuntimeError("JOB_NOT_TERMINAL", `Job ${job.job_id} is still running.`, "result.lookup");
|
|
31
|
+
}
|
|
32
|
+
if (!job.final_output_path) {
|
|
33
|
+
const fallbackBody = `${renderTerminalJobArtifact(job)}\n`;
|
|
34
|
+
return parsed.json ? renderResultEnvelope(job, null, fallbackBody) : fallbackBody;
|
|
35
|
+
}
|
|
36
|
+
const body = await readArtifact(job.final_output_path);
|
|
37
|
+
return parsed.json ? renderResultEnvelope(job, job.final_output_path, body) : body;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function renderResultEnvelope(job, artifactPath, body) {
|
|
41
|
+
return `${JSON.stringify({
|
|
42
|
+
job_id: job.job_id,
|
|
43
|
+
kind: job.command_type,
|
|
44
|
+
status: job.status,
|
|
45
|
+
budget_ms: job.budget_ms,
|
|
46
|
+
budget_source: renderBudgetSource(job),
|
|
47
|
+
thinking_effort_requested: job.thinking_effort ?? null,
|
|
48
|
+
hook_escapes: parseHookEscapes(job.hook_escapes),
|
|
49
|
+
kimi_session_id_source: job.kimi_session_id_source,
|
|
50
|
+
kimi_session_id: job.kimi_session_id,
|
|
51
|
+
// True when a budget timeout preserved assistant prose in the artifact.
|
|
52
|
+
partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
|
|
53
|
+
summary: job.summary,
|
|
54
|
+
error: job.error,
|
|
55
|
+
artifact_path: artifactPath,
|
|
56
|
+
body,
|
|
57
|
+
created_at: job.created_at,
|
|
58
|
+
completed_at: job.updated_at,
|
|
59
|
+
operation_kind: job.operation_kind,
|
|
60
|
+
intended_engine: job.intended_engine,
|
|
61
|
+
observed_engine: job.observed_engine,
|
|
62
|
+
kimi_version: job.kimi_version,
|
|
63
|
+
system_version: job.system_version,
|
|
64
|
+
resumed_from_job_id: job.resumed_from_job_id,
|
|
65
|
+
}, null, 2)}\n`;
|
|
66
|
+
}
|