kimi-plugin-cc 2.4.1 → 2.4.3

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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +12 -12
  5. package/dist/cli-client.js +17 -7
  6. package/dist/commands/cancel.js +14 -3
  7. package/dist/commands/recover-swarm.js +135 -0
  8. package/dist/commands/status.js +1 -1
  9. package/dist/commands/swarm.js +51 -54
  10. package/dist/companion.js +5 -1
  11. package/dist/git.js +5 -3
  12. package/dist/help.js +3 -2
  13. package/dist/hooks/config-safety.js +3 -2
  14. package/dist/job-store.js +13 -0
  15. package/dist/jobs.js +3 -0
  16. package/dist/kimi-engine.js +1 -0
  17. package/dist/kimi-errors.js +27 -21
  18. package/dist/parsing.js +1 -0
  19. package/dist/prompt-size.js +18 -0
  20. package/dist/swarm-activation.js +24 -3
  21. package/dist/swarm-recovery.js +134 -0
  22. package/dist/version.js +1 -1
  23. package/docs/registry-releases.md +14 -1
  24. package/docs/safety.md +9 -7
  25. package/docs/swarm-recovery.md +33 -0
  26. package/docs/windows-validation.md +39 -0
  27. package/package.json +6 -3
  28. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  29. package/plugins/kimi-codex/dist/cli-client.js +17 -7
  30. package/plugins/kimi-codex/dist/commands/cancel.js +14 -3
  31. package/plugins/kimi-codex/dist/commands/recover-swarm.js +135 -0
  32. package/plugins/kimi-codex/dist/commands/status.js +1 -1
  33. package/plugins/kimi-codex/dist/commands/swarm.js +51 -54
  34. package/plugins/kimi-codex/dist/companion.js +5 -1
  35. package/plugins/kimi-codex/dist/git.js +5 -3
  36. package/plugins/kimi-codex/dist/help.js +3 -2
  37. package/plugins/kimi-codex/dist/hooks/config-safety.js +3 -2
  38. package/plugins/kimi-codex/dist/job-store.js +13 -0
  39. package/plugins/kimi-codex/dist/jobs.js +3 -0
  40. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  41. package/plugins/kimi-codex/dist/kimi-errors.js +27 -21
  42. package/plugins/kimi-codex/dist/parsing.js +1 -0
  43. package/plugins/kimi-codex/dist/prompt-size.js +18 -0
  44. package/plugins/kimi-codex/dist/swarm-activation.js +24 -3
  45. package/plugins/kimi-codex/dist/swarm-recovery.js +134 -0
  46. package/plugins/kimi-codex/dist/version.js +1 -1
@@ -594,7 +594,7 @@ 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), 2.3.1 (684bae9e) and 2.3.2 (b8dd3a13): the additive
597
+ // 2.3.0 (b5e28fd6), 2.3.1 (684bae9e) 2.3.2 (b8dd3a13), and 2.3.3 (ec495f0c): the additive
598
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) ||
@@ -605,7 +605,8 @@ function validateVersionSensitiveEvents(events, version) {
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
607
  (version.version === "2.3.1" && version.patch === 1) ||
608
- (version.version === "2.3.2" && version.patch === 2))))) {
608
+ (version.version === "2.3.2" && version.patch === 2) ||
609
+ (version.version === "2.3.3" && version.patch === 3))))) {
609
610
  return { valid: true };
610
611
  }
611
612
  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
  }
@@ -42,6 +42,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
42
42
  "2.3.0",
43
43
  "2.3.1",
44
44
  "2.3.2",
45
+ "2.3.3",
45
46
  ]);
46
47
  /** Per-operation native-v2 certification. One green operation never certifies another. */
