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
|
@@ -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
|
+
}
|