kimi-plugin-cc 2.4.0 → 2.4.2
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 +18 -0
- package/README.md +17 -9
- package/dist/cli-client.js +19 -9
- package/dist/commands/cancel.js +14 -3
- package/dist/commands/cli-helpers.js +5 -1
- package/dist/commands/recover-swarm.js +135 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/swarm.js +241 -87
- package/dist/companion.js +5 -1
- package/dist/git.js +5 -3
- package/dist/help.js +3 -2
- package/dist/hooks/config-safety.js +4 -3
- package/dist/job-store.js +13 -0
- package/dist/jobs.js +3 -0
- package/dist/kimi-engine.js +1 -0
- package/dist/kimi-errors.js +27 -21
- package/dist/parsing.js +1 -0
- package/dist/process-tree.js +48 -17
- package/dist/prompt-size.js +18 -0
- package/dist/swarm-activation.js +833 -0
- package/dist/swarm-recovery.js +134 -0
- package/dist/version.js +1 -1
- package/docs/migration.md +12 -2
- package/docs/registry-releases.md +13 -2
- package/docs/safety.md +20 -6
- package/docs/swarm-recovery.md +33 -0
- package/docs/windows-validation.md +39 -0
- package/package.json +7 -4
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +19 -9
- package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +5 -1
- package/plugins/kimi-codex/dist/commands/recover-swarm.js +135 -0
- package/plugins/kimi-codex/dist/commands/status.js +1 -1
- package/plugins/kimi-codex/dist/commands/swarm.js +241 -87
- package/plugins/kimi-codex/dist/companion.js +5 -1
- package/plugins/kimi-codex/dist/git.js +5 -3
- package/plugins/kimi-codex/dist/help.js +3 -2
- package/plugins/kimi-codex/dist/hooks/config-safety.js +4 -3
- package/plugins/kimi-codex/dist/job-store.js +13 -0
- package/plugins/kimi-codex/dist/jobs.js +3 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +27 -21
- package/plugins/kimi-codex/dist/parsing.js +1 -0
- package/plugins/kimi-codex/dist/process-tree.js +48 -17
- package/plugins/kimi-codex/dist/prompt-size.js +18 -0
- package/plugins/kimi-codex/dist/swarm-activation.js +833 -0
- package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
|
@@ -85,12 +85,13 @@ async function runGit(args, cwd) {
|
|
|
85
85
|
}
|
|
86
86
|
return result.stdout;
|
|
87
87
|
}
|
|
88
|
-
async function runGitAllowFailure(args, cwd) {
|
|
88
|
+
async function runGitAllowFailure(args, cwd, signal) {
|
|
89
89
|
try {
|
|
90
90
|
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
91
91
|
cwd,
|
|
92
92
|
env: process.env,
|
|
93
93
|
maxBuffer: 10 * 1024 * 1024,
|
|
94
|
+
signal,
|
|
94
95
|
});
|
|
95
96
|
return { ok: true, stdout, stderr };
|
|
96
97
|
}
|
|
@@ -163,11 +164,11 @@ export async function createEphemeralWorktree(repoRoot, base, worktreePath) {
|
|
|
163
164
|
*
|
|
164
165
|
* Returns the patch text (empty string if the swarm changed nothing).
|
|
165
166
|
*/
|
|
166
|
-
export async function captureWorktreePatch(worktreePath) {
|
|
167
|
+
export async function captureWorktreePatch(worktreePath, signal) {
|
|
167
168
|
// Intent-to-add is required so untracked files surface in `git diff HEAD`.
|
|
168
169
|
// An add -N failure aborts capture rather than returning a patch that omits
|
|
169
170
|
// new files.
|
|
170
|
-
const intentToAdd = await runGitAllowFailure(["add", "-N", "--", "."], worktreePath);
|
|
171
|
+
const intentToAdd = await runGitAllowFailure(["add", "-N", "--", "."], worktreePath, signal);
|
|
171
172
|
if (!intentToAdd.ok) {
|
|
172
173
|
throw new RuntimeError("GIT_WORKTREE_INTENT_TO_ADD_FAILED", `git add -N (worktree capture) failed: ${intentToAdd.stderr || "unknown error"}`, "swarm.worktree", { details: { worktreePath } });
|
|
173
174
|
}
|
|
@@ -176,6 +177,7 @@ export async function captureWorktreePatch(worktreePath) {
|
|
|
176
177
|
cwd: worktreePath,
|
|
177
178
|
env: process.env,
|
|
178
179
|
maxBuffer: MAX_PATCH_BYTES,
|
|
180
|
+
signal,
|
|
179
181
|
});
|
|
180
182
|
return stdout;
|
|
181
183
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, } from "./parsing.js";
|
|
2
|
-
const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions";
|
|
1
|
+
import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, RECOVER_SWARM_SUPPORTED_FLAGS, } from "./parsing.js";
|
|
2
|
+
const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions, recover-swarm";
|
|
3
3
|
const TASK_KINDS = "rescue, challenge, pursue, swarm";
|
|
4
4
|
/** Help is a dispatch-only, side-effect-free exception to normal argument parsing. */
|
|
5
5
|
export function companionHelp(argv) {
|
|
@@ -33,6 +33,7 @@ export function companionHelp(argv) {
|
|
|
33
33
|
case "cancel":
|
|
34
34
|
return usage(command, "[job-id] [options]", JOB_LOOKUP_SUPPORTED_FLAGS, command === "status" ? "Show job state as JSON." : command === "result" ? "Read a terminal artifact, optionally in a JSON envelope." : "Cancel a running job; without an id, target the repository's latest running job.");
|
|
35
35
|
case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
|
|
36
|
+
case "recover-swarm": return `Usage: companion.sh recover-swarm <job-id> [${RECOVER_SWARM_SUPPORTED_FLAGS}]. Preview durable swarm recovery; --apply stops verified orphans, disables the recorded session and captures a patch. Never resumes prompts or changes terminal job outcomes.`;
|
|
36
37
|
case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
|
|
37
38
|
case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
|
|
38
39
|
case "mcp": return "Usage: companion.sh mcp. Run the Kimi MCP stdio server; normally launched by Claude Code or Codex plugin configuration.";
|
|
@@ -594,8 +594,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
594
594
|
}
|
|
595
595
|
// Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
|
|
596
596
|
// 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
|
|
597
|
-
// 2.3.0 (b5e28fd6)
|
|
598
|
-
// are unchanged. Require the full version string AND matching components;
|
|
597
|
+
// 2.3.0 (b5e28fd6), 2.3.1 (684bae9e) and 2.3.2 (b8dd3a13): the additive
|
|
598
|
+
// events and HookDefSchema are unchanged. Require the full version string AND matching components;
|
|
599
599
|
// no inferred 2.x compatibility.
|
|
600
600
|
if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
|
|
601
601
|
(version.version === "2.0.1" && version.patch === 1) ||
|
|
@@ -604,7 +604,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
604
604
|
(version.version === "2.1.1" && version.patch === 1))) ||
|
|
605
605
|
(version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))) ||
|
|
606
606
|
(version.minor === 3 && ((version.version === "2.3.0" && version.patch === 0) ||
|
|
607
|
-
(version.version === "2.3.1" && version.patch === 1)
|
|
607
|
+
(version.version === "2.3.1" && version.patch === 1) ||
|
|
608
|
+
(version.version === "2.3.2" && version.patch === 2))))) {
|
|
608
609
|
return { valid: true };
|
|
609
610
|
}
|
|
610
611
|
if (version.major !== 0) {
|
|
@@ -122,6 +122,8 @@ export class JobStore {
|
|
|
122
122
|
CREATE INDEX IF NOT EXISTS jobs_repo_updated_idx
|
|
123
123
|
ON jobs (repo_id, updated_at DESC);
|
|
124
124
|
|
|
125
|
+
CREATE TABLE IF NOT EXISTS swarm_recovery (job_id TEXT PRIMARY KEY, payload TEXT NOT NULL, recovery_owner TEXT);
|
|
126
|
+
|
|
125
127
|
CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
|
|
126
128
|
ON jobs (repo_id, command_type, updated_at DESC);
|
|
127
129
|
|
|
@@ -186,6 +188,17 @@ export class JobStore {
|
|
|
186
188
|
throw translateSqliteError(error);
|
|
187
189
|
}
|
|
188
190
|
}
|
|
191
|
+
getSwarmRecovery(jobId) {
|
|
192
|
+
const row = this.db.get("SELECT payload, recovery_owner FROM swarm_recovery WHERE job_id = ?", jobId);
|
|
193
|
+
return row ? { record: JSON.parse(row.payload), owner: row.recovery_owner } : null;
|
|
194
|
+
}
|
|
195
|
+
saveSwarmRecovery(jobId, record) {
|
|
196
|
+
this.db.run("INSERT INTO swarm_recovery (job_id, payload) VALUES (?, ?) ON CONFLICT(job_id) DO UPDATE SET payload = excluded.payload", [jobId, JSON.stringify(record)]);
|
|
197
|
+
}
|
|
198
|
+
claimSwarmRecovery(jobId, expected, owner) {
|
|
199
|
+
this.db.run("UPDATE swarm_recovery SET recovery_owner = ? WHERE job_id = ? AND recovery_owner IS ?", [owner, jobId, expected]);
|
|
200
|
+
return this.db.get("SELECT changes() AS count").count === 1;
|
|
201
|
+
}
|
|
189
202
|
close() {
|
|
190
203
|
this.db.close();
|
|
191
204
|
}
|
|
@@ -85,6 +85,9 @@ export async function sweepStaleJobs(store, paths) {
|
|
|
85
85
|
for (const job of store.listRunningJobsWithProcessHints()) {
|
|
86
86
|
if (store.getMcpTask(job.job_id))
|
|
87
87
|
continue;
|
|
88
|
+
// Recovery owns orphaned swarm resources; PID-only sweeping must not signal replacements.
|
|
89
|
+
if (store.getSwarmRecovery(job.job_id))
|
|
90
|
+
continue;
|
|
88
91
|
if (!isStaleEnoughToSweep(job)) {
|
|
89
92
|
continue;
|
|
90
93
|
}
|
|
@@ -44,6 +44,11 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
|
|
|
44
44
|
const details = { availability: classification.kind };
|
|
45
45
|
if (error instanceof RuntimeError) {
|
|
46
46
|
details.cause_code = error.code;
|
|
47
|
+
for (const key of ["spawn_code", "spawn_errno", "spawn_message"]) {
|
|
48
|
+
if (error.details[key] !== undefined)
|
|
49
|
+
details[key] = key === "spawn_message" && typeof error.details[key] === "string"
|
|
50
|
+
? truncateCauseMessageForDetails(error.details[key]) : error.details[key];
|
|
51
|
+
}
|
|
47
52
|
if (error.code === "RESPONSE_TIMEOUT") {
|
|
48
53
|
for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
|
|
49
54
|
if (error.details[key] !== undefined)
|
|
@@ -129,28 +134,29 @@ function classifyKimiAvailability(error) {
|
|
|
129
134
|
authProbe: "failed",
|
|
130
135
|
};
|
|
131
136
|
}
|
|
132
|
-
if (error instanceof RuntimeError && error.code === "CLI_SPAWN_FAILED") {
|
|
133
|
-
return {
|
|
134
|
-
kind: "binary_unavailable",
|
|
135
|
-
summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
|
|
136
|
-
nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
|
|
137
|
-
runtimeProbe: "failed",
|
|
138
|
-
authProbe: "failed",
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// v1.0 cli-client surfaces async ENOENT (Bun) as CLI_PROCESS_ERROR with
|
|
142
|
-
// "spawn ... ENOENT" in the message. Map that to binary_unavailable so
|
|
143
|
-
// the failure message points users at /kimi:setup.
|
|
144
137
|
if (error instanceof RuntimeError &&
|
|
145
|
-
error.code === "CLI_PROCESS_ERROR"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
(error.code === "CLI_SPAWN_FAILED" || error.code === "CLI_PROCESS_ERROR")) {
|
|
139
|
+
const spawnCode = error.details.spawn_code;
|
|
140
|
+
const binaryUnavailable = typeof spawnCode === "string" && ["ENOENT", "EACCES", "ENOEXEC"].includes(spawnCode);
|
|
141
|
+
if (typeof spawnCode === "string" && !binaryUnavailable) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "startup_failed",
|
|
144
|
+
summary: "the Kimi subprocess could not be started or used.",
|
|
145
|
+
nextStep: "Check the underlying process error; shorten the prompt or environment if an argument-size limit was exceeded, then retry.",
|
|
146
|
+
runtimeProbe: "failed",
|
|
147
|
+
authProbe: "failed",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Keep opaque synchronous failures and legacy async ENOENT diagnostics compatible.
|
|
151
|
+
if (binaryUnavailable || error.code === "CLI_SPAWN_FAILED" || /\bENOENT\b/.test(message)) {
|
|
152
|
+
return {
|
|
153
|
+
kind: "binary_unavailable",
|
|
154
|
+
summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
|
|
155
|
+
nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
|
|
156
|
+
runtimeProbe: "failed",
|
|
157
|
+
authProbe: "failed",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
// v1.0 cli-client surfaces non-ENOENT process errors and non-zero
|
|
156
162
|
// exits as CLI_PROCESS_ERROR / CLI_NONZERO_EXIT. Treat both as
|
|
@@ -18,6 +18,7 @@ export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|me
|
|
|
18
18
|
export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>";
|
|
19
19
|
export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
|
|
20
20
|
export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
|
|
21
|
+
export const RECOVER_SWARM_SUPPORTED_FLAGS = "--apply";
|
|
21
22
|
export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
|
|
22
23
|
export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
|
|
23
24
|
/** Validate syntax only; upstream per-model supportEfforts remains authoritative. */
|
|
@@ -6,7 +6,7 @@ const MAX_DESCENDANT_DEPTH = 8;
|
|
|
6
6
|
const MAX_DESCENDANT_PIDS = 512;
|
|
7
7
|
const PROCESS_EXIT_POLL_MS = 10;
|
|
8
8
|
const PROCESS_SNAPSHOT_TIMEOUT_MS = 1_000;
|
|
9
|
-
const PROCESS_IDENTITY_PROBE_TIMEOUT_MS =
|
|
9
|
+
const PROCESS_IDENTITY_PROBE_TIMEOUT_MS = 1_000;
|
|
10
10
|
const PROBE_TIMEOUT = Symbol("process-probe-timeout");
|
|
11
11
|
/**
|
|
12
12
|
* Capture descendant ancestry and reusable identities in the same bounded
|
|
@@ -23,6 +23,18 @@ export async function collectDescendantIdentities(rootPid) {
|
|
|
23
23
|
}
|
|
24
24
|
return snapshot;
|
|
25
25
|
}
|
|
26
|
+
/** Return current members of the selected process groups, or undefined when unreadable. */
|
|
27
|
+
export async function collectProcessGroupsIdentities(processGroupIds) {
|
|
28
|
+
if (process.platform === "win32")
|
|
29
|
+
return undefined;
|
|
30
|
+
const groups = new Set(processGroupIds.filter((id) => Number.isInteger(id) && id > 0));
|
|
31
|
+
if (groups.size === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const rows = await raceValueWithTimeout(collectProcessSnapshotRows(), PROCESS_SNAPSHOT_TIMEOUT_MS);
|
|
34
|
+
if (rows === PROBE_TIMEOUT || rows === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return rows.filter((row) => groups.has(row.processGroupId)).map(toProcessIdentity);
|
|
37
|
+
}
|
|
26
38
|
/**
|
|
27
39
|
* Compatibility surface for callers that only need numeric process listings.
|
|
28
40
|
* Cancellation must use collectDescendantIdentities so ancestry and identity
|
|
@@ -36,15 +48,21 @@ export async function collectDescendants(rootPid) {
|
|
|
36
48
|
return [];
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
|
-
/**
|
|
40
|
-
export async function
|
|
51
|
+
/** Distinguish a missing process from a failed or timed-out identity probe. */
|
|
52
|
+
export async function probeProcessIdentity(pid) {
|
|
41
53
|
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
42
|
-
return
|
|
54
|
+
return { complete: true };
|
|
43
55
|
const read = process.platform === "linux"
|
|
44
56
|
? readProcessIdentityBatchFromProc([pid])
|
|
45
57
|
: readProcessIdentityBatchWithPs([pid]);
|
|
46
58
|
const batch = await raceValueWithTimeout(read, PROCESS_IDENTITY_PROBE_TIMEOUT_MS);
|
|
47
|
-
|
|
59
|
+
if (batch === PROBE_TIMEOUT)
|
|
60
|
+
return { complete: false };
|
|
61
|
+
return { identity: batch.identities.get(pid), complete: batch.complete };
|
|
62
|
+
}
|
|
63
|
+
/** Read one reusable process identity. PID alone is not stable across teardown. */
|
|
64
|
+
export async function readProcessIdentity(pid) {
|
|
65
|
+
return (await probeProcessIdentity(pid)).identity;
|
|
48
66
|
}
|
|
49
67
|
/**
|
|
50
68
|
* Revalidate all captured identities concurrently under one deadline. The
|
|
@@ -63,16 +81,26 @@ export async function revalidateProcessIdentities(expectedIdentities, options =
|
|
|
63
81
|
if (batch === PROBE_TIMEOUT)
|
|
64
82
|
return { identities: [], complete: false };
|
|
65
83
|
const identities = [];
|
|
84
|
+
const groupChanged = [];
|
|
66
85
|
for (const expectedIdentity of expected) {
|
|
67
86
|
const current = batch.identities.get(expectedIdentity.pid);
|
|
68
|
-
if (current
|
|
69
|
-
current.startToken
|
|
70
|
-
current.
|
|
71
|
-
|
|
87
|
+
if (current === undefined ||
|
|
88
|
+
current.startToken !== expectedIdentity.startToken ||
|
|
89
|
+
isQuiescentState(current.state)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (current.processGroupId !== expectedIdentity.processGroupId) {
|
|
93
|
+
groupChanged.push(current);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
72
96
|
identities.push(current);
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
identities,
|
|
101
|
+
complete: batch.complete,
|
|
102
|
+
...(groupChanged.length > 0 ? { groupChanged } : {}),
|
|
103
|
+
};
|
|
76
104
|
}
|
|
77
105
|
/** Return the current identity only when pid, start token, and PGID still match. */
|
|
78
106
|
export async function revalidateProcessIdentity(expected, identityReader) {
|
|
@@ -104,7 +132,7 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
|
|
|
104
132
|
identityReader: options.identityReader,
|
|
105
133
|
timeoutMs: probeTimeoutMs,
|
|
106
134
|
});
|
|
107
|
-
active = !validation.complete || validation.identities.length > 0;
|
|
135
|
+
active = !validation.complete || validation.identities.length > 0 || (validation.groupChanged?.length ?? 0) > 0;
|
|
108
136
|
}
|
|
109
137
|
if (!active)
|
|
110
138
|
return true;
|
|
@@ -117,13 +145,13 @@ export async function waitForProcessTreeExit(identities, timeoutMs, options = {}
|
|
|
117
145
|
async function collectVerifiedDescendantIdentities(rootPid) {
|
|
118
146
|
const initialRows = await collectProcessSnapshotRows();
|
|
119
147
|
if (initialRows === undefined)
|
|
120
|
-
|
|
148
|
+
throw new Error("process snapshot unavailable");
|
|
121
149
|
const candidateRows = descendantRowsFromSnapshot(rootPid, initialRows);
|
|
122
150
|
const initialRoot = initialRows.find((row) => row.pid === rootPid);
|
|
123
151
|
if (initialRoot === undefined || candidateRows.length === 0)
|
|
124
152
|
return [];
|
|
125
|
-
// A second, targeted snapshot closes the cross-row /proc race:
|
|
126
|
-
//
|
|
153
|
+
// A second, targeted snapshot closes the cross-row /proc race: each child
|
|
154
|
+
// must retain its PID and start token. Reparenting and PGID changes are not exit.
|
|
127
155
|
const expectedRows = [initialRoot, ...candidateRows];
|
|
128
156
|
const expectedByPid = new Map(expectedRows.map((row) => [row.pid, row]));
|
|
129
157
|
const verificationRows = await readCurrentSnapshotRows(expectedRows);
|
|
@@ -131,11 +159,9 @@ async function collectVerifiedDescendantIdentities(rootPid) {
|
|
|
131
159
|
const expected = expectedByPid.get(current.pid);
|
|
132
160
|
return (expected !== undefined &&
|
|
133
161
|
current.startToken === expected.startToken &&
|
|
134
|
-
current.processGroupId === expected.processGroupId &&
|
|
135
|
-
current.parentPid === expected.parentPid &&
|
|
136
162
|
!isQuiescentState(current.state));
|
|
137
163
|
});
|
|
138
|
-
return
|
|
164
|
+
return stableRows.map(toProcessIdentity);
|
|
139
165
|
}
|
|
140
166
|
function descendantRowsFromSnapshot(rootPid, rows) {
|
|
141
167
|
const root = rows.find((row) => row.pid === rootPid);
|
|
@@ -186,6 +212,9 @@ async function readCurrentSnapshotRows(expectedRows) {
|
|
|
186
212
|
Promise.all(procPids.map((pid) => readProcRow(pid))),
|
|
187
213
|
collectProcessSnapshotRowsFromPs(psPids),
|
|
188
214
|
]);
|
|
215
|
+
if (procOutcomes.some((outcome) => outcome.status === "error") || (psPids.length > 0 && psRows === undefined)) {
|
|
216
|
+
throw new Error("process identity verification failed");
|
|
217
|
+
}
|
|
189
218
|
return [
|
|
190
219
|
...procOutcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []),
|
|
191
220
|
...(psRows ?? []),
|
|
@@ -212,6 +241,8 @@ async function collectProcessSnapshotRowsFromProc() {
|
|
|
212
241
|
const outcomes = await Promise.all(entries
|
|
213
242
|
.filter((entry) => /^\d+$/.test(entry))
|
|
214
243
|
.map((entry) => readProcRow(Number.parseInt(entry, 10))));
|
|
244
|
+
if (outcomes.some((outcome) => outcome.status === "error"))
|
|
245
|
+
return undefined;
|
|
215
246
|
return outcomes.flatMap((outcome) => outcome.status === "found" ? [outcome.row] : []);
|
|
216
247
|
}
|
|
217
248
|
async function collectProcessSnapshotRowsFromPs(pids) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
// Node/libuv's default CreateProcess argument quoting (UTF-16 code units).
|
|
3
|
+
export function windowsCommandLineLength(command, args) {
|
|
4
|
+
const quotedLength = (value) => {
|
|
5
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
6
|
+
return value.length;
|
|
7
|
+
return 2 + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1').length;
|
|
8
|
+
};
|
|
9
|
+
return [command, ...args].reduce((size, value) => size + quotedLength(value) + 1, 0);
|
|
10
|
+
}
|
|
11
|
+
export function assertPromptFitsCommandLine(command, args, platform = process.platform) {
|
|
12
|
+
if (platform !== "win32")
|
|
13
|
+
return;
|
|
14
|
+
const size = windowsCommandLineLength(command, args);
|
|
15
|
+
if (size <= 32_767)
|
|
16
|
+
return;
|
|
17
|
+
throw new RuntimeError("CLI_COMMAND_LINE_TOO_LONG", "The Kimi prompt exceeds the Windows command-line limit; the CLI was not started. Narrow the review or split it into smaller scopes. A short ask prompt can reference a saved diff inside the workspace. Reinstalling Kimi or running setup will not fix this. The current print transport does not support stdin prompts.", "cli-client.pre-spawn", { details: { command_line_utf16_units: size, limit_utf16_units: 32_767, platform } });
|
|
18
|
+
}
|