47
48
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -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
- /\bENOENT\b/.test(message)) {
147
- return {
148
- kind: "binary_unavailable",
149
- summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
150
- nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
151
- runtimeProbe: "failed",
152
- authProbe: "failed",
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. */
@@ -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
+ }
@@ -9,7 +9,7 @@ import { assertExecutionPlanMatchesSpawn, assertNoUnsafeExperimentalSelector, }
9
9
  import { resolveKimiHome } from "./kimi-home.js";
10
10
  import { assertNativeV2Preflight } from "./native-v2-preflight.js";
11
11
  import { collectDescendantIdentities, collectProcessGroupsIdentities, probeProcessIdentity, revalidateProcessIdentities, waitForProcessTreeExit, } from "./process-tree.js";
12
- const PREACTIVATION_VERSION = "2.3.2";
12
+ const PREACTIVATION_VERSIONS = ["2.3.2", "2.3.3"];
13
13
  const READY_TIMEOUT_MS = 10_000;
14
14
  const REST_TIMEOUT_MS = 5_000;
15
15
  const SHUTDOWN_TIMEOUT_MS = 5_000;
@@ -25,7 +25,7 @@ const ESCALATION_MS = 1_500;
25
25
  const POST_KILL_QUIESCENCE_MS = 500;
26
26
  const ENV_MODEL_ALIAS = "__kimi_env_model__";
27
27
  export function requiresSwarmPreactivation(version) {
28
- return version === PREACTIVATION_VERSION;
28
+ return version !== null && PREACTIVATION_VERSIONS.includes(version);
29
29
  }
30
30
  export async function preactivateSwarmSession(options) {
31
31
  const failureCode = "SWARM_ACTIVATION_FAILED";
@@ -39,6 +39,7 @@ export async function preactivateSwarmSession(options) {
39
39
  await verifyMeta(web, prepared.expectedVersion, options.signal, failureCode);
40
40
  const model = await resolveProfileModel(web, options, failureCode);
41
41
  identity = await createSession(web, options, failureCode);
42
+ options.onRecoveryEvent?.({ kind: "session", ...identity });
42
43
  profileMutationAttempted = true;
43
44
  await postSwarmProfile(web, identity, true, model, options.signal, failureCode);
44
45
  await verifySwarmStatus(web, identity, true, model, options.signal, failureCode);
@@ -88,6 +89,7 @@ export async function deactivateSwarmSession(options) {
88
89
  await verifySwarmStatus(web, options, false, undefined, options.signal, failureCode);
89
90
  await shutdownWeb(web, options.signal, failureCode);
90
91
  await requireSwarmJournal(prepared.kimiHome, options, "exit", options.signal, failureCode);
92
+ options.onRecoveryEvent?.({ kind: "disabled" });
91
93
  throwIfAborted(options.signal, failureCode, "swarm-deactivation.complete");
92
94
  }
93
95
  catch (error) {
@@ -114,7 +116,7 @@ async function prepareWebSpawn(options, failureCode) {
114
116
  }
115
117
  const expectedVersion = options.expectedVersion ?? options.executionPlan.kimiVersion;
116
118
  if (expectedVersion === null || !requiresSwarmPreactivation(expectedVersion)) {
117
- throw failure(failureCode, "swarm-activation.version", `Swarm web preactivation requires exact kimi-code ${PREACTIVATION_VERSION}.`);
119
+ throw failure(failureCode, "swarm-activation.version", `Swarm web preactivation requires exact kimi-code ${PREACTIVATION_VERSIONS.join(" or ")}.`);
118
120
  }
119
121
  const kimiHome = resolveKimiHome(options.env, options.cwd);
120
122
  if (options.executionPlan.intendedEngine === "native-v2") {
@@ -144,6 +146,7 @@ async function startWebChild(options, env, failureCode) {
144
146
  "error",
145
147
  ];
146
148
  let child;
149
+ options.onRecoveryEvent?.({ kind: "web-spawn" });
147
150
  try {
148
151
  child = spawn(options.command, args, {
149
152
  cwd: options.cwd,
@@ -153,6 +156,7 @@ async function startWebChild(options, env, failureCode) {
153
156
  });
154
157
  }
155
158
  catch {
159
+ options.onRecoveryEvent?.({ kind: "web-exit" });
156
160
  throw failure(failureCode, "swarm-activation.spawn", "Failed to spawn the Kimi web lifecycle process.");
157
161
  }
158
162
  let exited = false;
@@ -184,6 +188,7 @@ async function startWebChild(options, env, failureCode) {
184
188
  child,
185
189
  closePromise,
186
190
  descendants: [],
191
+ onRecoveryEvent: options.onRecoveryEvent,
187
192
  trackingStopped: false,
188
193
  lastSnapshotComplete: false,
189
194
  snapshotIncomplete: false,
@@ -198,6 +203,8 @@ async function startWebChild(options, env, failureCode) {
198
203
  try {
199
204
  const ready = await waitForReadyLine(owned, options.signal, failureCode);
200
205
  await refreshOwnedDescendants(owned);
206
+ if (owned.recoveryFailed)
207
+ throw failure(failureCode, "swarm-activation.recovery", "Cannot persist web ownership; refusing session mutation.");
201
208
  return Object.assign(owned, ready);
202
209
  }
203
210
  catch (error) {
@@ -610,12 +617,26 @@ async function refreshOwnedDescendants(owned) {
610
617
  owned.snapshotIncomplete = true;
611
618
  owned.lastSnapshotComplete = false;
612
619
  }
620
+ try {
621
+ owned.onRecoveryEvent?.({ kind: "web-processes", identities: [...(owned.rootIdentity ? [owned.rootIdentity] : []), ...owned.descendants], complete: owned.lastSnapshotComplete });
622
+ }
623
+ catch {
624
+ owned.recoveryFailed = true;
625
+ }
613
626
  })().finally(() => {
614
627
  owned.refreshPromise = undefined;
615
628
  });
616
629
  return await owned.refreshPromise;
617
630
  }
618
631
  async function teardownOwnedChild(owned) {
632
+ const quiescent = await teardownOwnedChildInner(owned);
633
+ if (quiescent && !owned.recoveryFailed) {
634
+ owned.onRecoveryEvent?.({ kind: "web-exit" });
635
+ return true;
636
+ }
637
+ return false;
638
+ }
639
+ async function teardownOwnedChildInner(owned) {
619
640
  await stopDescendantTracking(owned);
620
641
  const childPid = owned.child.pid;
621
642
  if (childPid === undefined)
@@ -0,0 +1,134 @@
1
+ import { RuntimeError } from "./errors.js";
2
+ import { collectDescendantIdentities, collectProcessGroupsIdentities, probeProcessIdentity, revalidateProcessIdentities } from "./process-tree.js";
3
+ export function recordRecoveryEvent(store, record, event) {
4
+ record.complete = false;
5
+ if (event.kind === "session") {
6
+ record.session = { sessionId: event.sessionId, workspaceId: event.workspaceId };
7
+ // Write-ahead: this must commit before the profile mutation is attempted.
8
+ record.sessionDirty = true;
9
+ }
10
+ else if (event.kind === "disabled") {
11
+ record.sessionDirty = false;
12
+ }
13
+ else if (event.kind.endsWith("-spawn")) {
14
+ record.launches.push({ kind: event.kind === "web-spawn" ? "web" : "print", pending: true, identities: [], quiescent: false, uncertain: false });
15
+ }
16
+ else {
17
+ const kind = event.kind.startsWith("web-") ? "web" : "print";
18
+ const launch = [...record.launches].reverse().find((entry) => entry.kind === kind);
19
+ if (!launch)
20
+ throw recoveryError("Missing durable launch intent.");
21
+ if (event.kind === "web-exit" || event.kind === "print-exit") {
22
+ launch.quiescent = true;
23
+ launch.pending = false;
24
+ }
25
+ else if ("identities" in event) {
26
+ const all = [...launch.identities, ...event.identities];
27
+ launch.identities = [...new Map(all.map((identity) => [`${identity.pid}:${identity.startToken}:${identity.processGroupId}`, identity])).values()];
28
+ launch.pending = launch.identities.length === 0;
29
+ launch.uncertain ||= !event.complete;
30
+ }
31
+ }
32
+ store.saveSwarmRecovery(record.jobId, record);
33
+ }
34
+ export function finishRecoveryRecord(store, record) {
35
+ record.complete = !record.sessionDirty && record.launches.every((entry) => entry.quiescent)
36
+ && (record.workspaceRoot === null || record.patchPath !== null);
37
+ store.saveSwarmRecovery(record.jobId, record);
38
+ }
39
+ export function recoveryError(message) {
40
+ return new RuntimeError("SWARM_RECOVERY_REQUIRED", message, "swarm.recovery");
41
+ }
42
+ export async function assertRecoveryOwnerGone(record) {
43
+ if (!record.owner || process.platform === "win32")
44
+ throw recoveryError("No durable, verifiable owner identity; recovery requires manual inspection on this host.");
45
+ const probe = await probeProcessIdentity(record.ownerPid);
46
+ if (!probe.complete)
47
+ throw recoveryError("Cannot verify the original companion's identity.");
48
+ if (probe.identity?.startToken === record.owner.startToken && !/^[ZX]/i.test(probe.identity.state)) {
49
+ throw recoveryError("The original companion is still alive. Use cancel for a running job, not recovery.");
50
+ }
51
+ }
52
+ /**
53
+ * Linux may fall back from /proc to ps for a whole-group snapshot while targeted
54
+ * identity reads still use /proc. Those start tokens are different namespaces,
55
+ * not different processes. Prove both representations current around the known
56
+ * identity read; never accept a member based on PID/PGID equality alone.
57
+ */
58
+ export async function validateRecoveryGroupMembers(known, members, validate = revalidateProcessIdentities) {
59
+ const membership = await validate(members);
60
+ const ownership = await validate(known);
61
+ const membershipAfter = await validate(membership.identities);
62
+ for (const proof of [membership, ownership, membershipAfter]) {
63
+ if (!proof.complete || proof.groupChanged?.length)
64
+ throw recoveryError("Process ownership or group membership is uncertain.");
65
+ }
66
+ for (const member of membershipAfter.identities) {
67
+ const owner = ownership.identities.find((id) => id.pid === member.pid && id.processGroupId === member.processGroupId);
68
+ const crossNamespace = owner && ((owner.startToken.startsWith("proc:") && member.startToken.startsWith("ps:")) ||
69
+ (owner.startToken.startsWith("ps:") && member.startToken.startsWith("proc:")));
70
+ if (!owner || (owner.startToken !== member.startToken && !crossNamespace)) {
71
+ throw recoveryError("An unrecorded process survives in the recorded group; refusing group cleanup.");
72
+ }
73
+ }
74
+ return ownership.identities;
75
+ }
76
+ /** Never signal an unproven group, PID replacement, or changed-PGID process. */
77
+ export async function recoverLaunch(launch, persist, signal) {
78
+ if (launch.quiescent)
79
+ return;
80
+ if (launch.pending || launch.uncertain || launch.identities.length === 0) {
81
+ throw recoveryError("Launch ownership was not fully recorded before interruption; manual inspection is required. No process was signalled.");
82
+ }
83
+ const root = launch.identities[0];
84
+ const rootProbe = await probeProcessIdentity(root.pid);
85
+ if (!rootProbe.complete)
86
+ throw recoveryError("Process identity probe is incomplete.");
87
+ if (rootProbe.identity?.startToken === root.startToken) {
88
+ if (rootProbe.identity.processGroupId !== root.processGroupId)
89
+ throw recoveryError("The recorded root changed process groups; refusing to signal it.");
90
+ const descendants = await collectDescendantIdentities(root.pid);
91
+ // Revalidate the ancestry anchor after collection before retaining new identities.
92
+ const after = await probeProcessIdentity(root.pid);
93
+ if (!after.complete || after.identity?.startToken !== root.startToken || after.identity.processGroupId !== root.processGroupId) {
94
+ throw recoveryError("Process ownership changed during recovery inspection.");
95
+ }
96
+ launch.identities = [...new Map([...launch.identities, ...descendants].map((id) => [`${id.pid}:${id.startToken}:${id.processGroupId}`, id])).values()];
97
+ persist();
98
+ }
99
+ const inspect = async () => {
100
+ const members = await collectProcessGroupsIdentities(launch.identities.map((id) => id.processGroupId));
101
+ if (members === undefined)
102
+ throw recoveryError("Process group membership is unreadable.");
103
+ return validateRecoveryGroupMembers(launch.identities, members);
104
+ };
105
+ for (const [killSignal, waitMs] of [["SIGTERM", 1500], ["SIGKILL", 500]]) {
106
+ signal?.throwIfAborted();
107
+ for (const identity of await inspect()) {
108
+ signal?.throwIfAborted();
109
+ const current = await revalidateProcessIdentities([identity]);
110
+ if (!current.complete || current.groupChanged?.length)
111
+ throw recoveryError("Process identity changed before signalling.");
112
+ if (current.identities.length) {
113
+ try {
114
+ process.kill(identity.pid, killSignal);
115
+ }
116
+ catch (error) {
117
+ if (error.code !== "ESRCH")
118
+ throw recoveryError("Unable to stop the recorded process.");
119
+ }
120
+ }
121
+ }
122
+ const deadline = Date.now() + waitMs;
123
+ do {
124
+ signal?.throwIfAborted();
125
+ if ((await inspect()).length === 0) {
126
+ launch.quiescent = true;
127
+ persist();
128
+ return;
129
+ }
130
+ await new Promise((resolve) => setTimeout(resolve, 25));
131
+ } while (Date.now() < deadline);
132
+ }
133
+ throw recoveryError("Recorded processes did not become quiescent; recovery remains pending.");
134
+ }
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.4.1";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.4.3";