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,159 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { RuntimeError } from "../errors.js";
|
|
6
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
7
|
+
import { digestPrompt } from "../jobs.js";
|
|
8
|
+
import { resolvePluginPaths } from "../paths.js";
|
|
9
|
+
import { syncKimiSessionTitle } from "../session-title.js";
|
|
10
|
+
// Repair is deliberately finite and local. Stream logs may be very large, but
|
|
11
|
+
// the spawn record follows the invocation header: read only a bounded prefix.
|
|
12
|
+
const MAX_JOBS = 1000;
|
|
13
|
+
const MAX_SPAWN_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
export async function runRepairSessions(argv, context) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (const arg of argv) {
|
|
17
|
+
if ((arg !== "--apply" && arg !== "--all") || seen.has(arg)) {
|
|
18
|
+
throw new RuntimeError("INVALID_ARGS", "Usage: repair-sessions [--apply] [--all]", "repair-sessions.args");
|
|
19
|
+
}
|
|
20
|
+
seen.add(arg);
|
|
21
|
+
}
|
|
22
|
+
const apply = seen.has("--apply");
|
|
23
|
+
const all = seen.has("--all");
|
|
24
|
+
const paths = resolvePluginPaths(context.env);
|
|
25
|
+
const repoId = all ? undefined : (await resolveRepoIdentity(context.cwd)).repoId;
|
|
26
|
+
const jobs = await readRepairJobs(paths.stateDbPath, repoId);
|
|
27
|
+
const sessions = new Set();
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const job of jobs) {
|
|
30
|
+
if (sessions.has(job.kimi_session_id))
|
|
31
|
+
continue;
|
|
32
|
+
sessions.add(job.kimi_session_id);
|
|
33
|
+
const promptText = await readVerifiedPrompt(job, paths.logsDir, paths.worktreesDir);
|
|
34
|
+
const status = promptText === undefined
|
|
35
|
+
? "unavailable-verified-prompt"
|
|
36
|
+
: await syncKimiSessionTitle({
|
|
37
|
+
env: context.env,
|
|
38
|
+
cwd: job.cwd,
|
|
39
|
+
sessionId: job.kimi_session_id,
|
|
40
|
+
title: "",
|
|
41
|
+
promptText,
|
|
42
|
+
dryRun: !apply,
|
|
43
|
+
preserveTitle: true,
|
|
44
|
+
requireNativeV2: true,
|
|
45
|
+
stderr: context.stderr,
|
|
46
|
+
});
|
|
47
|
+
results.push({ job_id: job.job_id, session_id: job.kimi_session_id, status });
|
|
48
|
+
}
|
|
49
|
+
const counts = {};
|
|
50
|
+
for (const { status } of results)
|
|
51
|
+
counts[status] = (counts[status] ?? 0) + 1;
|
|
52
|
+
return `${JSON.stringify({ mode: apply ? "apply" : "dry-run", scope: all ? "host" : "repository", sessions: results.length, counts, results }, null, 2)}\n`;
|
|
53
|
+
}
|
|
54
|
+
async function readRepairJobs(filename, repoId) {
|
|
55
|
+
try {
|
|
56
|
+
const stats = await lstat(filename);
|
|
57
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
58
|
+
throw new RuntimeError("SESSION_REPAIR_UNSAFE_STORE", "Repair requires a regular, non-symlink job database.", "repair-sessions.store");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === "ENOENT")
|
|
63
|
+
return [];
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const require = createRequire(import.meta.url);
|
|
67
|
+
// Bind constructors before `new`: compiler emission must not turn the
|
|
68
|
+
// module-loader call into the constructor (new require(...).Database).
|
|
69
|
+
const db = (() => {
|
|
70
|
+
if (typeof Bun !== "undefined") {
|
|
71
|
+
const { Database } = require("bun:sqlite");
|
|
72
|
+
return new Database(filename, { readonly: true });
|
|
73
|
+
}
|
|
74
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
75
|
+
return new DatabaseSync(filename, { readOnly: true });
|
|
76
|
+
})();
|
|
77
|
+
try {
|
|
78
|
+
// Never construct JobStore here: it performs migrations and stale-row
|
|
79
|
+
// reconciliation. Both preview and apply keep plugin job data read-only.
|
|
80
|
+
const columns = db.prepare("PRAGMA table_info(jobs)").all();
|
|
81
|
+
const names = new Set(columns.map((column) => column.name));
|
|
82
|
+
const required = ["job_id", "repo_id", "command_type", "operation_kind", "status", "kimi_session_id", "cwd", "kimi_version", "intended_engine", "observed_engine", "stream_log_path", "prompt_digest", "updated_at"];
|
|
83
|
+
if (required.some((name) => !names.has(name)))
|
|
84
|
+
return [];
|
|
85
|
+
const rows = db.prepare(`
|
|
86
|
+
SELECT j.job_id, j.repo_id, j.operation_kind, j.kimi_session_id, j.cwd, j.kimi_version, j.stream_log_path, j.prompt_digest
|
|
87
|
+
FROM jobs j
|
|
88
|
+
WHERE j.status = 'completed' AND j.intended_engine = 'native-v2' AND j.observed_engine = 'native-v2'
|
|
89
|
+
AND j.command_type != 'review_gate'
|
|
90
|
+
AND j.operation_kind IN ('ask', 'review', 'challenge', 'rescue', 'pursue', 'swarm', 'swarm-write')
|
|
91
|
+
AND j.kimi_session_id IS NOT NULL AND length(trim(j.kimi_session_id)) > 0
|
|
92
|
+
AND j.kimi_version IS NOT NULL
|
|
93
|
+
${repoId === undefined ? "" : "AND j.repo_id = ?"}
|
|
94
|
+
AND NOT EXISTS (SELECT 1 FROM jobs active WHERE active.kimi_session_id = j.kimi_session_id AND active.status = 'running')
|
|
95
|
+
ORDER BY j.updated_at DESC, j.job_id DESC LIMIT ${MAX_JOBS + 1}
|
|
96
|
+
`).all(...(repoId === undefined ? [] : [repoId]));
|
|
97
|
+
if (rows.length > MAX_JOBS) {
|
|
98
|
+
throw new RuntimeError("SESSION_REPAIR_LIMIT", `More than ${MAX_JOBS} eligible jobs; narrow the repair to one repository.`, "repair-sessions.store");
|
|
99
|
+
}
|
|
100
|
+
return rows;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function readVerifiedPrompt(job, logsDir, worktreesDir) {
|
|
107
|
+
// All plugin stream logs are direct children. Refuse nested/escaped paths,
|
|
108
|
+
// symlinks and special files; opening nonblocking avoids FIFO hangs.
|
|
109
|
+
if (!path.isAbsolute(job.stream_log_path) || path.dirname(job.stream_log_path) !== logsDir)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
const root = await lstat(logsDir);
|
|
113
|
+
if (!root.isDirectory() || root.isSymbolicLink())
|
|
114
|
+
return undefined;
|
|
115
|
+
const file = await open(job.stream_log_path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
116
|
+
try {
|
|
117
|
+
const stats = await file.stat();
|
|
118
|
+
if (!stats.isFile())
|
|
119
|
+
return undefined;
|
|
120
|
+
const buffer = Buffer.alloc(MAX_SPAWN_BYTES + 1);
|
|
121
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
122
|
+
const prefix = buffer.subarray(0, bytesRead).toString("utf8");
|
|
123
|
+
const lines = prefix.split("\n");
|
|
124
|
+
// A prefix cut mid-record must never be treated as a complete JSON line.
|
|
125
|
+
if (bytesRead > MAX_SPAWN_BYTES)
|
|
126
|
+
lines.pop();
|
|
127
|
+
for (const line of lines.slice(0, 32)) {
|
|
128
|
+
if (!line.trim())
|
|
129
|
+
continue;
|
|
130
|
+
const record = JSON.parse(line);
|
|
131
|
+
if (record.event !== "spawn") {
|
|
132
|
+
if (record.direction === "meta")
|
|
133
|
+
continue;
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const expectedCwd = job.operation_kind === "swarm-write"
|
|
137
|
+
? path.join(worktreesDir, job.repo_id, `swarm-write-${job.job_id}`)
|
|
138
|
+
: job.cwd;
|
|
139
|
+
if (record.intended_engine !== "native-v2" || record.kimi_version !== job.kimi_version ||
|
|
140
|
+
record.operation_kind !== job.operation_kind || record.cwd !== expectedCwd)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (!Array.isArray(record.args) || record.args.length < 2)
|
|
143
|
+
return undefined;
|
|
144
|
+
const args = record.args;
|
|
145
|
+
const prompt = args[args.length - 1];
|
|
146
|
+
if (args[args.length - 2] !== "-p" || typeof prompt !== "string" || !prompt.trim())
|
|
147
|
+
return undefined;
|
|
148
|
+
return digestPrompt(prompt) === job.prompt_digest ? prompt : undefined;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await file.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { access, stat, readFile } from "node:fs/promises";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { RuntimeError } from "../errors.js";
|
|
4
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
5
|
+
import { withJobStore } from "../job-store.js";
|
|
6
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
7
|
+
import { renderManagedJobOutput } from "../render.js";
|
|
8
|
+
import { sweepStaleJobs } from "../jobs.js";
|
|
9
|
+
const MAX_REPLAY_LOG_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
export async function runReplay(argv, context) {
|
|
11
|
+
const [jobId, ...rest] = argv;
|
|
12
|
+
if (!jobId || rest.length > 0) {
|
|
13
|
+
throw new RuntimeError("INVALID_ARGS", "replay expects exactly one job id: replay <job-id>.", "replay.parse");
|
|
14
|
+
}
|
|
15
|
+
const paths = resolvePluginPaths(context.env);
|
|
16
|
+
await ensurePluginPaths(paths);
|
|
17
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
18
|
+
return withJobStore(paths, async (store) => {
|
|
19
|
+
await sweepStaleJobs(store, paths);
|
|
20
|
+
const job = store.getJob(jobId);
|
|
21
|
+
if (!job || job.repo_id !== repoIdentity.repoId) {
|
|
22
|
+
throw new RuntimeError("JOB_NOT_FOUND", `No job matched ${jobId} for replay.`, "replay.lookup");
|
|
23
|
+
}
|
|
24
|
+
const replayed = await replayJob(job);
|
|
25
|
+
return `${replayed.rendered}${replayed.rendered.endsWith("\n") ? "" : "\n"}`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function replayJob(job) {
|
|
29
|
+
if (!job.stream_log_path) {
|
|
30
|
+
throw new RuntimeError("REPLAY_LOG_MISSING", `Job ${job.job_id} does not have a stored stream log path to replay.`, "replay.lookup");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await access(job.stream_log_path, fsConstants.R_OK);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new RuntimeError("REPLAY_LOG_MISSING", `Job ${job.job_id} cannot be replayed because ${job.stream_log_path} is missing.`, "replay.lookup");
|
|
37
|
+
}
|
|
38
|
+
const stats = await stat(job.stream_log_path);
|
|
39
|
+
if (stats.size > MAX_REPLAY_LOG_BYTES) {
|
|
40
|
+
throw new RuntimeError("REPLAY_LOG_TOO_LARGE", `Job ${job.job_id} stream log is ${stats.size} bytes, which exceeds the ${MAX_REPLAY_LOG_BYTES}-byte replay ceiling.`, "replay.lookup");
|
|
41
|
+
}
|
|
42
|
+
const finalText = await replayFinalText(job.stream_log_path);
|
|
43
|
+
const rendered = renderManagedJobOutput(job, finalText);
|
|
44
|
+
return {
|
|
45
|
+
rendered: rendered.rendered,
|
|
46
|
+
output: rendered.output,
|
|
47
|
+
summary: rendered.summary,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function replayFinalText(logPath) {
|
|
51
|
+
const contents = await readFile(logPath, "utf8");
|
|
52
|
+
const rawLines = contents.split("\n");
|
|
53
|
+
let sawSpawn = false;
|
|
54
|
+
let sawExit = false;
|
|
55
|
+
let sawV1Event = false;
|
|
56
|
+
let sawV04Direction = false;
|
|
57
|
+
let assistantContent = "";
|
|
58
|
+
for (let lineNumber = 0; lineNumber < rawLines.length; lineNumber += 1) {
|
|
59
|
+
const line = rawLines[lineNumber].trim();
|
|
60
|
+
if (!line)
|
|
61
|
+
continue;
|
|
62
|
+
let entry;
|
|
63
|
+
try {
|
|
64
|
+
entry = JSON.parse(line);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// A truncated trailing line is the expected shape when a worker
|
|
68
|
+
// was SIGKILL'd mid-write. Drop it silently so replay still works
|
|
69
|
+
// on the consistent prefix.
|
|
70
|
+
if (lineNumber === rawLines.length - 1)
|
|
71
|
+
continue;
|
|
72
|
+
throw new RuntimeError("REPLAY_LOG_INVALID", `Stream log ${logPath}:${lineNumber + 1} is malformed JSON: ${error.message}`, "replay.log", { cause: error });
|
|
73
|
+
}
|
|
74
|
+
if (typeof entry.event === "string")
|
|
75
|
+
sawV1Event = true;
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(entry, "direction"))
|
|
77
|
+
sawV04Direction = true;
|
|
78
|
+
const event = typeof entry.event === "string" ? entry.event : "";
|
|
79
|
+
switch (event) {
|
|
80
|
+
case "spawn":
|
|
81
|
+
sawSpawn = true;
|
|
82
|
+
break;
|
|
83
|
+
case "record": {
|
|
84
|
+
const record = entry.record;
|
|
85
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) {
|
|
86
|
+
throw new RuntimeError("REPLAY_LOG_INVALID", `Stream log ${logPath}:${lineNumber + 1} record entry is not an object.`, "replay.log");
|
|
87
|
+
}
|
|
88
|
+
const role = record.role;
|
|
89
|
+
const content = record.content;
|
|
90
|
+
if (role === "assistant" && typeof content === "string") {
|
|
91
|
+
assistantContent += content;
|
|
92
|
+
}
|
|
93
|
+
// Non-string content (null/undefined/object) silently skipped —
|
|
94
|
+
// these are diagnostic-only records we don't render.
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "exit":
|
|
98
|
+
sawExit = true;
|
|
99
|
+
break;
|
|
100
|
+
case "process_error": {
|
|
101
|
+
const detail = typeof entry.message === "string" ? entry.message : "<unknown>";
|
|
102
|
+
throw new RuntimeError("REPLAY_LOG_PROCESS_ERROR", `Stream log ${logPath} captured a subprocess error and cannot be replayed: ${detail}`, "replay.log", { details: { logPath, line: lineNumber + 1 } });
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
// Unknown / malformed-line / future events are diagnostics — skip.
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!sawSpawn) {
|
|
110
|
+
// Differentiate "this is a v0.4 wire log" from "this is a v1.0 log
|
|
111
|
+
// that died before reaching the spawn event". v0.4 logs had a
|
|
112
|
+
// `direction` field on every entry (out/in/meta/stderr); v1.0 logs
|
|
113
|
+
// use `event` instead.
|
|
114
|
+
if (sawV04Direction && !sawV1Event) {
|
|
115
|
+
throw new RuntimeError("REPLAY_LOG_UNREADABLE", `Stream log ${logPath} is a v0.4 wire log (no \`event\` key). v0.4 wire logs are not supported by the v1.0 replay command.`, "replay.log", { details: { logPath } });
|
|
116
|
+
}
|
|
117
|
+
throw new RuntimeError("REPLAY_LOG_NO_SPAWN", `Stream log ${logPath} contains no spawn event — the worker likely failed before launching kimi (e.g., synchronous spawn failure, early cancellation). No assistant content to replay.`, "replay.log", { details: { logPath } });
|
|
118
|
+
}
|
|
119
|
+
if (!sawExit && assistantContent.length === 0) {
|
|
120
|
+
throw new RuntimeError("REPLAY_LOG_INVALID", `Stream log ${logPath} did not capture any assistant content before truncation.`, "replay.log", { details: { logPath } });
|
|
121
|
+
}
|
|
122
|
+
return assistantContent;
|
|
123
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
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 { classifyManagedCommandFailure } from "../kimi-errors.js";
|
|
12
|
+
import { KIMI_RESCUE_PROMPT_TIMEOUT_MS } from "../kimi-timeouts.js";
|
|
13
|
+
import { writeInvocationLogHeader } from "../logging.js";
|
|
14
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
15
|
+
import { parseRescueArgs } from "../parsing.js";
|
|
16
|
+
import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.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
|
+
export { describeMissingResult } from "../background-spawn.js";
|
|
22
|
+
// v1.0 cutover note (PR 3):
|
|
23
|
+
//
|
|
24
|
+
// Rescue is the only write-capable command. v0.4 enforced safety
|
|
25
|
+
// in-band via `WireClient.approvalPolicy = createRescueApprovalPolicy(cwd)`,
|
|
26
|
+
// which called back into our runtime on every kimi-side approval
|
|
27
|
+
// request. v1.0 moves that enforcement into the PreToolUse hook
|
|
28
|
+
// (`runtime/hooks/approval-hook.ts`), which spawns under kimi-code
|
|
29
|
+
// and calls `evaluateRescueHookRequest` for `KIMI_PLUGIN_CC_CMD=rescue`
|
|
30
|
+
// tool calls. The runtime side now just sets the env var and lets
|
|
31
|
+
// the hook gate everything.
|
|
32
|
+
//
|
|
33
|
+
// The security helpers in `runtime/rescue-approval.ts` are unchanged.
|
|
34
|
+
// "Continue working on the bug" verbs that auto-attach to the latest
|
|
35
|
+
// rescue session. Documented UX trap: a prompt matching this pattern
|
|
36
|
+
// will resume the most recent rescue session regardless of whether
|
|
37
|
+
// that session was about the same bug. Users should pass an explicit
|
|
38
|
+
// `--resume <job-id>` or `--fresh` when the topic switches.
|
|
39
|
+
const AUTO_RESUME_PATTERN = /\b(continue|resume|keep going|keep working|apply the top fix|dig deeper)\b/i;
|
|
40
|
+
const RESCUE_SUMMARY_MAX = 120;
|
|
41
|
+
const RESCUE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
|
|
42
|
+
export async function runRescue(argv, context) {
|
|
43
|
+
const parsed = parseRescueArgs(argv);
|
|
44
|
+
const paths = resolvePluginPaths(context.env);
|
|
45
|
+
await ensurePluginPaths(paths);
|
|
46
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
47
|
+
const rescueConfig = getManagedCommandConfig("rescue");
|
|
48
|
+
const store = new JobStore(paths);
|
|
49
|
+
try {
|
|
50
|
+
await sweepStaleJobs(store, paths);
|
|
51
|
+
const sessionResolution = await resolveRescueSession(store, repoIdentity.repoId, parsed.prompt, parsed.fresh, parsed.resume, parsed.resumeTarget);
|
|
52
|
+
const prompt = buildRescuePrompt(parsed.prompt, sessionResolution.reusedSession);
|
|
53
|
+
const executionPlan = await prepareKimiExecutionPlan({
|
|
54
|
+
operationKind: "rescue",
|
|
55
|
+
cwd: context.cwd,
|
|
56
|
+
env: context.env,
|
|
57
|
+
resumedFromJobId: sessionResolution.resumedFromJobId,
|
|
58
|
+
resumeSessionId: sessionResolution.kimiSessionId ?? undefined,
|
|
59
|
+
resumeSource: sessionResolution.sourceJob,
|
|
60
|
+
});
|
|
61
|
+
const jobId = randomUUID();
|
|
62
|
+
const logPath = path.join(paths.logsDir, `rescue-${jobId}.jsonl`);
|
|
63
|
+
const job = store.createJob({
|
|
64
|
+
job_id: jobId,
|
|
65
|
+
repo_id: repoIdentity.repoId,
|
|
66
|
+
command_type: "rescue",
|
|
67
|
+
cwd: context.cwd,
|
|
68
|
+
model: parsed.model ?? null,
|
|
69
|
+
thinking: parsed.thinking ?? null,
|
|
70
|
+
background: parsed.background,
|
|
71
|
+
pid: parsed.background ? null : process.pid,
|
|
72
|
+
kimi_pid: null,
|
|
73
|
+
status: "running",
|
|
74
|
+
kimi_session_id: sessionResolution.kimiSessionId,
|
|
75
|
+
...persistedExecutionPlanFields(executionPlan),
|
|
76
|
+
agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
|
|
77
|
+
prompt_digest: digestPrompt(prompt),
|
|
78
|
+
summary: shorten(prompt, RESCUE_SUMMARY_MAX),
|
|
79
|
+
phase: parsed.background ? "queued" : "starting",
|
|
80
|
+
final_output_path: null,
|
|
81
|
+
stream_log_path: logPath,
|
|
82
|
+
error: null,
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
await writeInvocationLogHeader(logPath, {
|
|
86
|
+
commandType: "rescue",
|
|
87
|
+
kimiSessionId: sessionResolution.kimiSessionId ?? "(pending)",
|
|
88
|
+
cwd: context.cwd,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
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);
|
|
93
|
+
await markJobFailed(store, paths, job, classified, rescueConfig.cancellation.failedSummary, { phase: "failed" });
|
|
94
|
+
throw classified;
|
|
95
|
+
}
|
|
96
|
+
if (parsed.background) {
|
|
97
|
+
return startBackgroundJob(job, prompt, context, paths, {
|
|
98
|
+
workerKind: "rescue",
|
|
99
|
+
wait: parsed.wait,
|
|
100
|
+
promptEnvVar: "KIMI_PLUGIN_CC_RESCUE_PROMPT_B64",
|
|
101
|
+
reusedSessionEnvVar: "KIMI_PLUGIN_CC_RESCUE_REUSED_SESSION",
|
|
102
|
+
reusedSession: sessionResolution.reusedSession,
|
|
103
|
+
failedSummary: rescueConfig.cancellation.failedSummary,
|
|
104
|
+
missingResultErrorCode: "RESCUE_RESULT_MISSING",
|
|
105
|
+
spawnFailedErrorCode: "RESCUE_WORKER_SPAWN_FAILED",
|
|
106
|
+
earlyExitErrorCode: "RESCUE_WORKER_EXITED_EARLY",
|
|
107
|
+
nodeBinInvalidErrorCode: "RESCUE_NODE_BIN_INVALID",
|
|
108
|
+
waitStage: "rescue.wait",
|
|
109
|
+
spawnStage: "rescue.worker.spawn",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const completed = await executeRescueJob(job.job_id, prompt, context);
|
|
113
|
+
if (!completed.final_output_path) {
|
|
114
|
+
throw new RuntimeError("RESCUE_RESULT_MISSING", "Rescue finished without a rendered result.", "rescue.result");
|
|
115
|
+
}
|
|
116
|
+
return readArtifact(completed.final_output_path);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
store.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function executeRescueJob(jobId, prompt, context, options) {
|
|
123
|
+
const paths = resolvePluginPaths(context.env);
|
|
124
|
+
await ensurePluginPaths(paths);
|
|
125
|
+
const store = new JobStore(paths);
|
|
126
|
+
const job = store.getJob(jobId);
|
|
127
|
+
if (!job) {
|
|
128
|
+
store.close();
|
|
129
|
+
throw new RuntimeError("JOB_NOT_FOUND", `Rescue job ${jobId} was not found.`, "rescue.worker");
|
|
130
|
+
}
|
|
131
|
+
const rescueConfig = getManagedCommandConfig("rescue");
|
|
132
|
+
const cancel = rescueConfig.cancellation;
|
|
133
|
+
// Rescue writes directly to the workspace. Without the PreToolUse hook,
|
|
134
|
+
// kimi-code's `-p` mode auto-approves every Bash/Write/Edit, including
|
|
135
|
+
// destructive ones. Refuse before the worker can spawn Kimi.
|
|
136
|
+
//
|
|
137
|
+
// The hook check happens BEFORE `createCliCancellationHandlers()` so
|
|
138
|
+
// an early return doesn't leak SIGTERM/SIGINT listeners. Tests /
|
|
139
|
+
// setup probes / intentional bypass: KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1.
|
|
140
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
141
|
+
const installStatus = await verifyHookInstalled(context.env);
|
|
142
|
+
if (!installStatus.installed) {
|
|
143
|
+
maybeWarnHookMissing(installStatus, "rescue", context.stderr);
|
|
144
|
+
const classified = new RuntimeError("RESCUE_HOOK_NOT_INSTALLED", [
|
|
145
|
+
"rescue refuses to run without the kimi-plugin-cc PreToolUse hook.",
|
|
146
|
+
`Hook check failed: ${installStatus.reason ?? "unknown"}.`,
|
|
147
|
+
"Repair by running Claude Code /kimi:setup or Codex $kimi-setup, then retry.",
|
|
148
|
+
"KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1 is only for deliberate tests or diagnostics.",
|
|
149
|
+
hookRefusalRetryProtocol(context.env),
|
|
150
|
+
].join(" "), "rescue.hook-check", { details: hookRefusalDetails(installStatus) });
|
|
151
|
+
try {
|
|
152
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
store.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const handlers = createCliCancellationHandlers();
|
|
160
|
+
try {
|
|
161
|
+
const executionPlan = executionPlanFromPersisted(job);
|
|
162
|
+
// Re-validate resume lineage from the store before spawning: a detached
|
|
163
|
+
// worker (or a forged queued row) reloads the persisted plan and would
|
|
164
|
+
// otherwise hand kimi_session_id to the spawn without the dispatch-time
|
|
165
|
+
// lineage check. Fresh sessions (no resumedFromJobId) are exempt.
|
|
166
|
+
if (job.kimi_session_id !== null) {
|
|
167
|
+
await assertPersistedResumeLineage(store, executionPlan, job.kimi_session_id, "rescue");
|
|
168
|
+
}
|
|
169
|
+
if (options?.workerPid) {
|
|
170
|
+
store.updateRunningJob(job.job_id, { pid: options.workerPid, phase: "worker-running" });
|
|
171
|
+
}
|
|
172
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
173
|
+
const result = await runCliPromptWithBudget({
|
|
174
|
+
cwd: job.cwd,
|
|
175
|
+
env: context.env,
|
|
176
|
+
command: executionPlan.command,
|
|
177
|
+
prefixArgs: [...executionPlan.prefixArgs],
|
|
178
|
+
executionPlan,
|
|
179
|
+
prompt,
|
|
180
|
+
commandLabel: "rescue",
|
|
181
|
+
trustedWorkspaceRoot: job.cwd,
|
|
182
|
+
model: job.model ?? undefined,
|
|
183
|
+
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
184
|
+
logPath: job.stream_log_path,
|
|
185
|
+
signal: handlers.signal,
|
|
186
|
+
// SIGKILL escalation defaults to 1500ms inside cli-client —
|
|
187
|
+
// matches v0.4's cancellation.ts behavior for the wire client.
|
|
188
|
+
}, KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
|
|
189
|
+
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
190
|
+
if (handlers.cancelling) {
|
|
191
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
|
|
192
|
+
}
|
|
193
|
+
assertCliResultSuccess(result, "rescue.runtime");
|
|
194
|
+
if (result.sessionId !== undefined &&
|
|
195
|
+
result.sessionId.length > 0 &&
|
|
196
|
+
result.sessionId !== job.kimi_session_id) {
|
|
197
|
+
// length>0 guard: empty-string capture would poison the row. (Kimi
|
|
198
|
+
// alpha.4 challenge finding #3.)
|
|
199
|
+
store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
|
|
200
|
+
}
|
|
201
|
+
await syncKimiSessionTitle({
|
|
202
|
+
promptText: prompt,
|
|
203
|
+
env: context.env,
|
|
204
|
+
cwd: job.cwd,
|
|
205
|
+
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
206
|
+
title: buildKimiSessionTitle("rescue", prompt),
|
|
207
|
+
stderr: context.stderr,
|
|
208
|
+
});
|
|
209
|
+
if (job.kimi_session_id === null) {
|
|
210
|
+
warnIfSessionIdMissing(result, "rescue", job.job_id, context.stderr);
|
|
211
|
+
}
|
|
212
|
+
const finalText = reassembleProseFromRecords(result.records);
|
|
213
|
+
const rendered = renderManagedJobOutput(job, finalText);
|
|
214
|
+
let artifactPath;
|
|
215
|
+
try {
|
|
216
|
+
if (context.env.KIMI_PLUGIN_CC_TEST_FAIL_WRITE_ARTIFACT === "1") {
|
|
217
|
+
throw new Error("Simulated artifact write failure (test seam).");
|
|
218
|
+
}
|
|
219
|
+
artifactPath = await writeArtifact(paths, job, rendered.rendered);
|
|
220
|
+
}
|
|
221
|
+
catch (writeError) {
|
|
222
|
+
// LLM-caller discipline: writing the raw model output to stderr
|
|
223
|
+
// for a wrapper LLM-caller would corrupt its prose stream. Keep
|
|
224
|
+
// the raw text in the RuntimeError details — surfaces via
|
|
225
|
+
// /kimi:status / /kimi:result --json — and emit only a short
|
|
226
|
+
// human-facing line on stderr.
|
|
227
|
+
context.stderr.write(`[kimi-plugin-cc] rescue artifact write failed for job ${job.job_id}; raw output preserved in error details.\n`);
|
|
228
|
+
const classified = new RuntimeError("RESCUE_ARTIFACT_WRITE_FAILED", `Failed to write rescue artifact: ${writeError.message ?? String(writeError)}`, "rescue.artifact", {
|
|
229
|
+
...(writeError instanceof Error ? { cause: writeError } : {}),
|
|
230
|
+
details: { rawOutput: finalText },
|
|
231
|
+
});
|
|
232
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
233
|
+
}
|
|
234
|
+
if (handlers.cancelling) {
|
|
235
|
+
throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterArtifact, "rescue.runtime");
|
|
236
|
+
}
|
|
237
|
+
return (store.markCompleted(job.job_id, {
|
|
238
|
+
summary: rendered.summary,
|
|
239
|
+
phase: "done",
|
|
240
|
+
final_output_path: artifactPath,
|
|
241
|
+
error: null,
|
|
242
|
+
}) ?? job);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (handlers.cancelling) {
|
|
246
|
+
const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
|
|
247
|
+
return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
|
|
248
|
+
}
|
|
249
|
+
const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
|
|
250
|
+
preserveStage: true,
|
|
251
|
+
});
|
|
252
|
+
return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
handlers.dispose();
|
|
256
|
+
store.close();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function buildRescuePrompt(prompt, reusedSession) {
|
|
260
|
+
if (prompt?.trim()) {
|
|
261
|
+
return prompt.trim();
|
|
262
|
+
}
|
|
263
|
+
if (reusedSession) {
|
|
264
|
+
return "Continue the previous rescue task using the latest repository state.";
|
|
265
|
+
}
|
|
266
|
+
throw new RuntimeError("INVALID_ARGS", "rescue requires a task description.", "rescue.parse");
|
|
267
|
+
}
|
|
268
|
+
async function resolveRescueSession(store, repoId, prompt, fresh, resume, resumeTarget) {
|
|
269
|
+
if (fresh) {
|
|
270
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
271
|
+
}
|
|
272
|
+
if (resumeTarget) {
|
|
273
|
+
const byJob = store.getJob(resumeTarget);
|
|
274
|
+
const scoped = byJob?.repo_id === repoId && byJob.command_type === "rescue" ? byJob : null;
|
|
275
|
+
const exact = scoped ?? store.findRescueJobBySession(repoId, resumeTarget);
|
|
276
|
+
if (!exact) {
|
|
277
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", `No rescue job or session matched ${resumeTarget}.`, "rescue.resume");
|
|
278
|
+
}
|
|
279
|
+
const source = await reconcileHistoricalJobProvenance(store, exact);
|
|
280
|
+
ensureSessionIsNotRunning(source);
|
|
281
|
+
if (!source.kimi_session_id) {
|
|
282
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", `No rescue job or session matched ${resumeTarget}.`, "rescue.resume");
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
kimiSessionId: source.kimi_session_id,
|
|
286
|
+
reusedSession: true,
|
|
287
|
+
resumedFromJobId: source.job_id,
|
|
288
|
+
sourceJob: source,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (resume) {
|
|
292
|
+
const latest = store.findLatestJob({ repoId, commandType: "rescue" });
|
|
293
|
+
if (!latest) {
|
|
294
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", "No prior rescue session exists for this repository.", "rescue.resume");
|
|
295
|
+
}
|
|
296
|
+
const source = await reconcileHistoricalJobProvenance(store, latest);
|
|
297
|
+
ensureSessionIsNotRunning(source);
|
|
298
|
+
if (!source.kimi_session_id) {
|
|
299
|
+
throw new RuntimeError("RESCUE_RESUME_NOT_FOUND", "No prior rescue session exists for this repository.", "rescue.resume");
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
kimiSessionId: source.kimi_session_id,
|
|
303
|
+
reusedSession: true,
|
|
304
|
+
resumedFromJobId: source.job_id,
|
|
305
|
+
sourceJob: source,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (prompt && AUTO_RESUME_PATTERN.test(prompt)) {
|
|
309
|
+
const latest = store.findLatestJob({ repoId, commandType: "rescue" });
|
|
310
|
+
if (latest?.kimi_session_id && latest.status !== "running") {
|
|
311
|
+
const source = await reconcileHistoricalJobProvenance(store, latest);
|
|
312
|
+
return {
|
|
313
|
+
kimiSessionId: source.kimi_session_id,
|
|
314
|
+
reusedSession: true,
|
|
315
|
+
resumedFromJobId: source.job_id,
|
|
316
|
+
sourceJob: source,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|
|
321
|
+
}
|
|
322
|
+
function ensureSessionIsNotRunning(job) {
|
|
323
|
+
if (job.status === "running") {
|
|
324
|
+
throw new RuntimeError("RESCUE_ALREADY_RUNNING", `Rescue session ${job.kimi_session_id ?? "<unknown>"} is already active under job ${job.job_id}.`, "rescue.resume");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function shorten(text, max) {
|
|
328
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
329
|
+
if (normalized.length <= max) {
|
|
330
|
+
return normalized;
|
|
331
|
+
}
|
|
332
|
+
return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
333
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { withJobStore } 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
|
+
summary: job.summary,
|
|
46
|
+
error: job.error,
|
|
47
|
+
artifact_path: artifactPath,
|
|
48
|
+
body,
|
|
49
|
+
created_at: job.created_at,
|
|
50
|
+
completed_at: job.updated_at,
|
|
51
|
+
operation_kind: job.operation_kind,
|
|
52
|
+
intended_engine: job.intended_engine,
|
|
53
|
+
observed_engine: job.observed_engine,
|
|
54
|
+
kimi_version: job.kimi_version,
|
|
55
|
+
system_version: job.system_version,
|
|
56
|
+
resumed_from_job_id: job.resumed_from_job_id,
|
|
57
|
+
}, null, 2)}\n`;
|
|
58
|
+
}
|